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
This commit is contained in:
parent
5ffc39c32f
commit
f1eeee0525
@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## 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
|
||||
|
||||
- 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 { 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 { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
||||
import type {
|
||||
@ -8,7 +9,7 @@ import type {
|
||||
UniqueAsset
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { permissions } from '@ghostfolio/common/permissions';
|
||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import type { DateRange, RequestWithUser } from '@ghostfolio/common/types';
|
||||
|
||||
import {
|
||||
Body,
|
||||
@ -19,6 +20,7 @@ import {
|
||||
Inject,
|
||||
Param,
|
||||
Post,
|
||||
Query,
|
||||
UseGuards,
|
||||
UseInterceptors
|
||||
} from '@nestjs/common';
|
||||
@ -106,13 +108,18 @@ export class BenchmarkController {
|
||||
public async getBenchmarkMarketDataBySymbol(
|
||||
@Param('dataSource') dataSource: DataSource,
|
||||
@Param('startDateString') startDateString: string,
|
||||
@Param('symbol') symbol: string
|
||||
@Param('symbol') symbol: string,
|
||||
@Query('range') dateRange: DateRange = 'max'
|
||||
): Promise<BenchmarkMarketDataDetails> {
|
||||
const startDate = new Date(startDateString);
|
||||
const { endDate, startDate } = getInterval(
|
||||
dateRange,
|
||||
new Date(startDateString)
|
||||
);
|
||||
const userCurrency = this.request.user.Settings.settings.baseCurrency;
|
||||
|
||||
return this.benchmarkService.getMarketDataBySymbol({
|
||||
dataSource,
|
||||
endDate,
|
||||
startDate,
|
||||
symbol,
|
||||
userCurrency
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
||||
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 { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.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 { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import type { DateRange, RequestWithUser } from '@ghostfolio/common/types';
|
||||
|
||||
import {
|
||||
Body,
|
||||
@ -84,6 +85,7 @@ export class OrderController {
|
||||
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId,
|
||||
@Query('accounts') filterByAccounts?: string,
|
||||
@Query('assetClasses') filterByAssetClasses?: string,
|
||||
@Query('range') dateRange: DateRange = 'max',
|
||||
@Query('skip') skip?: number,
|
||||
@Query('sortColumn') sortColumn?: string,
|
||||
@Query('sortDirection') sortDirection?: Prisma.SortOrder,
|
||||
@ -96,14 +98,18 @@ export class OrderController {
|
||||
filterByTags
|
||||
});
|
||||
|
||||
const { endDate, startDate } = getInterval(dateRange);
|
||||
|
||||
const impersonationUserId =
|
||||
await this.impersonationService.validateImpersonationId(impersonationId);
|
||||
const userCurrency = this.request.user.Settings.settings.baseCurrency;
|
||||
|
||||
const { activities, count } = await this.orderService.getOrders({
|
||||
endDate,
|
||||
filters,
|
||||
sortColumn,
|
||||
sortDirection,
|
||||
startDate,
|
||||
userCurrency,
|
||||
includeDrafts: true,
|
||||
skip: isNaN(skip) ? undefined : skip,
|
||||
|
@ -198,22 +198,26 @@ export class OrderService {
|
||||
}
|
||||
|
||||
public async getOrders({
|
||||
endDate,
|
||||
filters,
|
||||
includeDrafts = false,
|
||||
skip,
|
||||
sortColumn,
|
||||
sortDirection,
|
||||
startDate,
|
||||
take = Number.MAX_SAFE_INTEGER,
|
||||
types,
|
||||
userCurrency,
|
||||
userId,
|
||||
withExcludedAccounts = false
|
||||
}: {
|
||||
endDate?: Date;
|
||||
filters?: Filter[];
|
||||
includeDrafts?: boolean;
|
||||
skip?: number;
|
||||
sortColumn?: string;
|
||||
sortDirection?: Prisma.SortOrder;
|
||||
startDate?: Date;
|
||||
take?: number;
|
||||
types?: ActivityType[];
|
||||
userCurrency: string;
|
||||
@ -225,6 +229,18 @@ export class OrderService {
|
||||
];
|
||||
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 {
|
||||
ACCOUNT: filtersByAccount,
|
||||
ASSET_CLASS: filtersByAssetClass,
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
hasNotDefinedValuesInObject,
|
||||
nullifyValuesInObject
|
||||
} 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 { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.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);
|
||||
const userCurrency = this.request.user.Settings.settings.baseCurrency;
|
||||
|
||||
const { endDate, startDate } = getInterval(dateRange);
|
||||
|
||||
const { activities } = await this.orderService.getOrders({
|
||||
endDate,
|
||||
filters,
|
||||
startDate,
|
||||
userCurrency,
|
||||
userId: impersonationUserId || this.request.user.id,
|
||||
types: ['DIVIDEND']
|
||||
@ -245,7 +250,6 @@ export class PortfolioController {
|
||||
|
||||
let dividends = await this.portfolioService.getDividends({
|
||||
activities,
|
||||
dateRange,
|
||||
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 { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.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 { 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';
|
||||
@ -73,16 +76,8 @@ import {
|
||||
isBefore,
|
||||
isSameMonth,
|
||||
isSameYear,
|
||||
isValid,
|
||||
max,
|
||||
min,
|
||||
parseISO,
|
||||
set,
|
||||
startOfWeek,
|
||||
startOfMonth,
|
||||
startOfYear,
|
||||
subDays,
|
||||
subYears
|
||||
set
|
||||
} from 'date-fns';
|
||||
import { isEmpty, last, uniq, uniqBy } from 'lodash';
|
||||
|
||||
@ -221,11 +216,9 @@ export class PortfolioService {
|
||||
|
||||
public async getDividends({
|
||||
activities,
|
||||
dateRange = 'max',
|
||||
groupBy
|
||||
}: {
|
||||
activities: Activity[];
|
||||
dateRange?: DateRange;
|
||||
groupBy?: GroupBy;
|
||||
}): Promise<InvestmentItem[]> {
|
||||
let dividends = activities.map(({ date, valueInBaseCurrency }) => {
|
||||
@ -239,14 +232,7 @@ export class PortfolioService {
|
||||
dividends = this.getDividendsByGroup({ dividends, groupBy });
|
||||
}
|
||||
|
||||
const startDate = this.getStartDate(
|
||||
dateRange,
|
||||
parseDate(dividends[0]?.date)
|
||||
);
|
||||
|
||||
return dividends.filter(({ date }) => {
|
||||
return !isBefore(parseDate(date), startDate);
|
||||
});
|
||||
return dividends;
|
||||
}
|
||||
|
||||
public async getInvestments({
|
||||
@ -375,7 +361,7 @@ export class PortfolioService {
|
||||
exchangeRateDataService: this.exchangeRateDataService
|
||||
});
|
||||
|
||||
const startDate = this.getStartDate(
|
||||
const { startDate } = getInterval(
|
||||
dateRange,
|
||||
portfolioCalculator.getStartDate()
|
||||
);
|
||||
@ -960,7 +946,10 @@ export class PortfolioService {
|
||||
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
||||
const user = await this.userService.user({ id: userId });
|
||||
|
||||
const { endDate, startDate } = getInterval(dateRange);
|
||||
|
||||
const { activities } = await this.orderService.getOrders({
|
||||
endDate,
|
||||
filters,
|
||||
userId,
|
||||
types: ['BUY', 'SELL'],
|
||||
@ -981,12 +970,10 @@ export class PortfolioService {
|
||||
exchangeRateDataService: this.exchangeRateDataService
|
||||
});
|
||||
|
||||
const startDate = this.getStartDate(
|
||||
dateRange,
|
||||
portfolioCalculator.getStartDate()
|
||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||
startDate,
|
||||
endDate
|
||||
);
|
||||
const currentPositions =
|
||||
await portfolioCalculator.getCurrentPositions(startDate);
|
||||
|
||||
let positions = currentPositions.positions.filter(({ quantity }) => {
|
||||
return !quantity.eq(0);
|
||||
@ -1133,7 +1120,10 @@ export class PortfolioService {
|
||||
)
|
||||
);
|
||||
|
||||
const { endDate, startDate } = getInterval(dateRange);
|
||||
|
||||
const { activities } = await this.orderService.getOrders({
|
||||
endDate,
|
||||
filters,
|
||||
userCurrency,
|
||||
userId,
|
||||
@ -1169,16 +1159,6 @@ export class PortfolioService {
|
||||
exchangeRateDataService: this.exchangeRateDataService
|
||||
});
|
||||
|
||||
const portfolioStart = min(
|
||||
[
|
||||
parseDate(accountBalanceItems[0]?.date),
|
||||
portfolioCalculator.getStartDate()
|
||||
].filter((date) => {
|
||||
return isValid(date);
|
||||
})
|
||||
);
|
||||
|
||||
const startDate = this.getStartDate(dateRange, portfolioStart);
|
||||
const {
|
||||
currentValueInBaseCurrency,
|
||||
errors,
|
||||
@ -1192,7 +1172,7 @@ export class PortfolioService {
|
||||
netPerformancePercentageWithCurrencyEffect,
|
||||
netPerformanceWithCurrencyEffect,
|
||||
totalInvestment
|
||||
} = await portfolioCalculator.getCurrentPositions(startDate);
|
||||
} = await portfolioCalculator.getCurrentPositions(startDate, endDate);
|
||||
|
||||
let currentNetPerformance = netPerformance;
|
||||
|
||||
@ -1448,11 +1428,11 @@ export class PortfolioService {
|
||||
|
||||
userId = await this.getUserId(impersonationId, userId);
|
||||
|
||||
const startDate = this.getStartDate(
|
||||
const { endDate, startDate } = getInterval(
|
||||
dateRange,
|
||||
portfolioCalculator.getStartDate()
|
||||
);
|
||||
const endDate = new Date();
|
||||
|
||||
const daysInMarket = differenceInDays(endDate, startDate) + 1;
|
||||
const step = withDataDecimation
|
||||
? 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({
|
||||
investments,
|
||||
savingsRate
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
IsOptional,
|
||||
IsString
|
||||
} from 'class-validator';
|
||||
import { eachYearOfInterval, format } from 'date-fns';
|
||||
|
||||
export class UpdateUserSettingDto {
|
||||
@IsNumber()
|
||||
@ -32,7 +33,20 @@ export class UpdateUserSettingDto {
|
||||
@IsOptional()
|
||||
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()
|
||||
dateRange?: DateRange;
|
||||
|
||||
|
@ -51,13 +51,22 @@ export class UserService {
|
||||
{ Account, id, permissions, Settings, subscription }: UserWithSettings,
|
||||
aLocale = locale
|
||||
): Promise<IUser> {
|
||||
const access = await this.prismaService.access.findMany({
|
||||
let [access, firstActivity, tags] = await Promise.all([
|
||||
this.prismaService.access.findMany({
|
||||
include: {
|
||||
User: true
|
||||
},
|
||||
orderBy: { alias: 'asc' },
|
||||
where: { GranteeUser: { id } }
|
||||
});
|
||||
}),
|
||||
this.prismaService.order.findFirst({
|
||||
orderBy: {
|
||||
date: 'asc'
|
||||
},
|
||||
where: { userId: id }
|
||||
}),
|
||||
this.tagService.getByUser(id)
|
||||
]);
|
||||
|
||||
let systemMessage: SystemMessage;
|
||||
|
||||
@ -69,8 +78,6 @@ export class UserService {
|
||||
systemMessage = systemMessageProperty;
|
||||
}
|
||||
|
||||
let tags = await this.tagService.getByUser(id);
|
||||
|
||||
if (
|
||||
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
|
||||
subscription.type === 'Basic'
|
||||
@ -91,6 +98,7 @@ export class UserService {
|
||||
};
|
||||
}),
|
||||
accounts: Account,
|
||||
dateOfFirstActivity: firstActivity?.date ?? new Date(),
|
||||
settings: {
|
||||
...(<UserSettings>Settings.settings),
|
||||
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 {
|
||||
endOfDay,
|
||||
max,
|
||||
subDays,
|
||||
startOfMonth,
|
||||
startOfWeek,
|
||||
startOfYear,
|
||||
subYears,
|
||||
endOfYear
|
||||
} from 'date-fns';
|
||||
|
||||
export function getFactor(activityType: ActivityType) {
|
||||
let factor: number;
|
||||
@ -19,3 +32,49 @@ export function getFactor(activityType: ActivityType) {
|
||||
|
||||
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';
|
||||
import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config';
|
||||
import {
|
||||
DATE_FORMAT,
|
||||
getBackgroundColor,
|
||||
getDateFormatString,
|
||||
getLocale,
|
||||
@ -39,16 +38,8 @@ import {
|
||||
} from 'chart.js';
|
||||
import 'chartjs-adapter-date-fns';
|
||||
import annotationPlugin from 'chartjs-plugin-annotation';
|
||||
import {
|
||||
addDays,
|
||||
format,
|
||||
isAfter,
|
||||
isValid,
|
||||
min,
|
||||
parseISO,
|
||||
subDays
|
||||
} from 'date-fns';
|
||||
import { first, last } from 'lodash';
|
||||
import { isAfter, isValid, min, subDays } from 'date-fns';
|
||||
import { first } from 'lodash';
|
||||
|
||||
@Component({
|
||||
selector: 'gf-investment-chart',
|
||||
@ -112,46 +103,6 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
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'> = {
|
||||
labels: this.historicalDataItems.map(({ date }) => {
|
||||
return parseDate(date);
|
||||
@ -303,7 +254,6 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
display: false
|
||||
},
|
||||
min: scaleXMin,
|
||||
suggestedMax: new Date().toISOString(),
|
||||
type: 'time',
|
||||
time: {
|
||||
tooltipFormat: getDateFormatString(this.locale),
|
||||
|
@ -124,6 +124,9 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
||||
this.dataService
|
||||
.fetchActivities({
|
||||
filters: this.userService.getFilters(),
|
||||
range: this.user?.settings?.isExperimentalFeatures
|
||||
? this.user?.settings?.dateRange
|
||||
: undefined,
|
||||
skip: this.pageIndex * this.pageSize,
|
||||
sortColumn: this.sortColumn,
|
||||
sortDirection: this.sortDirection,
|
||||
|
@ -352,6 +352,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
.fetchBenchmarkBySymbol({
|
||||
dataSource,
|
||||
symbol,
|
||||
range: this.user?.settings?.dateRange,
|
||||
startDate: this.firstOrderDate
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
|
@ -159,12 +159,14 @@ export class DataService {
|
||||
|
||||
public fetchActivities({
|
||||
filters,
|
||||
range,
|
||||
skip,
|
||||
sortColumn,
|
||||
sortDirection,
|
||||
take
|
||||
}: {
|
||||
filters?: Filter[];
|
||||
range?: DateRange;
|
||||
skip?: number;
|
||||
sortColumn?: string;
|
||||
sortDirection?: SortDirection;
|
||||
@ -172,6 +174,10 @@ export class DataService {
|
||||
}): Observable<Activities> {
|
||||
let params = this.buildFiltersAsQueryParams({ filters });
|
||||
|
||||
if (range) {
|
||||
params = params.append('range', range);
|
||||
}
|
||||
|
||||
if (skip) {
|
||||
params = params.append('skip', skip);
|
||||
}
|
||||
@ -269,16 +275,25 @@ export class DataService {
|
||||
|
||||
public fetchBenchmarkBySymbol({
|
||||
dataSource,
|
||||
range,
|
||||
startDate,
|
||||
symbol
|
||||
}: {
|
||||
range: DateRange;
|
||||
startDate: Date;
|
||||
} & UniqueAsset): Observable<BenchmarkMarketDataDetails> {
|
||||
let params = new HttpParams();
|
||||
|
||||
if (range) {
|
||||
params = params.append('range', range);
|
||||
}
|
||||
|
||||
return this.http.get<BenchmarkMarketDataDetails>(
|
||||
`/api/v1/benchmark/${dataSource}/${symbol}/${format(
|
||||
startDate,
|
||||
DATE_FORMAT
|
||||
)}`
|
||||
)}`,
|
||||
{ params }
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -82,6 +82,10 @@ export class UserService extends ObservableStore<UserStoreState> {
|
||||
private fetchUser(): Observable<User> {
|
||||
return this.http.get<any>('/api/v1/user').pipe(
|
||||
map((user) => {
|
||||
if (user.dateOfFirstActivity) {
|
||||
user.dateOfFirstActivity = parseISO(user.dateOfFirstActivity);
|
||||
}
|
||||
|
||||
if (user.settings?.retirementDate) {
|
||||
user.settings.retirementDate = parseISO(user.settings.retirementDate);
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ export interface User {
|
||||
id: string;
|
||||
}[];
|
||||
accounts: Account[];
|
||||
dateOfFirstActivity: Date;
|
||||
id: string;
|
||||
permissions: string[];
|
||||
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 { MatMenuTrigger } from '@angular/material/menu';
|
||||
import { Account, AssetClass } from '@prisma/client';
|
||||
import { eachYearOfInterval, format } from 'date-fns';
|
||||
import { EMPTY, Observable, Subject, lastValueFrom } from 'rxjs';
|
||||
import {
|
||||
catchError,
|
||||
@ -35,7 +36,11 @@ import {
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import { AssistantListItemComponent } from './assistant-list-item/assistant-list-item.component';
|
||||
import { ISearchResultItem, ISearchResults } from './interfaces/interfaces';
|
||||
import {
|
||||
IDateRangeOption,
|
||||
ISearchResultItem,
|
||||
ISearchResults
|
||||
} from './interfaces/interfaces';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
@ -95,27 +100,7 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit {
|
||||
public accounts: Account[] = [];
|
||||
public assetClasses: Filter[] = [];
|
||||
public dateRangeFormControl = new FormControl<string>(undefined);
|
||||
public readonly 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' }
|
||||
];
|
||||
public dateRangeOptions: IDateRangeOption[] = [];
|
||||
public filterForm = this.formBuilder.group({
|
||||
account: new FormControl<string>(undefined),
|
||||
assetClass: new FormControl<string>(undefined),
|
||||
@ -199,6 +184,44 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
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.filterForm.setValue(
|
||||
|
@ -1,4 +1,10 @@
|
||||
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||
import { DateRange } from '@ghostfolio/common/types';
|
||||
|
||||
export interface IDateRangeOption {
|
||||
label: string;
|
||||
value: DateRange;
|
||||
}
|
||||
|
||||
export interface ISearchResultItem extends UniqueAsset {
|
||||
assetSubClassString: string;
|
||||
|
Loading…
x
Reference in New Issue
Block a user