Browse Source

Bugfix/date ranges in time zones other than UTC (#8099)

* Fix start date of date ranges for instances in time zones with negative UTC offset

* Update changelog
main
Thomas Kaul 10 hours ago
committed by GitHub
parent
commit
8fa4f6a673
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 9
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  3. 44
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.spec.ts
  4. 8
      apps/api/src/app/portfolio/portfolio.service.spec.ts
  5. 10
      apps/api/src/app/portfolio/portfolio.service.ts
  6. 24
      libs/common/src/lib/calculation-helper.spec.ts
  7. 20
      libs/common/src/lib/calculation-helper.ts

1
CHANGELOG.md

@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Fixed the _Copy AI prompt to clipboard_ actions on the analysis page in impersonation mode to be based on the impersonated user (experimental)
- Fixed the start date of the date ranges for instances in time zones with a negative UTC offset
## 3.83.0 - 2026-10-10

9
apps/api/src/app/portfolio/calculator/portfolio-calculator.ts

@ -64,7 +64,6 @@ import {
format,
isAfter,
isBefore,
isEqual,
isFuture,
isPast,
isWithinInterval,
@ -1418,8 +1417,8 @@ export abstract class PortfolioCalculator {
let netPerformanceAtStartDate: number;
let netPerformanceWithCurrencyEffectAtStartDate: number;
const endDate = this.toChartDate(end);
const startDate = this.toChartDate(start);
const endDate = resetHours(end);
const startDate = resetHours(start);
for (const historicalDataItem of historicalData) {
const date = resetHours(parseDate(historicalDataItem.date));
@ -1914,8 +1913,4 @@ export abstract class PortfolioCalculator {
await this.initialize(attempt + 1);
}
}
private toChartDate(aDate: Date) {
return isEqual(aDate, endOfDay(aDate)) ? resetHours(aDate) : aDate;
}
}

44
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.spec.ts

@ -190,6 +190,10 @@ describe('PortfolioCalculator', () => {
});
describe('get performance', () => {
afterEach(() => {
jest.useRealTimers();
});
it('with a calendar year date range', async () => {
const snapshot = await portfolioCalculator.getSnapshot();
@ -226,5 +230,45 @@ describe('PortfolioCalculator', () => {
})
).toEqual(['2016-12-31', '2017-12-31']);
});
it('with a year to date date range', async () => {
const snapshot = await portfolioCalculator.getSnapshot();
snapshot.historicalData = [
getHistoricalDataItem({
date: '2016-12-30',
investment: 100,
netPerformance: 0
}),
getHistoricalDataItem({
date: '2016-12-31',
investment: 100,
netPerformance: 10
}),
getHistoricalDataItem({
date: '2017-01-01',
investment: 100,
netPerformance: 20
})
];
// 1 January 2017, 10:00 in New York
jest.useFakeTimers().setSystemTime(new Date('2017-01-01T15:00:00.000Z'));
const { endDate, startDate } = getIntervalFromDateRange({
dateRange: 'ytd'
});
const { chart } = await portfolioCalculator.getPerformance({
end: endDate,
start: startDate
});
expect(
chart.map(({ date }) => {
return date;
})
).toEqual(['2016-12-31', '2017-01-01']);
});
});
});

8
apps/api/src/app/portfolio/portfolio.service.spec.ts

@ -16,7 +16,7 @@ import {
TAG_ID_EXCLUDE_FROM_ANALYSIS,
UNKNOWN_KEY
} from '@ghostfolio/common/config';
import { parseDate, resetHours } from '@ghostfolio/common/helper';
import { parseDate } from '@ghostfolio/common/helper';
import {
Activity,
AssetProfileIdentifier,
@ -1045,7 +1045,7 @@ describe('PortfolioService', () => {
expect(getPerformanceOfCalculator).toHaveBeenCalledTimes(3);
expect(getPerformanceOfCalculator).toHaveBeenNthCalledWith(2, {
end: endOfDay(parseDate('2024-12-31')),
start: resetHours(parseDate('2024-06-15'))
start: parseDate('2024-06-15')
});
expect(getPerformanceOfCalculator).toHaveBeenNthCalledWith(3, {
end: endOfDay(parseDate('2025-06-15')),
@ -1099,11 +1099,11 @@ describe('PortfolioService', () => {
expect(getPerformanceOfCalculator).toHaveBeenCalledTimes(3);
expect(getPerformanceOfCalculator).toHaveBeenNthCalledWith(1, {
end: endOfDay(parseDate('2026-01-01')),
start: resetHours(parseDate('2025-01-01'))
start: parseDate('2025-01-01')
});
expect(getPerformanceOfCalculator).toHaveBeenNthCalledWith(2, {
end: endOfDay(parseDate('2025-12-31')),
start: resetHours(parseDate('2025-01-01'))
start: parseDate('2025-01-01')
});
expect(getPerformanceOfCalculator).toHaveBeenNthCalledWith(3, {
end: endOfDay(parseDate('2026-01-01')),

10
apps/api/src/app/portfolio/portfolio.service.ts

@ -47,6 +47,7 @@ import {
isAccountExcluded,
isDraftActivity,
parseDate,
resetHours,
resolveUserSettings
} from '@ghostfolio/common/helper';
import {
@ -559,8 +560,13 @@ export class PortfolioService {
const { historicalData } = await portfolioCalculator.getSnapshot();
const items = historicalData.filter(({ date }) => {
return !isBefore(date, startDate) && !isAfter(date, endDate);
const items = historicalData.filter((historicalDataItem) => {
const date = resetHours(parseDate(historicalDataItem.date));
return (
!isBefore(date, resetHours(startDate)) &&
!isAfter(date, resetHours(endDate))
);
});
let investments: InvestmentItem[];

24
libs/common/src/lib/calculation-helper.spec.ts

@ -6,9 +6,14 @@ import {
getIntervalFromDateRange
} from './calculation-helper';
import { DATE_FORMAT } from './helper';
import { DateRange } from './types';
describe('CalculationHelper', () => {
describe('interval from date range', () => {
afterEach(() => {
jest.useRealTimers();
});
it('Get interval of a calendar year', async () => {
const { endDate, startDate } = getIntervalFromDateRange({
dateRange: '2024'
@ -23,6 +28,25 @@ describe('CalculationHelper', () => {
// part of the interval
expect(startDate.getTime()).toEqual(new Date(2024, 0, 1).getTime() - 1);
});
it('Get interval of a date range relative to today', () => {
jest.useFakeTimers().setSystemTime(new Date(2024, 9, 10, 22, 0));
expect(
(['1d', 'mtd', 'wtd', 'ytd', '1y', '5y'] as DateRange[]).map(
(dateRange) => {
return getIntervalFromDateRange({ dateRange }).startDate;
}
)
).toEqual([
new Date(2024, 9, 9),
new Date(2024, 8, 30),
new Date(2024, 9, 6),
new Date(2023, 11, 31),
new Date(2023, 9, 10),
new Date(2019, 9, 10)
]);
});
});
describe('annualized performance percentage', () => {

20
libs/common/src/lib/calculation-helper.ts

@ -3,6 +3,7 @@ import {
endOfDay,
endOfYear,
max,
startOfDay,
startOfMonth,
startOfWeek,
startOfYear,
@ -12,7 +13,6 @@ import {
} from 'date-fns';
import { isFinite, isNumber } from 'lodash-es';
import { resetHours } from './helper';
import { DateRange } from './types';
export function getAnnualizedPerformancePercent({
@ -48,31 +48,25 @@ export function getIntervalFromDateRange(params: {
switch (dateRange) {
case '1d':
startDate = max([startDate, subDays(resetHours(new Date()), 1)]);
startDate = max([startDate, subDays(startOfDay(new Date()), 1)]);
break;
case 'mtd':
startDate = max([
startDate,
subDays(startOfMonth(resetHours(new Date())), 1)
]);
startDate = max([startDate, subDays(startOfMonth(new Date()), 1)]);
break;
case 'wtd':
startDate = max([
startDate,
subDays(startOfWeek(resetHours(new Date()), { weekStartsOn: 1 }), 1)
subDays(startOfWeek(new Date(), { weekStartsOn: 1 }), 1)
]);
break;
case 'ytd':
startDate = max([
startDate,
subDays(startOfYear(resetHours(new Date())), 1)
]);
startDate = max([startDate, subDays(startOfYear(new Date()), 1)]);
break;
case '1y':
startDate = max([startDate, subYears(resetHours(new Date()), 1)]);
startDate = max([startDate, subYears(startOfDay(new Date()), 1)]);
break;
case '5y':
startDate = max([startDate, subYears(resetHours(new Date()), 5)]);
startDate = max([startDate, subYears(startOfDay(new Date()), 5)]);
break;
case 'max':
break;

Loading…
Cancel
Save