Feature/extend date range support by specific years (#3190)

* Extend date range support by specific years

* Support date range in benchmark endpoint

* Support date range in activities endpoint

* Update changelog
pull/3212/head
Thomas Kaul 6 months ago committed by GitHub
parent 5ffc39c32f
commit f1eeee0525
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Added
- Added the date range support in the activities table on the portfolio activities page (experimental)
- Extended the date range support by specific years (`2023`, `2022`, `2021`, etc.) in the assistant (experimental)
### Changed ### Changed
- Improved the usability to delete an asset profile in the historical market data table and the asset profile details dialog of the admin control - Improved the usability to delete an asset profile in the historical market data table and the asset profile details dialog of the admin control

@ -1,5 +1,6 @@
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { getInterval } from '@ghostfolio/api/helper/portfolio.helper';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
import type { import type {
@ -8,7 +9,7 @@ import type {
UniqueAsset UniqueAsset
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions'; import { permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types'; import type { DateRange, RequestWithUser } from '@ghostfolio/common/types';
import { import {
Body, Body,
@ -19,6 +20,7 @@ import {
Inject, Inject,
Param, Param,
Post, Post,
Query,
UseGuards, UseGuards,
UseInterceptors UseInterceptors
} from '@nestjs/common'; } from '@nestjs/common';
@ -106,13 +108,18 @@ export class BenchmarkController {
public async getBenchmarkMarketDataBySymbol( public async getBenchmarkMarketDataBySymbol(
@Param('dataSource') dataSource: DataSource, @Param('dataSource') dataSource: DataSource,
@Param('startDateString') startDateString: string, @Param('startDateString') startDateString: string,
@Param('symbol') symbol: string @Param('symbol') symbol: string,
@Query('range') dateRange: DateRange = 'max'
): Promise<BenchmarkMarketDataDetails> { ): Promise<BenchmarkMarketDataDetails> {
const startDate = new Date(startDateString); const { endDate, startDate } = getInterval(
dateRange,
new Date(startDateString)
);
const userCurrency = this.request.user.Settings.settings.baseCurrency; const userCurrency = this.request.user.Settings.settings.baseCurrency;
return this.benchmarkService.getMarketDataBySymbol({ return this.benchmarkService.getMarketDataBySymbol({
dataSource, dataSource,
endDate,
startDate, startDate,
symbol, symbol,
userCurrency userCurrency

@ -1,5 +1,6 @@
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { getInterval } from '@ghostfolio/api/helper/portfolio.helper';
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor'; import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
@ -8,7 +9,7 @@ import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/da
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types'; import type { DateRange, RequestWithUser } from '@ghostfolio/common/types';
import { import {
Body, Body,
@ -84,6 +85,7 @@ export class OrderController {
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId, @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId,
@Query('accounts') filterByAccounts?: string, @Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string, @Query('assetClasses') filterByAssetClasses?: string,
@Query('range') dateRange: DateRange = 'max',
@Query('skip') skip?: number, @Query('skip') skip?: number,
@Query('sortColumn') sortColumn?: string, @Query('sortColumn') sortColumn?: string,
@Query('sortDirection') sortDirection?: Prisma.SortOrder, @Query('sortDirection') sortDirection?: Prisma.SortOrder,
@ -96,14 +98,18 @@ export class OrderController {
filterByTags filterByTags
}); });
const { endDate, startDate } = getInterval(dateRange);
const impersonationUserId = const impersonationUserId =
await this.impersonationService.validateImpersonationId(impersonationId); await this.impersonationService.validateImpersonationId(impersonationId);
const userCurrency = this.request.user.Settings.settings.baseCurrency; const userCurrency = this.request.user.Settings.settings.baseCurrency;
const { activities, count } = await this.orderService.getOrders({ const { activities, count } = await this.orderService.getOrders({
endDate,
filters, filters,
sortColumn, sortColumn,
sortDirection, sortDirection,
startDate,
userCurrency, userCurrency,
includeDrafts: true, includeDrafts: true,
skip: isNaN(skip) ? undefined : skip, skip: isNaN(skip) ? undefined : skip,

@ -198,22 +198,26 @@ export class OrderService {
} }
public async getOrders({ public async getOrders({
endDate,
filters, filters,
includeDrafts = false, includeDrafts = false,
skip, skip,
sortColumn, sortColumn,
sortDirection, sortDirection,
startDate,
take = Number.MAX_SAFE_INTEGER, take = Number.MAX_SAFE_INTEGER,
types, types,
userCurrency, userCurrency,
userId, userId,
withExcludedAccounts = false withExcludedAccounts = false
}: { }: {
endDate?: Date;
filters?: Filter[]; filters?: Filter[];
includeDrafts?: boolean; includeDrafts?: boolean;
skip?: number; skip?: number;
sortColumn?: string; sortColumn?: string;
sortDirection?: Prisma.SortOrder; sortDirection?: Prisma.SortOrder;
startDate?: Date;
take?: number; take?: number;
types?: ActivityType[]; types?: ActivityType[];
userCurrency: string; userCurrency: string;
@ -225,6 +229,18 @@ export class OrderService {
]; ];
const where: Prisma.OrderWhereInput = { userId }; const where: Prisma.OrderWhereInput = { userId };
if (endDate || startDate) {
where.AND = [];
if (endDate) {
where.AND.push({ date: { lte: endDate } });
}
if (startDate) {
where.AND.push({ date: { gt: startDate } });
}
}
const { const {
ACCOUNT: filtersByAccount, ACCOUNT: filtersByAccount,
ASSET_CLASS: filtersByAssetClass, ASSET_CLASS: filtersByAssetClass,

@ -6,6 +6,7 @@ import {
hasNotDefinedValuesInObject, hasNotDefinedValuesInObject,
nullifyValuesInObject nullifyValuesInObject
} from '@ghostfolio/api/helper/object.helper'; } from '@ghostfolio/api/helper/object.helper';
import { getInterval } from '@ghostfolio/api/helper/portfolio.helper';
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor'; import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
@ -236,8 +237,12 @@ export class PortfolioController {
await this.impersonationService.validateImpersonationId(impersonationId); await this.impersonationService.validateImpersonationId(impersonationId);
const userCurrency = this.request.user.Settings.settings.baseCurrency; const userCurrency = this.request.user.Settings.settings.baseCurrency;
const { endDate, startDate } = getInterval(dateRange);
const { activities } = await this.orderService.getOrders({ const { activities } = await this.orderService.getOrders({
endDate,
filters, filters,
startDate,
userCurrency, userCurrency,
userId: impersonationUserId || this.request.user.id, userId: impersonationUserId || this.request.user.id,
types: ['DIVIDEND'] types: ['DIVIDEND']
@ -245,7 +250,6 @@ export class PortfolioController {
let dividends = await this.portfolioService.getDividends({ let dividends = await this.portfolioService.getDividends({
activities, activities,
dateRange,
groupBy groupBy
}); });

@ -5,7 +5,10 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interf
import { OrderService } from '@ghostfolio/api/app/order/order.service'; import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { UserService } from '@ghostfolio/api/app/user/user.service'; import { UserService } from '@ghostfolio/api/app/user/user.service';
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper'; import {
getFactor,
getInterval
} from '@ghostfolio/api/helper/portfolio.helper';
import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment'; import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment';
import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account'; import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account';
import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/base-currency-current-investment'; import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/base-currency-current-investment';
@ -73,16 +76,8 @@ import {
isBefore, isBefore,
isSameMonth, isSameMonth,
isSameYear, isSameYear,
isValid,
max,
min,
parseISO, parseISO,
set, set
startOfWeek,
startOfMonth,
startOfYear,
subDays,
subYears
} from 'date-fns'; } from 'date-fns';
import { isEmpty, last, uniq, uniqBy } from 'lodash'; import { isEmpty, last, uniq, uniqBy } from 'lodash';
@ -221,11 +216,9 @@ export class PortfolioService {
public async getDividends({ public async getDividends({
activities, activities,
dateRange = 'max',
groupBy groupBy
}: { }: {
activities: Activity[]; activities: Activity[];
dateRange?: DateRange;
groupBy?: GroupBy; groupBy?: GroupBy;
}): Promise<InvestmentItem[]> { }): Promise<InvestmentItem[]> {
let dividends = activities.map(({ date, valueInBaseCurrency }) => { let dividends = activities.map(({ date, valueInBaseCurrency }) => {
@ -239,14 +232,7 @@ export class PortfolioService {
dividends = this.getDividendsByGroup({ dividends, groupBy }); dividends = this.getDividendsByGroup({ dividends, groupBy });
} }
const startDate = this.getStartDate( return dividends;
dateRange,
parseDate(dividends[0]?.date)
);
return dividends.filter(({ date }) => {
return !isBefore(parseDate(date), startDate);
});
} }
public async getInvestments({ public async getInvestments({
@ -375,7 +361,7 @@ export class PortfolioService {
exchangeRateDataService: this.exchangeRateDataService exchangeRateDataService: this.exchangeRateDataService
}); });
const startDate = this.getStartDate( const { startDate } = getInterval(
dateRange, dateRange,
portfolioCalculator.getStartDate() portfolioCalculator.getStartDate()
); );
@ -960,7 +946,10 @@ export class PortfolioService {
const userId = await this.getUserId(impersonationId, this.request.user.id); const userId = await this.getUserId(impersonationId, this.request.user.id);
const user = await this.userService.user({ id: userId }); const user = await this.userService.user({ id: userId });
const { endDate, startDate } = getInterval(dateRange);
const { activities } = await this.orderService.getOrders({ const { activities } = await this.orderService.getOrders({
endDate,
filters, filters,
userId, userId,
types: ['BUY', 'SELL'], types: ['BUY', 'SELL'],
@ -981,12 +970,10 @@ export class PortfolioService {
exchangeRateDataService: this.exchangeRateDataService exchangeRateDataService: this.exchangeRateDataService
}); });
const startDate = this.getStartDate( const currentPositions = await portfolioCalculator.getCurrentPositions(
dateRange, startDate,
portfolioCalculator.getStartDate() endDate
); );
const currentPositions =
await portfolioCalculator.getCurrentPositions(startDate);
let positions = currentPositions.positions.filter(({ quantity }) => { let positions = currentPositions.positions.filter(({ quantity }) => {
return !quantity.eq(0); return !quantity.eq(0);
@ -1133,7 +1120,10 @@ export class PortfolioService {
) )
); );
const { endDate, startDate } = getInterval(dateRange);
const { activities } = await this.orderService.getOrders({ const { activities } = await this.orderService.getOrders({
endDate,
filters, filters,
userCurrency, userCurrency,
userId, userId,
@ -1169,16 +1159,6 @@ export class PortfolioService {
exchangeRateDataService: this.exchangeRateDataService exchangeRateDataService: this.exchangeRateDataService
}); });
const portfolioStart = min(
[
parseDate(accountBalanceItems[0]?.date),
portfolioCalculator.getStartDate()
].filter((date) => {
return isValid(date);
})
);
const startDate = this.getStartDate(dateRange, portfolioStart);
const { const {
currentValueInBaseCurrency, currentValueInBaseCurrency,
errors, errors,
@ -1192,7 +1172,7 @@ export class PortfolioService {
netPerformancePercentageWithCurrencyEffect, netPerformancePercentageWithCurrencyEffect,
netPerformanceWithCurrencyEffect, netPerformanceWithCurrencyEffect,
totalInvestment totalInvestment
} = await portfolioCalculator.getCurrentPositions(startDate); } = await portfolioCalculator.getCurrentPositions(startDate, endDate);
let currentNetPerformance = netPerformance; let currentNetPerformance = netPerformance;
@ -1448,11 +1428,11 @@ export class PortfolioService {
userId = await this.getUserId(impersonationId, userId); userId = await this.getUserId(impersonationId, userId);
const startDate = this.getStartDate( const { endDate, startDate } = getInterval(
dateRange, dateRange,
portfolioCalculator.getStartDate() portfolioCalculator.getStartDate()
); );
const endDate = new Date();
const daysInMarket = differenceInDays(endDate, startDate) + 1; const daysInMarket = differenceInDays(endDate, startDate) + 1;
const step = withDataDecimation const step = withDataDecimation
? Math.round(daysInMarket / Math.min(daysInMarket, MAX_CHART_ITEMS)) ? Math.round(daysInMarket / Math.min(daysInMarket, MAX_CHART_ITEMS))
@ -1617,52 +1597,6 @@ export class PortfolioService {
}; };
} }
private getStartDate(aDateRange: DateRange, portfolioStart: Date) {
switch (aDateRange) {
case '1d':
portfolioStart = max([
portfolioStart,
subDays(new Date().setHours(0, 0, 0, 0), 1)
]);
break;
case 'mtd':
portfolioStart = max([
portfolioStart,
subDays(startOfMonth(new Date().setHours(0, 0, 0, 0)), 1)
]);
break;
case 'wtd':
portfolioStart = max([
portfolioStart,
subDays(
startOfWeek(new Date().setHours(0, 0, 0, 0), { weekStartsOn: 1 }),
1
)
]);
break;
case 'ytd':
portfolioStart = max([
portfolioStart,
subDays(startOfYear(new Date().setHours(0, 0, 0, 0)), 1)
]);
break;
case '1y':
portfolioStart = max([
portfolioStart,
subYears(new Date().setHours(0, 0, 0, 0), 1)
]);
break;
case '5y':
portfolioStart = max([
portfolioStart,
subYears(new Date().setHours(0, 0, 0, 0), 5)
]);
break;
}
return portfolioStart;
}
private getStreaks({ private getStreaks({
investments, investments,
savingsRate savingsRate

@ -14,6 +14,7 @@ import {
IsOptional, IsOptional,
IsString IsString
} from 'class-validator'; } from 'class-validator';
import { eachYearOfInterval, format } from 'date-fns';
export class UpdateUserSettingDto { export class UpdateUserSettingDto {
@IsNumber() @IsNumber()
@ -32,7 +33,20 @@ export class UpdateUserSettingDto {
@IsOptional() @IsOptional()
colorScheme?: ColorScheme; colorScheme?: ColorScheme;
@IsIn(<DateRange[]>['1d', '1y', '5y', 'max', 'mtd', 'wtd', 'ytd']) @IsIn(<DateRange[]>[
'1d',
'1y',
'5y',
'max',
'mtd',
'wtd',
'ytd',
...eachYearOfInterval({ end: new Date(), start: new Date(0) }).map(
(date) => {
return format(date, 'yyyy');
}
)
])
@IsOptional() @IsOptional()
dateRange?: DateRange; dateRange?: DateRange;

@ -51,13 +51,22 @@ export class UserService {
{ Account, id, permissions, Settings, subscription }: UserWithSettings, { Account, id, permissions, Settings, subscription }: UserWithSettings,
aLocale = locale aLocale = locale
): Promise<IUser> { ): Promise<IUser> {
const access = await this.prismaService.access.findMany({ let [access, firstActivity, tags] = await Promise.all([
include: { this.prismaService.access.findMany({
User: true include: {
}, User: true
orderBy: { alias: 'asc' }, },
where: { GranteeUser: { id } } orderBy: { alias: 'asc' },
}); where: { GranteeUser: { id } }
}),
this.prismaService.order.findFirst({
orderBy: {
date: 'asc'
},
where: { userId: id }
}),
this.tagService.getByUser(id)
]);
let systemMessage: SystemMessage; let systemMessage: SystemMessage;
@ -69,8 +78,6 @@ export class UserService {
systemMessage = systemMessageProperty; systemMessage = systemMessageProperty;
} }
let tags = await this.tagService.getByUser(id);
if ( if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
subscription.type === 'Basic' subscription.type === 'Basic'
@ -91,6 +98,7 @@ export class UserService {
}; };
}), }),
accounts: Account, accounts: Account,
dateOfFirstActivity: firstActivity?.date ?? new Date(),
settings: { settings: {
...(<UserSettings>Settings.settings), ...(<UserSettings>Settings.settings),
locale: (<UserSettings>Settings.settings)?.locale ?? aLocale locale: (<UserSettings>Settings.settings)?.locale ?? aLocale

@ -1,4 +1,17 @@
import { resetHours } from '@ghostfolio/common/helper';
import { DateRange } from '@ghostfolio/common/types';
import { Type as ActivityType } from '@prisma/client'; import { Type as ActivityType } from '@prisma/client';
import {
endOfDay,
max,
subDays,
startOfMonth,
startOfWeek,
startOfYear,
subYears,
endOfYear
} from 'date-fns';
export function getFactor(activityType: ActivityType) { export function getFactor(activityType: ActivityType) {
let factor: number; let factor: number;
@ -19,3 +32,49 @@ export function getFactor(activityType: ActivityType) {
return factor; return factor;
} }
export function getInterval(
aDateRange: DateRange,
portfolioStart = new Date(0)
) {
let endDate = endOfDay(new Date());
let startDate = portfolioStart;
switch (aDateRange) {
case '1d':
startDate = max([startDate, subDays(resetHours(new Date()), 1)]);
break;
case 'mtd':
startDate = max([
startDate,
subDays(startOfMonth(resetHours(new Date())), 1)
]);
break;
case 'wtd':
startDate = max([
startDate,
subDays(startOfWeek(resetHours(new Date()), { weekStartsOn: 1 }), 1)
]);
break;
case 'ytd':
startDate = max([
startDate,
subDays(startOfYear(resetHours(new Date())), 1)
]);
break;
case '1y':
startDate = max([startDate, subYears(resetHours(new Date()), 1)]);
break;
case '5y':
startDate = max([startDate, subYears(resetHours(new Date()), 5)]);
break;
case 'max':
break;
default:
// '2024', '2023', '2022', etc.
endDate = endOfYear(new Date(aDateRange));
startDate = max([startDate, new Date(aDateRange)]);
}
return { endDate, startDate };
}

@ -6,7 +6,6 @@ import {
} from '@ghostfolio/common/chart-helper'; } from '@ghostfolio/common/chart-helper';
import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config'; import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config';
import { import {
DATE_FORMAT,
getBackgroundColor, getBackgroundColor,
getDateFormatString, getDateFormatString,
getLocale, getLocale,
@ -39,16 +38,8 @@ import {
} from 'chart.js'; } from 'chart.js';
import 'chartjs-adapter-date-fns'; import 'chartjs-adapter-date-fns';
import annotationPlugin from 'chartjs-plugin-annotation'; import annotationPlugin from 'chartjs-plugin-annotation';
import { import { isAfter, isValid, min, subDays } from 'date-fns';
addDays, import { first } from 'lodash';
format,
isAfter,
isValid,
min,
parseISO,
subDays
} from 'date-fns';
import { first, last } from 'lodash';
@Component({ @Component({
selector: 'gf-investment-chart', selector: 'gf-investment-chart',
@ -112,46 +103,6 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
Object.assign({}, item) Object.assign({}, item)
); );
if (!this.groupBy && this.investments?.length > 0) {
let date: string;
if (this.range === 'max') {
// Extend chart by 5% of days in market (before)
date = format(
subDays(
parseISO(this.investments[0].date),
this.daysInMarket * 0.05 || 90
),
DATE_FORMAT
);
this.investments.unshift({
date,
investment: 0
});
this.values.unshift({
date,
value: 0
});
}
// Extend chart by 5% of days in market (after)
date = format(
addDays(
parseDate(last(this.investments).date),
this.daysInMarket * 0.05 || 90
),
DATE_FORMAT
);
this.investments.push({
date,
investment: last(this.investments).investment
});
this.values.push({
date,
value: last(this.values).value
});
}
const chartData: ChartData<'bar' | 'line'> = { const chartData: ChartData<'bar' | 'line'> = {
labels: this.historicalDataItems.map(({ date }) => { labels: this.historicalDataItems.map(({ date }) => {
return parseDate(date); return parseDate(date);
@ -303,7 +254,6 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
display: false display: false
}, },
min: scaleXMin, min: scaleXMin,
suggestedMax: new Date().toISOString(),
type: 'time', type: 'time',
time: { time: {
tooltipFormat: getDateFormatString(this.locale), tooltipFormat: getDateFormatString(this.locale),

@ -124,6 +124,9 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
this.dataService this.dataService
.fetchActivities({ .fetchActivities({
filters: this.userService.getFilters(), filters: this.userService.getFilters(),
range: this.user?.settings?.isExperimentalFeatures
? this.user?.settings?.dateRange
: undefined,
skip: this.pageIndex * this.pageSize, skip: this.pageIndex * this.pageSize,
sortColumn: this.sortColumn, sortColumn: this.sortColumn,
sortDirection: this.sortDirection, sortDirection: this.sortDirection,

@ -352,6 +352,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
.fetchBenchmarkBySymbol({ .fetchBenchmarkBySymbol({
dataSource, dataSource,
symbol, symbol,
range: this.user?.settings?.dateRange,
startDate: this.firstOrderDate startDate: this.firstOrderDate
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))

@ -159,12 +159,14 @@ export class DataService {
public fetchActivities({ public fetchActivities({
filters, filters,
range,
skip, skip,
sortColumn, sortColumn,
sortDirection, sortDirection,
take take
}: { }: {
filters?: Filter[]; filters?: Filter[];
range?: DateRange;
skip?: number; skip?: number;
sortColumn?: string; sortColumn?: string;
sortDirection?: SortDirection; sortDirection?: SortDirection;
@ -172,6 +174,10 @@ export class DataService {
}): Observable<Activities> { }): Observable<Activities> {
let params = this.buildFiltersAsQueryParams({ filters }); let params = this.buildFiltersAsQueryParams({ filters });
if (range) {
params = params.append('range', range);
}
if (skip) { if (skip) {
params = params.append('skip', skip); params = params.append('skip', skip);
} }
@ -269,16 +275,25 @@ export class DataService {
public fetchBenchmarkBySymbol({ public fetchBenchmarkBySymbol({
dataSource, dataSource,
range,
startDate, startDate,
symbol symbol
}: { }: {
range: DateRange;
startDate: Date; startDate: Date;
} & UniqueAsset): Observable<BenchmarkMarketDataDetails> { } & UniqueAsset): Observable<BenchmarkMarketDataDetails> {
let params = new HttpParams();
if (range) {
params = params.append('range', range);
}
return this.http.get<BenchmarkMarketDataDetails>( return this.http.get<BenchmarkMarketDataDetails>(
`/api/v1/benchmark/${dataSource}/${symbol}/${format( `/api/v1/benchmark/${dataSource}/${symbol}/${format(
startDate, startDate,
DATE_FORMAT DATE_FORMAT
)}` )}`,
{ params }
); );
} }

@ -82,6 +82,10 @@ export class UserService extends ObservableStore<UserStoreState> {
private fetchUser(): Observable<User> { private fetchUser(): Observable<User> {
return this.http.get<any>('/api/v1/user').pipe( return this.http.get<any>('/api/v1/user').pipe(
map((user) => { map((user) => {
if (user.dateOfFirstActivity) {
user.dateOfFirstActivity = parseISO(user.dateOfFirstActivity);
}
if (user.settings?.retirementDate) { if (user.settings?.retirementDate) {
user.settings.retirementDate = parseISO(user.settings.retirementDate); user.settings.retirementDate = parseISO(user.settings.retirementDate);
} }

@ -13,6 +13,7 @@ export interface User {
id: string; id: string;
}[]; }[];
accounts: Account[]; accounts: Account[];
dateOfFirstActivity: Date;
id: string; id: string;
permissions: string[]; permissions: string[];
settings: UserSettings; settings: UserSettings;

@ -1 +1,9 @@
export type DateRange = '1d' | '1y' | '5y' | 'max' | 'mtd' | 'wtd' | 'ytd'; export type DateRange =
| '1d'
| '1y'
| '5y'
| 'max'
| 'mtd'
| 'wtd'
| 'ytd'
| string; // '2024', '2023', '2022', etc.

@ -24,6 +24,7 @@ import {
import { FormBuilder, FormControl } from '@angular/forms'; import { FormBuilder, FormControl } from '@angular/forms';
import { MatMenuTrigger } from '@angular/material/menu'; import { MatMenuTrigger } from '@angular/material/menu';
import { Account, AssetClass } from '@prisma/client'; import { Account, AssetClass } from '@prisma/client';
import { eachYearOfInterval, format } from 'date-fns';
import { EMPTY, Observable, Subject, lastValueFrom } from 'rxjs'; import { EMPTY, Observable, Subject, lastValueFrom } from 'rxjs';
import { import {
catchError, catchError,
@ -35,7 +36,11 @@ import {
} from 'rxjs/operators'; } from 'rxjs/operators';
import { AssistantListItemComponent } from './assistant-list-item/assistant-list-item.component'; import { AssistantListItemComponent } from './assistant-list-item/assistant-list-item.component';
import { ISearchResultItem, ISearchResults } from './interfaces/interfaces'; import {
IDateRangeOption,
ISearchResultItem,
ISearchResults
} from './interfaces/interfaces';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@ -95,27 +100,7 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit {
public accounts: Account[] = []; public accounts: Account[] = [];
public assetClasses: Filter[] = []; public assetClasses: Filter[] = [];
public dateRangeFormControl = new FormControl<string>(undefined); public dateRangeFormControl = new FormControl<string>(undefined);
public readonly dateRangeOptions = [ public dateRangeOptions: IDateRangeOption[] = [];
{ label: $localize`Today`, value: '1d' },
{
label: $localize`Week to date` + ' (' + $localize`WTD` + ')',
value: 'wtd'
},
{
label: $localize`Month to date` + ' (' + $localize`MTD` + ')',
value: 'mtd'
},
{
label: $localize`Year to date` + ' (' + $localize`YTD` + ')',
value: 'ytd'
},
{ label: '1 ' + $localize`year` + ' (' + $localize`1Y` + ')', value: '1y' },
{
label: '5 ' + $localize`years` + ' (' + $localize`5Y` + ')',
value: '5y'
},
{ label: $localize`Max`, value: 'max' }
];
public filterForm = this.formBuilder.group({ public filterForm = this.formBuilder.group({
account: new FormControl<string>(undefined), account: new FormControl<string>(undefined),
assetClass: new FormControl<string>(undefined), assetClass: new FormControl<string>(undefined),
@ -199,6 +184,44 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit {
} }
public ngOnChanges() { public ngOnChanges() {
this.dateRangeOptions = [
{ label: $localize`Today`, value: '1d' },
{
label: $localize`Week to date` + ' (' + $localize`WTD` + ')',
value: 'wtd'
},
{
label: $localize`Month to date` + ' (' + $localize`MTD` + ')',
value: 'mtd'
},
{
label: $localize`Year to date` + ' (' + $localize`YTD` + ')',
value: 'ytd'
},
{
label: '1 ' + $localize`year` + ' (' + $localize`1Y` + ')',
value: '1y'
},
{
label: '5 ' + $localize`years` + ' (' + $localize`5Y` + ')',
value: '5y'
},
{ label: $localize`Max`, value: 'max' }
];
if (this.user?.settings?.isExperimentalFeatures) {
this.dateRangeOptions = this.dateRangeOptions.concat(
eachYearOfInterval({
end: new Date(),
start: this.user?.dateOfFirstActivity ?? new Date()
})
.map((date) => {
return { label: format(date, 'yyyy'), value: format(date, 'yyyy') };
})
.slice(0, -1)
);
}
this.dateRangeFormControl.setValue(this.user?.settings?.dateRange ?? null); this.dateRangeFormControl.setValue(this.user?.settings?.dateRange ?? null);
this.filterForm.setValue( this.filterForm.setValue(

@ -1,4 +1,10 @@
import { UniqueAsset } from '@ghostfolio/common/interfaces'; import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { DateRange } from '@ghostfolio/common/types';
export interface IDateRangeOption {
label: string;
value: DateRange;
}
export interface ISearchResultItem extends UniqueAsset { export interface ISearchResultItem extends UniqueAsset {
assetSubClassString: string; assetSubClassString: string;

Loading…
Cancel
Save