From 0aa0425938fc1cbf5a53ccd39764dd186fd2000b Mon Sep 17 00:00:00 2001 From: tomato6966 Date: Tue, 24 Dec 2024 12:39:59 +0100 Subject: [PATCH] v1.2.0 new date management and intervals + slight fixes --- package.json | 2 +- src/components/InvestmentForm.tsx | 88 ++++++++++++------- .../Modals/EditSavingsPlanModal.tsx | 40 ++++++--- .../Modals/FutureProjectionModal.tsx | 16 ++-- src/components/PortfolioChart.tsx | 63 ++++++++----- src/components/PortfolioTable.tsx | 6 +- src/components/utils/DateRangePicker.tsx | 57 ++++++++---- src/providers/PortfolioProvider.tsx | 6 +- src/services/yahooFinanceService.ts | 8 +- src/types/index.ts | 22 ++--- src/utils/calculations/assetValue.ts | 76 +++++++++++----- src/utils/calculations/futureProjection.ts | 10 +-- src/utils/calculations/performance.ts | 32 +++---- src/utils/calculations/portfolioValue.ts | 33 ++++--- 14 files changed, 285 insertions(+), 174 deletions(-) diff --git a/package.json b/package.json index 47f9812..26b406c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "investment-portfolio-tracker", "private": true, - "version": "1.1.1", + "version": "1.2.0", "type": "module", "scripts": { "dev": "vite", diff --git a/src/components/InvestmentForm.tsx b/src/components/InvestmentForm.tsx index 74bf48f..214a6f8 100644 --- a/src/components/InvestmentForm.tsx +++ b/src/components/InvestmentForm.tsx @@ -63,17 +63,25 @@ export default function InvestmentFormWrapper() { ); } +interface IntervalConfig { + value: number; + unit: 'days' | 'months' | 'years'; +} + const InvestmentForm = ({ assetId }: { assetId: string }) => { const [type, setType] = useState<'single' | 'periodic'>('single'); const [amount, setAmount] = useState(''); const [date, setDate] = useState(''); const [dayOfMonth, setDayOfMonth] = useState('1'); - const [interval, setInterval] = useState('30'); const [isDynamic, setIsDynamic] = useState(false); const [dynamicType, setDynamicType] = useState<'percentage' | 'fixed'>('percentage'); const [dynamicValue, setDynamicValue] = useState(''); const [yearInterval, setYearInterval] = useState('1'); const [isSubmitting, setIsSubmitting] = useState(false); + const [intervalConfig, setIntervalConfig] = useState({ + value: 1, + unit: 'months' + }); const { dateRange, addInvestment } = usePortfolioSelector((state) => ({ dateRange: state.dateRange, @@ -84,14 +92,9 @@ const InvestmentForm = ({ assetId }: { assetId: string }) => { e.preventDefault(); e.stopPropagation(); - console.log("submitting") - console.time('generatePeriodicInvestments'); - console.timeLog('generatePeriodicInvestments', "1"); - setIsSubmitting(true); setTimeout(() => { - console.log("timeout") try { if (type === "single") { const investment = { @@ -99,16 +102,17 @@ const InvestmentForm = ({ assetId }: { assetId: string }) => { assetId, type, amount: parseFloat(amount), - date + date: new Date(date), }; addInvestment(assetId, investment); toast.success('Investment added successfully'); } else { const periodicSettings = { - startDate: date, + startDate: new Date(date), dayOfMonth: parseInt(dayOfMonth), - interval: parseInt(interval), + interval: intervalConfig.value, amount: parseFloat(amount), + intervalUnit: intervalConfig.unit, ...(isDynamic ? { dynamic: { type: dynamicType, @@ -117,23 +121,19 @@ const InvestmentForm = ({ assetId }: { assetId: string }) => { }, } : undefined), }; - console.timeLog('generatePeriodicInvestments', "2"); const investments = generatePeriodicInvestments( periodicSettings, - dateRange.endDate, + new Date(dateRange.endDate), assetId ); - console.timeLog('generatePeriodicInvestments', "3"); addInvestment(assetId, investments); - toast.success('Periodic investment plan created successfully'); + toast.success('Sparplan erfolgreich erstellt'); } } catch (error:any) { - toast.error('Failed to add investment. Please try again.' + String(error?.message || error)); + toast.error('Fehler beim Erstellen des Investments: ' + String(error?.message || error)); } finally { - console.timeLog('generatePeriodicInvestments', "4"); - console.timeEnd('generatePeriodicInvestments'); setIsSubmitting(false); setAmount(''); } @@ -193,27 +193,51 @@ const InvestmentForm = ({ assetId }: { assetId: string }) => { required /> - - setDate(e.target.value)} - className="w-full p-2 border rounded dark:bg-slate-800 dark:border-slate-700 dark:outline-none dark:text-gray-300 [&::-webkit-calendar-picker-indicator]:dark:invert" - required - />
+
+ setIntervalConfig(prev => ({ + ...prev, + value: parseInt(e.target.value) + }))} + className="w-24 p-2 border rounded dark:bg-slate-800 dark:border-slate-700 dark:outline-none dark:text-gray-300" + min="1" + required + /> + +
+
+ +
+ setInterval(e.target.value)} - className="w-full p-2 border rounded dark:bg-slate-800 dark:border-slate-700 dark:outline-none dark:text-gray-300" - min="14" - max="365" + type="date" + value={date} + onChange={(e) => setDate(e.target.value)} + className="w-full p-2 border rounded dark:bg-slate-800 dark:border-slate-700 dark:outline-none dark:text-gray-300 [&::-webkit-calendar-picker-indicator]:dark:invert" required + lang="de" />
diff --git a/src/components/Modals/EditSavingsPlanModal.tsx b/src/components/Modals/EditSavingsPlanModal.tsx index 3816f41..f27df1b 100644 --- a/src/components/Modals/EditSavingsPlanModal.tsx +++ b/src/components/Modals/EditSavingsPlanModal.tsx @@ -3,6 +3,7 @@ import { useState } from "react"; import toast from "react-hot-toast"; import { usePortfolioSelector } from "../../hooks/usePortfolio"; +import { PeriodicSettings } from "../../types"; import { generatePeriodicInvestments } from "../../utils/calculations/assetValue"; interface EditSavingsPlanModalProps { @@ -31,6 +32,7 @@ export const EditSavingsPlanModal = ({ const [amount, setAmount] = useState(initialAmount.toString()); const [dayOfMonth, setDayOfMonth] = useState(initialDayOfMonth.toString()); const [interval, setInterval] = useState(initialInterval.toString()); + const [intervalUnit, setIntervalUnit] = useState<'days' | 'weeks' | 'months' | 'quarters' | 'years'>('months'); const [isDynamic, setIsDynamic] = useState(!!initialDynamic); const [dynamicType, setDynamicType] = useState<'percentage' | 'fixed'>(initialDynamic?.type || 'percentage'); const [dynamicValue, setDynamicValue] = useState(initialDynamic?.value.toString() || ''); @@ -61,10 +63,11 @@ export const EditSavingsPlanModal = ({ }); // Generate and add new investments - const periodicSettings = { - startDate, + const periodicSettings: PeriodicSettings = { + startDate: new Date(startDate), dayOfMonth: parseInt(dayOfMonth), interval: parseInt(interval), + intervalUnit: intervalUnit, amount: parseFloat(amount), ...(isDynamic ? { dynamic: { @@ -134,17 +137,28 @@ export const EditSavingsPlanModal = ({
- - setInterval(e.target.value)} - className="w-full p-2 border rounded dark:bg-slate-800 dark:border-slate-700 dark:outline-none dark:text-gray-300" - min="1" - required - /> + +
+ setInterval(e.target.value)} + className="w-24 p-2 border rounded dark:bg-slate-800 dark:border-slate-700 dark:outline-none dark:text-gray-300" + min="1" + required + /> + +
diff --git a/src/components/Modals/FutureProjectionModal.tsx b/src/components/Modals/FutureProjectionModal.tsx index 7a50bd8..2c31174 100644 --- a/src/components/Modals/FutureProjectionModal.tsx +++ b/src/components/Modals/FutureProjectionModal.tsx @@ -1,3 +1,4 @@ +import { isSameDay } from "date-fns"; import { BarChart as BarChartIcon, LineChart as LineChartIcon, Loader2, X } from "lucide-react"; import { useCallback, useState } from "react"; import { @@ -9,7 +10,6 @@ import { calculateFutureProjection } from "../../utils/calculations/futureProjec import { formatCurrency } from "../../utils/formatters"; import type { ProjectionData, SustainabilityAnalysis, WithdrawalPlan } from "../../types"; - interface FutureProjectionModalProps { performancePerAnno: number; bestPerformancePerAnno: { percentage: number, year: number }[]; @@ -39,7 +39,7 @@ export const FutureProjectionModal = ({ amount: 0, interval: 'monthly', startTrigger: 'auto', - startDate: new Date().toISOString().split('T')[0], + startDate: new Date(), startPortfolioValue: 0, enabled: false, autoStrategy: { @@ -65,8 +65,8 @@ export const FutureProjectionModal = ({ ); setProjectionData(projection); setSustainabilityAnalysis(sustainability); - const slicedBestCase = bestPerformancePerAnno.slice(0, Math.floor(bestPerformancePerAnno.length / 2)); - const slicedWorstCase = worstPerformancePerAnno.slice(0, Math.floor(worstPerformancePerAnno.length / 2)); + const slicedBestCase = bestPerformancePerAnno.slice(0, bestPerformancePerAnno.length > 1 ? Math.floor(bestPerformancePerAnno.length / 2) : 1); + const slicedWorstCase = worstPerformancePerAnno.slice(0, worstPerformancePerAnno.length > 1 ? Math.floor(worstPerformancePerAnno.length / 2) : 1); const bestCase = slicedBestCase.reduce((acc, curr) => acc + curr.percentage, 0) / slicedBestCase.length || 0; const worstCase = slicedWorstCase.reduce((acc, curr) => acc + curr.percentage, 0) / slicedWorstCase.length || 0; @@ -335,8 +335,8 @@ export const FutureProjectionModal = ({ // Create a merged and sorted dataset for consistent x-axis const mergedData = projectionData.map(basePoint => { const date = basePoint.date; - const bestPoint = scenarios.best.projection.find(p => p.date === date); - const worstPoint = scenarios.worst.projection.find(p => p.date === date); + const bestPoint = scenarios.best.projection.find(p => isSameDay(p.date, date)); + const worstPoint = scenarios.worst.projection.find(p => isSameDay(p.date, date)); return { date, @@ -549,10 +549,10 @@ export const FutureProjectionModal = ({ setWithdrawalPlan(prev => ({ ...prev, - startDate: e.target.value + startDate: new Date(e.target.value) }))} min={new Date().toISOString().split('T')[0]} className="w-full p-2 border rounded dark:bg-slate-700 dark:border-slate-600 dark:text-gray-200" diff --git a/src/components/PortfolioChart.tsx b/src/components/PortfolioChart.tsx index 9693a25..43e6f12 100644 --- a/src/components/PortfolioChart.tsx +++ b/src/components/PortfolioChart.tsx @@ -1,5 +1,5 @@ import { format } from "date-fns"; -import { BarChart2, Eye, EyeOff, Maximize2, X } from "lucide-react"; +import { BarChart2, Eye, EyeOff, Maximize2, RefreshCcw, X } from "lucide-react"; import { useCallback, useMemo, useState } from "react"; import { CartesianGrid, Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis @@ -27,7 +27,7 @@ export default function PortfolioChart() { })); const fetchHistoricalData = useCallback( - async (startDate: string, endDate: string) => { + async (startDate: Date, endDate: Date) => { for (const asset of assets) { const { historicalData, longName } = await getHistoricalData(asset.symbol, startDate, endDate); updateAssetHistoricalData(asset.id, historicalData, longName); @@ -66,14 +66,15 @@ export default function PortfolioChart() { // Calculate percentage changes for each asset const processedData = useMemo(() => data.map(point => { - const processed: { [key: string]: number | string } = { - date: point.date, + const processed: { date: string, total: number, invested: number, percentageChange: number, ttwor: number, ttwor_percent: number, [key: string]: number | string } = { + date: format(point.date, 'yyyy-MM-dd'), total: point.total, invested: point.invested, percentageChange: point.percentageChange, + ttwor: 0, + ttwor_percent: 0, }; - processed["ttwor"] = 0; for (const asset of assets) { const initialPrice = data[0].assets[asset.id]; const currentPrice = point.assets[asset.id]; @@ -81,11 +82,11 @@ export default function PortfolioChart() { processed[`${asset.id}_price`] = currentPrice; const percentDecimal = ((currentPrice - initialPrice) / initialPrice); processed[`${asset.id}_percent`] = percentDecimal * 100; - processed["ttwor"] += allAssetsInvestedKapitals[asset.id] + allAssetsInvestedKapitals[asset.id] * percentDecimal; + processed.ttwor += allAssetsInvestedKapitals[asset.id] + allAssetsInvestedKapitals[asset.id] * percentDecimal; } } - processed["ttwor_percent"] = (processed["ttwor"] - Object.values(allAssetsInvestedKapitals).reduce((acc, curr) => acc + curr, 0)) / Object.values(allAssetsInvestedKapitals).reduce((acc, curr) => acc + curr, 0) * 100; + processed.ttwor_percent = (processed.ttwor - Object.values(allAssetsInvestedKapitals).reduce((acc, curr) => acc + curr, 0)) / Object.values(allAssetsInvestedKapitals).reduce((acc, curr) => acc + curr, 0) * 100; // add a processed["ttwor"] ttwor is what if you invested all of the kapital of all assets at the start of the period @@ -170,6 +171,12 @@ export default function PortfolioChart() { debouncedFetchHistoricalData(newRange.startDate, newRange.endDate); }, [updateDateRange, debouncedFetchHistoricalData]); + const [renderKey, setRenderKey] = useState(0); + + const handleReRender = useCallback(() => { + setRenderKey(prevKey => prevKey + 1); + }, []); + const ChartContent = useCallback(() => ( <>
@@ -179,26 +186,34 @@ export default function PortfolioChart() { onStartDateChange={(date) => handleUpdateDateRange({ ...dateRange, startDate: date })} onEndDateChange={(date) => handleUpdateDateRange({ ...dateRange, endDate: date })} /> - +
+ + +
-
+
- + format(new Date(date), 'MMM dd')} + tickFormatter={(date) => format(new Date(date), 'dd.MM.yyyy')} /> `${value.toLocaleString()}€`} + tickFormatter={(value) => `${value.toFixed(2)}€`} /> format(new Date(date), 'MMM dd, yyyy')} - /> + labelFormatter={(date) => format(new Date(date), 'dd.MM.yyyy')} + > + } />
- Note: The YAxis on the left shows the value of your portfolio (black line) and invested capital (dotted line), + *Note: The YAxis on the left shows the value of your portfolio (black line) and invested capital (dotted line), all other assets are scaled by their % gain/loss and thus scaled to the right YAxis. +

+ **Note: The % is based on daily weighted average data, thus the percentages might alter slightly. +

- ), [assets, isDarkMode, assetColors, handleUpdateDateRange, hideAssets, hiddenAssets, processedData, CustomLegend, dateRange, isFullscreen]); + ), [assets, handleReRender, isDarkMode, assetColors, handleUpdateDateRange, hideAssets, hiddenAssets, processedData, CustomLegend, dateRange, isFullscreen, renderKey]); if (isFullscreen) { return ( -
+

Portfolio Chart

@@ -70,11 +92,12 @@ export const DateRangePicker = ({
diff --git a/src/providers/PortfolioProvider.tsx b/src/providers/PortfolioProvider.tsx index 9bb69a3..d335b75 100644 --- a/src/providers/PortfolioProvider.tsx +++ b/src/providers/PortfolioProvider.tsx @@ -1,4 +1,4 @@ -import { format, startOfYear } from "date-fns"; +import { startOfYear } from "date-fns"; import { createContext, useMemo, useReducer } from "react"; import { Asset, DateRange, HistoricalData, Investment } from "../types"; @@ -29,8 +29,8 @@ const initialState: PortfolioState = { assets: [], isLoading: false, dateRange: { - startDate: format(startOfYear(new Date()), 'yyyy-MM-dd'), - endDate: format(new Date(), 'yyyy-MM-dd'), + startDate: startOfYear(new Date()), + endDate: new Date(), }, }; diff --git a/src/services/yahooFinanceService.ts b/src/services/yahooFinanceService.ts index a0b107f..a712c94 100644 --- a/src/services/yahooFinanceService.ts +++ b/src/services/yahooFinanceService.ts @@ -55,10 +55,10 @@ export const searchAssets = async (query: string): Promise => { } }; -export const getHistoricalData = async (symbol: string, startDate: string, endDate: string) => { +export const getHistoricalData = async (symbol: string, startDate: Date, endDate: Date) => { try { - const start = Math.floor(new Date(startDate).getTime() / 1000); - const end = Math.floor(new Date(endDate).getTime() / 1000); + const start = Math.floor(startDate.getTime() / 1000); + const end = Math.floor(endDate.getTime() / 1000); const params = new URLSearchParams({ period1: start.toString(), @@ -76,7 +76,7 @@ export const getHistoricalData = async (symbol: string, startDate: string, endDa return { historicalData: timestamp.map((time: number, index: number) => ({ - date: new Date(time * 1000).toISOString().split('T')[0], + date: new Date(time * 1000), price: quotes.close[index], })), longName: meta.longName diff --git a/src/types/index.ts b/src/types/index.ts index 7e981eb..c01c1b3 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -11,7 +11,7 @@ export interface Asset { } export interface HistoricalData { - date: string; + date: Date; price: number; } @@ -20,13 +20,15 @@ export interface Investment { assetId: string; type: 'single' | 'periodic'; amount: number; - date?: string; + date?: Date; periodicGroupId?: string; } export interface PeriodicSettings { dayOfMonth: number; interval: number; + intervalUnit: 'days' | 'weeks' | 'months' | 'quarters' | 'years'; + startDate: Date; dynamic?: { type: 'percentage' | 'fixed'; value: number; @@ -37,7 +39,7 @@ export interface PeriodicSettings { export interface InvestmentPerformance { id: string; assetName: string; - date: string; + date: Date; investedAmount: number; investedAtPrice: number; currentValue: number; @@ -46,14 +48,14 @@ export interface InvestmentPerformance { } export interface DateRange { - startDate: string; - endDate: string; + startDate: Date; + endDate: Date; } export interface InvestmentPerformance { id: string; assetName: string; - date: string; + date: Date; investedAmount: number; investedAtPrice: number; currentValue: number; @@ -75,7 +77,7 @@ export interface PortfolioPerformance { } export type DayData = { - date: string; + date: Date; total: number; invested: number; percentageChange: number; @@ -87,7 +89,7 @@ export interface WithdrawalPlan { amount: number; interval: 'monthly' | 'yearly'; startTrigger: 'date' | 'portfolioValue' | 'auto'; - startDate?: string; + startDate?: Date; startPortfolioValue?: number; enabled: boolean; autoStrategy?: { @@ -98,7 +100,7 @@ export interface WithdrawalPlan { } export interface ProjectionData { - date: string; + date: Date; value: number; invested: number; withdrawals: number; @@ -112,7 +114,7 @@ export interface SustainabilityAnalysis { } export interface PeriodicSettings { - startDate: string; + startDate: Date; dayOfMonth: number; interval: number; amount: number; diff --git a/src/utils/calculations/assetValue.ts b/src/utils/calculations/assetValue.ts index d049235..a7bc672 100644 --- a/src/utils/calculations/assetValue.ts +++ b/src/utils/calculations/assetValue.ts @@ -1,4 +1,6 @@ -import { isAfter, isBefore, isSameDay } from "date-fns"; +import { + addDays, addMonths, addWeeks, addYears, isAfter, isBefore, isSameDay, setDate +} from "date-fns"; import type { Asset, Investment, PeriodicSettings } from "../../types"; @@ -13,7 +15,7 @@ export const calculateAssetValueAtDate = (asset: Asset, date: Date, currentPrice // Find price at investment date const investmentPrice = asset.historicalData.find( - (data) => data.date === investment.date + (data) => isSameDay(data.date, invDate) )?.price || 0; // if no investment price found, use the previous price @@ -39,26 +41,42 @@ export const calculateAssetValueAtDate = (asset: Asset, date: Date, currentPrice } }; -export const generatePeriodicInvestments = (settings: PeriodicSettings, endDate: string, assetId: string): Investment[] => { + +export const generatePeriodicInvestments = (settings: PeriodicSettings, endDate: Date, assetId: string): Investment[] => { const investments: Investment[] = []; const periodicGroupId = crypto.randomUUID(); - let currentDate = new Date(settings.startDate); + + // Create UTC dates + let currentDate = new Date(Date.UTC( + settings.startDate.getUTCFullYear(), + settings.startDate.getUTCMonth(), + settings.startDate.getUTCDate() + )); + + const end = new Date(Date.UTC( + endDate.getUTCFullYear(), + endDate.getUTCMonth(), + endDate.getUTCDate() + )); + let currentAmount = settings.amount; - const end = new Date(endDate); while (currentDate <= end) { - // Only create investment if it's on the specified day of month - if (currentDate.getDate() === settings.dayOfMonth) { + // For monthly/yearly intervals, ensure we're on the correct day of month + if (settings.intervalUnit !== 'days') { + currentDate = setDate(currentDate, settings.dayOfMonth); + } + + // Only add investment if we haven't passed the end date + if (currentDate <= end) { // Handle dynamic increases if configured if (settings.dynamic) { const yearsSinceStart = - (currentDate.getTime() - new Date(settings.startDate).getTime()) / + (currentDate.getTime() - settings.startDate.getTime()) / (1000 * 60 * 60 * 24 * 365); - // Check if we've reached a year interval for increase if (yearsSinceStart > 0 && yearsSinceStart % settings.dynamic.yearInterval === 0) { if (settings.dynamic.type === 'percentage') { - console.log('percentage', settings.dynamic.value, (1 + (settings.dynamic.value / 100))); currentAmount *= (1 + (settings.dynamic.value / 100)); } else { currentAmount += settings.dynamic.value; @@ -70,24 +88,38 @@ export const generatePeriodicInvestments = (settings: PeriodicSettings, endDate: id: crypto.randomUUID(), type: 'periodic', amount: currentAmount, - date: currentDate.toISOString().split('T')[0], + date: currentDate, periodicGroupId, assetId }); } - // Move to next interval day - const nextDate = new Date(currentDate); - nextDate.setDate(nextDate.getDate() + settings.interval); - - // Ensure we maintain the correct day of month - if (nextDate.getDate() !== settings.dayOfMonth) { - nextDate.setDate(1); - nextDate.setMonth(nextDate.getMonth() + 1); - nextDate.setDate(settings.dayOfMonth); + // Calculate next date based on interval unit + switch (settings.intervalUnit) { + case 'days': + currentDate = addDays(currentDate, settings.interval); + break; + case 'weeks': + currentDate = addWeeks(currentDate, settings.interval); + break; + case 'months': + currentDate = addMonths(currentDate, settings.interval); + // Ensure we maintain the correct day of month using UTC + if (currentDate.getUTCDate() !== settings.dayOfMonth) { + currentDate = setDate(currentDate, settings.dayOfMonth); + } + break; + case 'quarters': + currentDate = addMonths(currentDate, settings.interval * 3); + break; + case 'years': + currentDate = addYears(currentDate, settings.interval); + // Ensure we maintain the correct day of month using UTC + if (currentDate.getUTCDate() !== settings.dayOfMonth) { + currentDate = setDate(currentDate, settings.dayOfMonth); + } + break; } - - currentDate = nextDate; } return investments; diff --git a/src/utils/calculations/futureProjection.ts b/src/utils/calculations/futureProjection.ts index c40206e..7609c1a 100644 --- a/src/utils/calculations/futureProjection.ts +++ b/src/utils/calculations/futureProjection.ts @@ -1,4 +1,4 @@ -import { addMonths, differenceInYears, format } from "date-fns"; +import { addMonths, differenceInYears } from "date-fns"; import type { ProjectionData, SustainabilityAnalysis, WithdrawalPlan, Asset, Investment @@ -10,7 +10,7 @@ const findOptimalStartingPoint = ( desiredWithdrawal: number, strategy: WithdrawalPlan['autoStrategy'], interval: 'monthly' | 'yearly' -): { startDate: string; requiredPortfolioValue: number } => { +): { startDate: Date; requiredPortfolioValue: number } => { const monthlyWithdrawal = interval === 'yearly' ? desiredWithdrawal / 12 : desiredWithdrawal; let requiredPortfolioValue = 0; @@ -42,7 +42,7 @@ const findOptimalStartingPoint = ( startDate.setMonth(startDate.getMonth() + Math.max(0, monthsToReach)); return { - startDate: startDate.toISOString().split('T')[0], + startDate, requiredPortfolioValue, }; }; @@ -105,7 +105,7 @@ export const calculateFutureProjection = async ( future.push({ ...lastInvestment, - date: format(currentDate, 'yyyy-MM-dd'), + date: currentDate, amount: currentAmount, }); } @@ -208,7 +208,7 @@ export const calculateFutureProjection = async ( // Only add to projection data if within display timeframe if (currentDate <= endDateForDisplay) { projectionData.push({ - date: format(currentDate, 'yyyy-MM-dd'), + date: currentDate, value: Math.max(0, portfolioValue), invested: totalInvested, withdrawals: monthlyWithdrawal, diff --git a/src/utils/calculations/performance.ts b/src/utils/calculations/performance.ts index 483b73a..0704402 100644 --- a/src/utils/calculations/performance.ts +++ b/src/utils/calculations/performance.ts @@ -1,4 +1,4 @@ -import { isAfter, isBefore } from "date-fns"; +import { isAfter, isBefore, isSameDay } from "date-fns"; import type { Asset, InvestmentPerformance, PortfolioPerformance } from "../../types"; @@ -76,16 +76,18 @@ export const calculateInvestmentPerformance = (assets: Asset[]): PortfolioPerfor ); for (const investment of relevantInvestments) { + const invDate = new Date(investment.date!); + const investmentPrice = asset.historicalData.find( - (data) => data.date === investment.date + (data) => isSameDay(data.date, invDate) )?.price || 0; const previousPrice = investmentPrice || asset.historicalData.filter( - (data) => isBefore(new Date(data.date), new Date(investment.date!)) + (data) => isBefore(new Date(data.date), invDate) ).reverse().find((v) => v.price !== 0)?.price || 0; const buyInPrice = investmentPrice || previousPrice || asset.historicalData.filter( - (data) => isAfter(new Date(data.date), new Date(investment.date!)) + (data) => isAfter(new Date(data.date), invDate) ).find((v) => v.price !== 0)?.price || 0; if (buyInPrice > 0) { @@ -128,16 +130,17 @@ export const calculateInvestmentPerformance = (assets: Asset[]): PortfolioPerfor const currentPrice = asset.historicalData[asset.historicalData.length - 1]?.price || 0; for (const investment of asset.investments) { + const invDate = new Date(investment.date!); const investmentPrice = asset.historicalData.find( - (data) => data.date === investment.date + (data) => isSameDay(data.date, invDate) )?.price || 0; const previousPrice = investmentPrice || asset.historicalData.filter( - (data) => isBefore(new Date(data.date), new Date(investment.date!)) + (data) => isBefore(new Date(data.date), invDate) ).reverse().find((v) => v.price !== 0)?.price || 0; const buyInPrice = investmentPrice || previousPrice || asset.historicalData.filter( - (data) => isAfter(new Date(data.date), new Date(investment.date!)) + (data) => isAfter(new Date(data.date), invDate) ).find((v) => v.price !== 0)?.price || 0; const shares = buyInPrice > 0 ? investment.amount / buyInPrice : 0; @@ -165,24 +168,9 @@ export const calculateInvestmentPerformance = (assets: Asset[]): PortfolioPerfor ? ((ttworValue - totalInvested) / totalInvested) * 100 : 0; - // Berechne die jährliche Performance - // const performancePerAnnoPerformance = (() => { - // if (!earliestDate || totalInvested === 0) return 0; - - // const years = differenceInDays(new Date(), earliestDate) / 365; - // if (years < 0.01) return 0; // Verhindere Division durch sehr kleine Zahlen - - // // Formel: (1 + r)^n = FV/PV - // // r = (FV/PV)^(1/n) - 1 - // const totalReturn = totalCurrentValue / totalInvested; - // const annualizedReturn = Math.pow(totalReturn, 1 / years) - 1; - - // return annualizedReturn * 100; - // })(); const performancePerAnnoPerformance = annualPerformances.reduce((acc, curr) => acc + curr.percentage, 0) / annualPerformances.length; - console.log(performancePerAnnoPerformance, annualPerformances); return { investments, summary: { diff --git a/src/utils/calculations/portfolioValue.ts b/src/utils/calculations/portfolioValue.ts index b7f0f52..d7e4482 100644 --- a/src/utils/calculations/portfolioValue.ts +++ b/src/utils/calculations/portfolioValue.ts @@ -1,4 +1,4 @@ -import { addDays, isAfter, isBefore } from "date-fns"; +import { addDays, isAfter, isBefore, isSameDay } from "date-fns"; import { calculateAssetValueAtDate } from "./assetValue"; @@ -8,14 +8,14 @@ export const calculatePortfolioValue = (assets: Asset[], dateRange: DateRange) = const { startDate, endDate } = dateRange; const data: DayData[] = []; - let currentDate = new Date(startDate); - const end = new Date(endDate); + let currentDate = startDate; + const end = endDate; const beforeValue: { [assetId: string]: number } = {}; while (isBefore(currentDate, end)) { const dayData: DayData = { - date: currentDate.toISOString().split('T')[0], + date: currentDate, total: 0, invested: 0, percentageChange: 0, @@ -38,7 +38,7 @@ export const calculatePortfolioValue = (assets: Asset[], dateRange: DateRange) = // Get historical price for the asset const currentValueOfAsset = asset.historicalData.find( - (data) => data.date === dayData.date + (data) => isSameDay(data.date, dayData.date) )?.price || beforeValue[asset.id]; beforeValue[asset.id] = currentValueOfAsset; @@ -52,13 +52,14 @@ export const calculatePortfolioValue = (assets: Asset[], dateRange: DateRange) = dayData.total += investedValue || 0; dayData.assets[asset.id] = currentValueOfAsset; - const percent = ((currentValueOfAsset - avgBuyIn) / avgBuyIn) * 100; - if (!Number.isNaN(percent) && investedValue && investedValue > 0) { - weightedPercents.push({ - percent, - weight: investedValue - }); - } + const performancePercentage = investedValue > 0 + ? ((currentValueOfAsset - avgBuyIn) / avgBuyIn) * 100 + : 0; + + weightedPercents.push({ + percent: performancePercentage, + weight: investedValue + }); } } @@ -71,6 +72,14 @@ export const calculatePortfolioValue = (assets: Asset[], dateRange: DateRange) = dayData.percentageChange = 0; } + const totalInvested = dayData.invested; // Total invested amount for the day + const totalCurrentValue = dayData.total; // Total current value for the day + + dayData.percentageChange = totalInvested > 0 + ? ((totalCurrentValue - totalInvested) / totalInvested) * 100 + : 0; + + currentDate = addDays(currentDate, 1); data.push(dayData); }