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
|
## 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…
x
Reference in New Issue
Block a user