diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a105dd492..721c285e6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed the missing first data point in the portfolio evolution chart on the analysis page - Fixed the end date of calendar year date ranges in the portfolio performance calculation for instances in time zones with a negative UTC offset - Fixed the missing mapping for Korea in the country weightings of the _Financial Modeling Prep_ service +- Fixed the alignment of the average price in the chart of the holding detail dialog for custom asset profiles ## 3.81.0 - 2026-10-07 diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts index 8c3ec2206e..2c3d197c7a 100644 --- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts +++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts @@ -9,6 +9,7 @@ import { } from '@ghostfolio/common/config'; import { CreateOrderDto } from '@ghostfolio/common/dtos'; import { + alignLineChartItemsToDates, DATE_FORMAT, downloadAsFile, getCountryName @@ -69,6 +70,7 @@ import { SortDirection } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; import { MatTabsModule } from '@angular/material/tabs'; import { NavigationStart, Router, RouterModule } from '@angular/router'; +import { utc } from '@date-fns/utc'; import { IonIcon } from '@ionic/angular/standalone'; import { isUUID } from 'class-validator'; import { format, isSameMonth, isToday, parseISO } from 'date-fns'; @@ -719,12 +721,19 @@ export class GfHoldingDetailDialogComponent implements OnInit { this.historicalDataItems = this.marketDataItems.map( ({ date, marketPrice }) => { return { - date: format(date, DATE_FORMAT), + date: format(date, DATE_FORMAT, { in: utc }), value: marketPrice }; } ); + this.benchmarkDataItems = alignLineChartItemsToDates({ + dates: this.historicalDataItems.map(({ date }) => { + return date; + }), + items: this.benchmarkDataItems + }); + this.changeDetectorRef.markForCheck(); }); } diff --git a/libs/common/src/lib/helper.spec.ts b/libs/common/src/lib/helper.spec.ts index 71b934628c..6a0248d810 100644 --- a/libs/common/src/lib/helper.spec.ts +++ b/libs/common/src/lib/helper.spec.ts @@ -3,6 +3,7 @@ import { TAG_ID_EXCLUDE_FROM_ANALYSIS } from '@ghostfolio/common/config'; import { + alignLineChartItemsToDates, canApplyFiltersToAccess, extractNumberFromString, getCountryCodeFromCurrency, @@ -25,6 +26,66 @@ import { import { UserSettings } from '@ghostfolio/common/interfaces'; describe('Helper', () => { + describe('Align line chart items to dates', () => { + it('Dates before the first item', () => { + expect( + alignLineChartItemsToDates({ + dates: ['2025-01-02', '2025-01-03'], + items: [{ date: '2026-01-09', value: 100 }] + }) + ).toEqual([ + { date: '2025-01-02', value: null }, + { date: '2025-01-03', value: null } + ]); + }); + + it('Dates on and after the items', () => { + expect( + alignLineChartItemsToDates({ + dates: [ + '2026-01-08', + '2026-01-09', + '2026-01-10', + '2026-01-12', + '2026-01-13' + ], + items: [ + { date: '2026-01-09', value: 100 }, + { date: '2026-01-12', value: 110 } + ] + }) + ).toEqual([ + { date: '2026-01-08', value: null }, + { date: '2026-01-09', value: 100 }, + { date: '2026-01-10', value: 100 }, + { date: '2026-01-12', value: 110 }, + { date: '2026-01-13', value: 110 } + ]); + }); + + it('Null values', () => { + expect( + alignLineChartItemsToDates({ + dates: ['2026-01-09', '2026-01-10', '2026-01-11'], + items: [ + { date: '2026-01-09', value: 100 }, + { date: '2026-01-10', value: null } + ] + }) + ).toEqual([ + { date: '2026-01-09', value: 100 }, + { date: '2026-01-10', value: null }, + { date: '2026-01-11', value: null } + ]); + }); + + it('No items', () => { + expect( + alignLineChartItemsToDates({ dates: ['2026-01-09'], items: [] }) + ).toEqual([{ date: '2026-01-09', value: null }]); + }); + }); + describe('Can apply filters to access', () => { it('An access of the model context protocol', () => { expect(canApplyFiltersToAccess({ type: 'MCP' })).toEqual(false); diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index 7e4e13de7e..a355faec99 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -60,6 +60,7 @@ import { AssetProfileIdentifier, AssetProfileItem, Benchmark, + NullableLineChartItem, PortfolioPosition, UserSettings } from './interfaces'; @@ -94,6 +95,27 @@ const USER_SETTINGS_KEYS_OF_AUTHENTICATED_USER: (keyof UserSettings)[] = [ const VALID_COUNTRY_CODES = new Set([...Object.keys(countries), 'EU']); +export function alignLineChartItemsToDates({ + dates, + items +}: { + dates: string[]; + items: NullableLineChartItem[]; +}): NullableLineChartItem[] { + let index = -1; + + return dates.map((date) => { + while ( + index + 1 < items.length && + !isAfter(parseISO(items[index + 1].date), parseISO(date)) + ) { + index++; + } + + return { date, value: items[index]?.value ?? null }; + }); +} + export function applyAssetProfileOverrides>( assetProfile: T, assetProfileOverrides: AssetProfileOverrides | null