Compare commits
33 Commits
Author | SHA1 | Date | |
---|---|---|---|
0f9455cf02 | |||
d4afa03505 | |||
c9237146e2 | |||
faad65b6f3 | |||
e459c72100 | |||
a8add30125 | |||
b535aee91d | |||
4434d0315f | |||
8b10695353 | |||
e82dcc8ace | |||
6dcb0d8583 | |||
40b6777814 | |||
25deba16df | |||
be93ca8968 | |||
0436cc6487 | |||
857708dc4d | |||
1ca4f885b0 | |||
c9368c5cf2 | |||
29423efea3 | |||
f3ee99fb2b | |||
3df8810412 | |||
b8ca88c6df | |||
2c068c412d | |||
9fdbd22cb5 | |||
8f5f4c5875 | |||
50fb82a6e6 | |||
2c10cd7edf | |||
bbde86c66e | |||
73c0843d51 | |||
04fc2cd3e1 | |||
b39c97ab9f | |||
1dd5e9c787 | |||
a9985b65b8 |
52
CHANGELOG.md
52
CHANGELOG.md
@ -5,7 +5,57 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## 2.43.0 - 2024-01-23
|
||||
## 2.47.0 - 2024-02-02
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the tag selector to only show used tags in the assistant (experimental)
|
||||
- Improved the language localization for German (`de`)
|
||||
- Upgraded `prettier` from version `3.2.1` to `3.2.4`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed a rendering issue caused by the date range selector in the assistant (experimental)
|
||||
- Fixed an issue with the currency conversion in the investment timeline
|
||||
- Fixed the export in the lazy-loaded activities table on the portfolio activities page (experimental)
|
||||
|
||||
## 2.46.0 - 2024-01-28
|
||||
|
||||
### Added
|
||||
|
||||
- Added a button to reset the active filters in the assistant (experimental)
|
||||
|
||||
### Changed
|
||||
|
||||
- Migrated the portfolio allocations to work with the filters of the assistant (experimental)
|
||||
- Migrated the portfolio holdings to work with the filters of the assistant (experimental)
|
||||
|
||||
## 2.45.0 - 2024-01-27
|
||||
|
||||
### Added
|
||||
|
||||
- Extended the assistant by an account selector (experimental)
|
||||
- Added support to grant private access with permissions (experimental)
|
||||
- Added `permissions` to the `Access` model
|
||||
|
||||
### Changed
|
||||
|
||||
- Migrated the tag selector to a form group in the assistant (experimental)
|
||||
- Formatted the name in the _EOD Historical Data_ service
|
||||
- Improved the language localization for German (`de`)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the import for activities with `MANUAL` data source and type `FEE`, `INTEREST`, `ITEM` or `LIABILITY`
|
||||
- Removed holdings with incomplete data from the _Top 3_ and _Bottom 3_ performers on the analysis page
|
||||
|
||||
## 2.44.0 - 2024-01-24
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improved the validation for non-numeric results in the _EOD Historical Data_ service
|
||||
|
||||
## 2.43.1 - 2024-01-23
|
||||
|
||||
### Added
|
||||
|
||||
|
@ -13,7 +13,6 @@ COPY ./.yarnrc .yarnrc
|
||||
COPY ./prisma/schema.prisma prisma/schema.prisma
|
||||
|
||||
RUN apt update && apt install -y \
|
||||
curl \
|
||||
g++ \
|
||||
git \
|
||||
make \
|
||||
@ -53,6 +52,7 @@ RUN yarn database:generate-typings
|
||||
# Image to run, copy everything needed from builder
|
||||
FROM node:18-slim
|
||||
RUN apt update && apt install -y \
|
||||
curl \
|
||||
openssl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
@ -42,23 +42,27 @@ export class AccessController {
|
||||
where: { userId: this.request.user.id }
|
||||
});
|
||||
|
||||
return accessesWithGranteeUser.map((access) => {
|
||||
if (access.GranteeUser) {
|
||||
return accessesWithGranteeUser.map(
|
||||
({ alias, GranteeUser, id, permissions }) => {
|
||||
if (GranteeUser) {
|
||||
return {
|
||||
alias,
|
||||
id,
|
||||
permissions,
|
||||
grantee: GranteeUser?.id,
|
||||
type: 'PRIVATE'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
alias: access.alias,
|
||||
grantee: access.GranteeUser?.id,
|
||||
id: access.id,
|
||||
type: 'RESTRICTED_VIEW'
|
||||
alias,
|
||||
id,
|
||||
permissions,
|
||||
grantee: 'Public',
|
||||
type: 'PUBLIC'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
alias: access.alias,
|
||||
grantee: 'Public',
|
||||
id: access.id,
|
||||
type: 'PUBLIC'
|
||||
};
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
@HasPermission(permissions.createAccess)
|
||||
@ -83,6 +87,7 @@ export class AccessController {
|
||||
GranteeUser: data.granteeUserId
|
||||
? { connect: { id: data.granteeUserId } }
|
||||
: undefined,
|
||||
permissions: data.permissions,
|
||||
User: { connect: { id: this.request.user.id } }
|
||||
});
|
||||
} catch {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
import { AccessPermission } from '@prisma/client';
|
||||
import { IsEnum, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
|
||||
export class CreateAccessDto {
|
||||
@IsOptional()
|
||||
@ -9,7 +10,7 @@ export class CreateAccessDto {
|
||||
@IsUUID()
|
||||
granteeUserId?: string;
|
||||
|
||||
@IsEnum(AccessPermission, { each: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
type?: 'PUBLIC';
|
||||
permissions?: AccessPermission[];
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
||||
import { ApiService } from '@ghostfolio/api/services/api/api.service';
|
||||
import { Export } from '@ghostfolio/common/interfaces';
|
||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import { Controller, Get, Inject, Query, UseGuards } from '@nestjs/common';
|
||||
@ -10,6 +11,7 @@ import { ExportService } from './export.service';
|
||||
@Controller('export')
|
||||
export class ExportController {
|
||||
public constructor(
|
||||
private readonly apiService: ApiService,
|
||||
private readonly exportService: ExportService,
|
||||
@Inject(REQUEST) private readonly request: RequestWithUser
|
||||
) {}
|
||||
@ -17,10 +19,20 @@ export class ExportController {
|
||||
@Get()
|
||||
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
||||
public async export(
|
||||
@Query('activityIds') activityIds?: string[]
|
||||
@Query('accounts') filterByAccounts?: string,
|
||||
@Query('activityIds') activityIds?: string[],
|
||||
@Query('assetClasses') filterByAssetClasses?: string,
|
||||
@Query('tags') filterByTags?: string
|
||||
): Promise<Export> {
|
||||
const filters = this.apiService.buildFiltersFromQueryParams({
|
||||
filterByAccounts,
|
||||
filterByAssetClasses,
|
||||
filterByTags
|
||||
});
|
||||
|
||||
return this.exportService.export({
|
||||
activityIds,
|
||||
filters,
|
||||
userCurrency: this.request.user.Settings.settings.baseCurrency,
|
||||
userId: this.request.user.id
|
||||
});
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { AccountModule } from '@ghostfolio/api/app/account/account.module';
|
||||
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
|
||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
|
||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||
@ -12,6 +13,7 @@ import { ExportService } from './export.service';
|
||||
@Module({
|
||||
imports: [
|
||||
AccountModule,
|
||||
ApiModule,
|
||||
ConfigurationModule,
|
||||
DataGatheringModule,
|
||||
DataProviderModule,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
||||
import { environment } from '@ghostfolio/api/environments/environment';
|
||||
import { Export } from '@ghostfolio/common/interfaces';
|
||||
import { Filter, Export } from '@ghostfolio/common/interfaces';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
@ -13,10 +13,12 @@ export class ExportService {
|
||||
|
||||
public async export({
|
||||
activityIds,
|
||||
filters,
|
||||
userCurrency,
|
||||
userId
|
||||
}: {
|
||||
activityIds?: string[];
|
||||
filters?: Filter[];
|
||||
userCurrency: string;
|
||||
userId: string;
|
||||
}): Promise<Export> {
|
||||
@ -42,6 +44,7 @@ export class ExportService {
|
||||
);
|
||||
|
||||
let { activities } = await this.orderService.getOrders({
|
||||
filters,
|
||||
userCurrency,
|
||||
userId,
|
||||
includeDrafts: true,
|
||||
|
@ -575,7 +575,7 @@ export class ImportService {
|
||||
|
||||
for (const [
|
||||
index,
|
||||
{ currency, dataSource, symbol }
|
||||
{ currency, dataSource, symbol, type }
|
||||
] of uniqueActivitiesDto.entries()) {
|
||||
if (!this.configurationService.get('DATA_SOURCES').includes(dataSource)) {
|
||||
throw new Error(
|
||||
@ -583,28 +583,33 @@ export class ImportService {
|
||||
);
|
||||
}
|
||||
|
||||
const assetProfile = (
|
||||
await this.dataProviderService.getAssetProfiles([
|
||||
{ dataSource, symbol }
|
||||
])
|
||||
)?.[symbol];
|
||||
const assetProfile = {
|
||||
currency,
|
||||
...(
|
||||
await this.dataProviderService.getAssetProfiles([
|
||||
{ dataSource, symbol }
|
||||
])
|
||||
)?.[symbol]
|
||||
};
|
||||
|
||||
if (!assetProfile?.name) {
|
||||
throw new Error(
|
||||
`activities.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")`
|
||||
);
|
||||
}
|
||||
if (type === 'BUY' || type === 'DIVIDEND' || type === 'SELL') {
|
||||
if (!assetProfile?.name) {
|
||||
throw new Error(
|
||||
`activities.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")`
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
assetProfile.currency !== currency &&
|
||||
!this.exchangeRateDataService.hasCurrencyPair(
|
||||
currency,
|
||||
assetProfile.currency
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
`activities.${index}.currency ("${currency}") does not match with "${assetProfile.currency}" and no exchange rate is available from "${currency}" to "${assetProfile.currency}"`
|
||||
);
|
||||
if (
|
||||
assetProfile.currency !== currency &&
|
||||
!this.exchangeRateDataService.hasCurrencyPair(
|
||||
currency,
|
||||
assetProfile.currency
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
`activities.${index}.currency ("${currency}") does not match with "${assetProfile.currency}" and no exchange rate is available from "${currency}" to "${assetProfile.currency}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
assetProfiles[getAssetProfileIdentifier({ dataSource, symbol })] =
|
||||
|
@ -1,8 +0,0 @@
|
||||
import { TimelinePeriod } from '@ghostfolio/api/app/portfolio/interfaces/timeline-period.interface';
|
||||
import Big from 'big.js';
|
||||
|
||||
export interface TimelineInfoInterface {
|
||||
maxNetPerformance: Big;
|
||||
minNetPerformance: Big;
|
||||
timelinePeriods: TimelinePeriod[];
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
import Big from 'big.js';
|
||||
|
||||
export interface TimelinePeriod {
|
||||
date: string;
|
||||
grossPerformance: Big;
|
||||
investment: Big;
|
||||
netPerformance: Big;
|
||||
value: Big;
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
export type Accuracy = 'day' | 'month' | 'year';
|
||||
|
||||
export interface TimelineSpecification {
|
||||
accuracy: Accuracy;
|
||||
start: string;
|
||||
}
|
@ -68,14 +68,20 @@ describe('PortfolioCalculator', () => {
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
||||
|
||||
const chartData = await portfolioCalculator.getChartData({
|
||||
start: parseDate('2021-11-22')
|
||||
});
|
||||
|
||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||
parseDate('2021-11-22')
|
||||
);
|
||||
|
||||
const investments = portfolioCalculator.getInvestments();
|
||||
|
||||
const investmentsByMonth =
|
||||
portfolioCalculator.getInvestmentsByGroup('month');
|
||||
const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
|
||||
data: chartData,
|
||||
groupBy: 'month'
|
||||
});
|
||||
|
||||
spy.mockRestore();
|
||||
|
||||
@ -135,7 +141,8 @@ describe('PortfolioCalculator', () => {
|
||||
]);
|
||||
|
||||
expect(investmentsByMonth).toEqual([
|
||||
{ date: '2021-11-01', investment: new Big('12.6') }
|
||||
{ date: '2021-11-01', investment: 0 },
|
||||
{ date: '2021-12-01', investment: 0 }
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
@ -57,14 +57,20 @@ describe('PortfolioCalculator', () => {
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
||||
|
||||
const chartData = await portfolioCalculator.getChartData({
|
||||
start: parseDate('2021-11-30')
|
||||
});
|
||||
|
||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||
parseDate('2021-11-30')
|
||||
);
|
||||
|
||||
const investments = portfolioCalculator.getInvestments();
|
||||
|
||||
const investmentsByMonth =
|
||||
portfolioCalculator.getInvestmentsByGroup('month');
|
||||
const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
|
||||
data: chartData,
|
||||
groupBy: 'month'
|
||||
});
|
||||
|
||||
spy.mockRestore();
|
||||
|
||||
@ -123,7 +129,8 @@ describe('PortfolioCalculator', () => {
|
||||
]);
|
||||
|
||||
expect(investmentsByMonth).toEqual([
|
||||
{ date: '2021-11-01', investment: new Big('273.2') }
|
||||
{ date: '2021-11-01', investment: 273.2 },
|
||||
{ date: '2021-12-01', investment: 0 }
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
@ -81,14 +81,20 @@ describe('PortfolioCalculator', () => {
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => parseDate('2018-01-01').getTime());
|
||||
|
||||
const chartData = await portfolioCalculator.getChartData({
|
||||
start: parseDate('2015-01-01')
|
||||
});
|
||||
|
||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||
parseDate('2015-01-01')
|
||||
);
|
||||
|
||||
const investments = portfolioCalculator.getInvestments();
|
||||
|
||||
const investmentsByMonth =
|
||||
portfolioCalculator.getInvestmentsByGroup('month');
|
||||
const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
|
||||
data: chartData,
|
||||
groupBy: 'month'
|
||||
});
|
||||
|
||||
spy.mockRestore();
|
||||
|
||||
@ -155,42 +161,43 @@ describe('PortfolioCalculator', () => {
|
||||
]);
|
||||
|
||||
expect(investmentsByMonth).toEqual([
|
||||
{ date: '2015-01-01', investment: new Big('640.86') },
|
||||
{ date: '2015-02-01', investment: new Big('0') },
|
||||
{ date: '2015-03-01', investment: new Big('0') },
|
||||
{ date: '2015-04-01', investment: new Big('0') },
|
||||
{ date: '2015-05-01', investment: new Big('0') },
|
||||
{ date: '2015-06-01', investment: new Big('0') },
|
||||
{ date: '2015-07-01', investment: new Big('0') },
|
||||
{ date: '2015-08-01', investment: new Big('0') },
|
||||
{ date: '2015-09-01', investment: new Big('0') },
|
||||
{ date: '2015-10-01', investment: new Big('0') },
|
||||
{ date: '2015-11-01', investment: new Big('0') },
|
||||
{ date: '2015-12-01', investment: new Big('0') },
|
||||
{ date: '2016-01-01', investment: new Big('0') },
|
||||
{ date: '2016-02-01', investment: new Big('0') },
|
||||
{ date: '2016-03-01', investment: new Big('0') },
|
||||
{ date: '2016-04-01', investment: new Big('0') },
|
||||
{ date: '2016-05-01', investment: new Big('0') },
|
||||
{ date: '2016-06-01', investment: new Big('0') },
|
||||
{ date: '2016-07-01', investment: new Big('0') },
|
||||
{ date: '2016-08-01', investment: new Big('0') },
|
||||
{ date: '2016-09-01', investment: new Big('0') },
|
||||
{ date: '2016-10-01', investment: new Big('0') },
|
||||
{ date: '2016-11-01', investment: new Big('0') },
|
||||
{ date: '2016-12-01', investment: new Big('0') },
|
||||
{ date: '2017-01-01', investment: new Big('0') },
|
||||
{ date: '2017-02-01', investment: new Big('0') },
|
||||
{ date: '2017-03-01', investment: new Big('0') },
|
||||
{ date: '2017-04-01', investment: new Big('0') },
|
||||
{ date: '2017-05-01', investment: new Big('0') },
|
||||
{ date: '2017-06-01', investment: new Big('0') },
|
||||
{ date: '2017-07-01', investment: new Big('0') },
|
||||
{ date: '2017-08-01', investment: new Big('0') },
|
||||
{ date: '2017-09-01', investment: new Big('0') },
|
||||
{ date: '2017-10-01', investment: new Big('0') },
|
||||
{ date: '2017-11-01', investment: new Big('0') },
|
||||
{ date: '2017-12-01', investment: new Big('-14156.4') }
|
||||
{ date: '2015-01-01', investment: 637.0853345999999 },
|
||||
{ date: '2015-02-01', investment: 0 },
|
||||
{ date: '2015-03-01', investment: 0 },
|
||||
{ date: '2015-04-01', investment: 0 },
|
||||
{ date: '2015-05-01', investment: 0 },
|
||||
{ date: '2015-06-01', investment: 0 },
|
||||
{ date: '2015-07-01', investment: 0 },
|
||||
{ date: '2015-08-01', investment: 0 },
|
||||
{ date: '2015-09-01', investment: 0 },
|
||||
{ date: '2015-10-01', investment: 0 },
|
||||
{ date: '2015-11-01', investment: 0 },
|
||||
{ date: '2015-12-01', investment: 0 },
|
||||
{ date: '2016-01-01', investment: 0 },
|
||||
{ date: '2016-02-01', investment: 0 },
|
||||
{ date: '2016-03-01', investment: 0 },
|
||||
{ date: '2016-04-01', investment: 0 },
|
||||
{ date: '2016-05-01', investment: 0 },
|
||||
{ date: '2016-06-01', investment: 0 },
|
||||
{ date: '2016-07-01', investment: 0 },
|
||||
{ date: '2016-08-01', investment: 0 },
|
||||
{ date: '2016-09-01', investment: 0 },
|
||||
{ date: '2016-10-01', investment: 0 },
|
||||
{ date: '2016-11-01', investment: 0 },
|
||||
{ date: '2016-12-01', investment: 0 },
|
||||
{ date: '2017-01-01', investment: 0 },
|
||||
{ date: '2017-02-01', investment: 0 },
|
||||
{ date: '2017-03-01', investment: 0 },
|
||||
{ date: '2017-04-01', investment: 0 },
|
||||
{ date: '2017-05-01', investment: 0 },
|
||||
{ date: '2017-06-01', investment: 0 },
|
||||
{ date: '2017-07-01', investment: 0 },
|
||||
{ date: '2017-08-01', investment: 0 },
|
||||
{ date: '2017-09-01', investment: 0 },
|
||||
{ date: '2017-10-01', investment: 0 },
|
||||
{ date: '2017-11-01', investment: 0 },
|
||||
{ date: '2017-12-01', investment: -318.54266729999995 },
|
||||
{ date: '2018-01-01', investment: 0 }
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
@ -70,14 +70,20 @@ describe('PortfolioCalculator', () => {
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => parseDate('2023-07-10').getTime());
|
||||
|
||||
const chartData = await portfolioCalculator.getChartData({
|
||||
start: parseDate('2023-01-03')
|
||||
});
|
||||
|
||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||
parseDate('2023-01-03')
|
||||
);
|
||||
|
||||
const investments = portfolioCalculator.getInvestments();
|
||||
|
||||
const investmentsByMonth =
|
||||
portfolioCalculator.getInvestmentsByGroup('month');
|
||||
const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
|
||||
data: chartData,
|
||||
groupBy: 'month'
|
||||
});
|
||||
|
||||
spy.mockRestore();
|
||||
|
||||
@ -137,7 +143,31 @@ describe('PortfolioCalculator', () => {
|
||||
]);
|
||||
|
||||
expect(investmentsByMonth).toEqual([
|
||||
{ date: '2023-01-01', investment: new Big('89.12') }
|
||||
{ date: '2023-01-01', investment: 82.329056 },
|
||||
{
|
||||
date: '2023-02-01',
|
||||
investment: 0
|
||||
},
|
||||
{
|
||||
date: '2023-03-01',
|
||||
investment: 0
|
||||
},
|
||||
{
|
||||
date: '2023-04-01',
|
||||
investment: 0
|
||||
},
|
||||
{
|
||||
date: '2023-05-01',
|
||||
investment: 0
|
||||
},
|
||||
{
|
||||
date: '2023-06-01',
|
||||
investment: 0
|
||||
},
|
||||
{
|
||||
date: '2023-07-01',
|
||||
investment: 0
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
@ -45,14 +45,20 @@ describe('PortfolioCalculator', () => {
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
||||
|
||||
const chartData = await portfolioCalculator.getChartData({
|
||||
start: new Date()
|
||||
});
|
||||
|
||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||
new Date()
|
||||
);
|
||||
|
||||
const investments = portfolioCalculator.getInvestments();
|
||||
|
||||
const investmentsByMonth =
|
||||
portfolioCalculator.getInvestmentsByGroup('month');
|
||||
const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
|
||||
data: chartData,
|
||||
groupBy: 'month'
|
||||
});
|
||||
|
||||
spy.mockRestore();
|
||||
|
||||
|
@ -68,14 +68,20 @@ describe('PortfolioCalculator', () => {
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => parseDate('2022-04-11').getTime());
|
||||
|
||||
const chartData = await portfolioCalculator.getChartData({
|
||||
start: parseDate('2022-03-07')
|
||||
});
|
||||
|
||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||
parseDate('2022-03-07')
|
||||
);
|
||||
|
||||
const investments = portfolioCalculator.getInvestments();
|
||||
|
||||
const investmentsByMonth =
|
||||
portfolioCalculator.getInvestmentsByGroup('month');
|
||||
const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
|
||||
data: chartData,
|
||||
groupBy: 'month'
|
||||
});
|
||||
|
||||
spy.mockRestore();
|
||||
|
||||
@ -137,8 +143,8 @@ describe('PortfolioCalculator', () => {
|
||||
]);
|
||||
|
||||
expect(investmentsByMonth).toEqual([
|
||||
{ date: '2022-03-01', investment: new Big('151.6') },
|
||||
{ date: '2022-04-01', investment: new Big('-85.73') }
|
||||
{ date: '2022-03-01', investment: 151.6 },
|
||||
{ date: '2022-04-01', investment: -75.8 }
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
@ -68,9 +68,9 @@ describe('PortfolioCalculator', () => {
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => parseDate('2022-04-11').getTime());
|
||||
|
||||
const chartData = await portfolioCalculator.getChartData(
|
||||
parseDate('2022-03-07')
|
||||
);
|
||||
const chartData = await portfolioCalculator.getChartData({
|
||||
start: parseDate('2022-03-07')
|
||||
});
|
||||
|
||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||
parseDate('2022-03-07')
|
||||
@ -78,13 +78,16 @@ describe('PortfolioCalculator', () => {
|
||||
|
||||
const investments = portfolioCalculator.getInvestments();
|
||||
|
||||
const investmentsByMonth =
|
||||
portfolioCalculator.getInvestmentsByGroup('month');
|
||||
const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
|
||||
data: chartData,
|
||||
groupBy: 'month'
|
||||
});
|
||||
|
||||
spy.mockRestore();
|
||||
|
||||
expect(chartData[0]).toEqual({
|
||||
date: '2022-03-07',
|
||||
investmentValueWithCurrencyEffect: 151.6,
|
||||
netPerformance: 0,
|
||||
netPerformanceInPercentage: 0,
|
||||
netPerformanceInPercentageWithCurrencyEffect: 0,
|
||||
@ -97,6 +100,7 @@ describe('PortfolioCalculator', () => {
|
||||
|
||||
expect(chartData[chartData.length - 1]).toEqual({
|
||||
date: '2022-04-11',
|
||||
investmentValueWithCurrencyEffect: 0,
|
||||
netPerformance: 19.86,
|
||||
netPerformanceInPercentage: 13.100263852242744,
|
||||
netPerformanceInPercentageWithCurrencyEffect: 13.100263852242744,
|
||||
@ -163,8 +167,8 @@ describe('PortfolioCalculator', () => {
|
||||
]);
|
||||
|
||||
expect(investmentsByMonth).toEqual([
|
||||
{ date: '2022-03-01', investment: new Big('151.6') },
|
||||
{ date: '2022-04-01', investment: new Big('-171.46') }
|
||||
{ date: '2022-03-01', investment: 151.6 },
|
||||
{ date: '2022-04-01', investment: -151.6 }
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { TimelineInfoInterface } from '@ghostfolio/api/app/portfolio/interfaces/timeline-info.interface';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
DataProviderInfo,
|
||||
HistoricalDataItem,
|
||||
InvestmentItem,
|
||||
ResponseError,
|
||||
SymbolMetrics,
|
||||
TimelinePosition
|
||||
@ -15,41 +16,19 @@ import Big from 'big.js';
|
||||
import {
|
||||
addDays,
|
||||
addMilliseconds,
|
||||
addMonths,
|
||||
addYears,
|
||||
differenceInDays,
|
||||
endOfDay,
|
||||
format,
|
||||
isAfter,
|
||||
isBefore,
|
||||
isSameDay,
|
||||
isSameMonth,
|
||||
isSameYear,
|
||||
max,
|
||||
min,
|
||||
set,
|
||||
subDays
|
||||
} from 'date-fns';
|
||||
import {
|
||||
cloneDeep,
|
||||
first,
|
||||
flatten,
|
||||
isNumber,
|
||||
last,
|
||||
sortBy,
|
||||
uniq
|
||||
} from 'lodash';
|
||||
import { cloneDeep, first, isNumber, last, sortBy, uniq } from 'lodash';
|
||||
|
||||
import { CurrentRateService } from './current-rate.service';
|
||||
import { CurrentPositions } from './interfaces/current-positions.interface';
|
||||
import { GetValueObject } from './interfaces/get-value-object.interface';
|
||||
import { PortfolioOrderItem } from './interfaces/portfolio-calculator.interface';
|
||||
import { PortfolioOrder } from './interfaces/portfolio-order.interface';
|
||||
import { TimelinePeriod } from './interfaces/timeline-period.interface';
|
||||
import {
|
||||
Accuracy,
|
||||
TimelineSpecification
|
||||
} from './interfaces/timeline-specification.interface';
|
||||
import { TransactionPointSymbol } from './interfaces/transaction-point-symbol.interface';
|
||||
import { TransactionPoint } from './interfaces/transaction-point.interface';
|
||||
|
||||
@ -193,7 +172,15 @@ export class PortfolioCalculator {
|
||||
this.transactionPoints = transactionPoints;
|
||||
}
|
||||
|
||||
public async getChartData(start: Date, end = new Date(Date.now()), step = 1) {
|
||||
public async getChartData({
|
||||
end = new Date(Date.now()),
|
||||
start,
|
||||
step = 1
|
||||
}: {
|
||||
end?: Date;
|
||||
start: Date;
|
||||
step?: number;
|
||||
}): Promise<HistoricalDataItem[]> {
|
||||
const symbols: { [symbol: string]: boolean } = {};
|
||||
|
||||
const transactionPointsBeforeEndDate =
|
||||
@ -217,13 +204,15 @@ export class PortfolioCalculator {
|
||||
dates.push(resetHours(end));
|
||||
}
|
||||
|
||||
for (const item of transactionPointsBeforeEndDate[firstIndex - 1].items) {
|
||||
dataGatheringItems.push({
|
||||
dataSource: item.dataSource,
|
||||
symbol: item.symbol
|
||||
});
|
||||
currencies[item.symbol] = item.currency;
|
||||
symbols[item.symbol] = true;
|
||||
if (transactionPointsBeforeEndDate.length > 0) {
|
||||
for (const item of transactionPointsBeforeEndDate[firstIndex - 1].items) {
|
||||
dataGatheringItems.push({
|
||||
dataSource: item.dataSource,
|
||||
symbol: item.symbol
|
||||
});
|
||||
currencies[item.symbol] = item.currency;
|
||||
symbols[item.symbol] = true;
|
||||
}
|
||||
}
|
||||
|
||||
const { dataProviderInfos, values: marketSymbols } =
|
||||
@ -262,6 +251,7 @@ export class PortfolioCalculator {
|
||||
|
||||
const accumulatedValuesByDate: {
|
||||
[date: string]: {
|
||||
investmentValueWithCurrencyEffect: Big;
|
||||
totalCurrentValue: Big;
|
||||
totalCurrentValueWithCurrencyEffect: Big;
|
||||
totalInvestmentValue: Big;
|
||||
@ -277,7 +267,8 @@ export class PortfolioCalculator {
|
||||
[symbol: string]: {
|
||||
currentValues: { [date: string]: Big };
|
||||
currentValuesWithCurrencyEffect: { [date: string]: Big };
|
||||
investmentValues: { [date: string]: Big };
|
||||
investmentValuesAccumulated: { [date: string]: Big };
|
||||
investmentValuesAccumulatedWithCurrencyEffect: { [date: string]: Big };
|
||||
investmentValuesWithCurrencyEffect: { [date: string]: Big };
|
||||
netPerformanceValues: { [date: string]: Big };
|
||||
netPerformanceValuesWithCurrencyEffect: { [date: string]: Big };
|
||||
@ -290,7 +281,8 @@ export class PortfolioCalculator {
|
||||
const {
|
||||
currentValues,
|
||||
currentValuesWithCurrencyEffect,
|
||||
investmentValues,
|
||||
investmentValuesAccumulated,
|
||||
investmentValuesAccumulatedWithCurrencyEffect,
|
||||
investmentValuesWithCurrencyEffect,
|
||||
netPerformanceValues,
|
||||
netPerformanceValuesWithCurrencyEffect,
|
||||
@ -310,7 +302,8 @@ export class PortfolioCalculator {
|
||||
valuesBySymbol[symbol] = {
|
||||
currentValues,
|
||||
currentValuesWithCurrencyEffect,
|
||||
investmentValues,
|
||||
investmentValuesAccumulated,
|
||||
investmentValuesAccumulatedWithCurrencyEffect,
|
||||
investmentValuesWithCurrencyEffect,
|
||||
netPerformanceValues,
|
||||
netPerformanceValuesWithCurrencyEffect,
|
||||
@ -332,8 +325,13 @@ export class PortfolioCalculator {
|
||||
symbolValues.currentValuesWithCurrencyEffect?.[dateString] ??
|
||||
new Big(0);
|
||||
|
||||
const investmentValue =
|
||||
symbolValues.investmentValues?.[dateString] ?? new Big(0);
|
||||
const investmentValueAccumulated =
|
||||
symbolValues.investmentValuesAccumulated?.[dateString] ?? new Big(0);
|
||||
|
||||
const investmentValueAccumulatedWithCurrencyEffect =
|
||||
symbolValues.investmentValuesAccumulatedWithCurrencyEffect?.[
|
||||
dateString
|
||||
] ?? new Big(0);
|
||||
|
||||
const investmentValueWithCurrencyEffect =
|
||||
symbolValues.investmentValuesWithCurrencyEffect?.[dateString] ??
|
||||
@ -355,6 +353,10 @@ export class PortfolioCalculator {
|
||||
] ?? new Big(0);
|
||||
|
||||
accumulatedValuesByDate[dateString] = {
|
||||
investmentValueWithCurrencyEffect: (
|
||||
accumulatedValuesByDate[dateString]
|
||||
?.investmentValueWithCurrencyEffect ?? new Big(0)
|
||||
).add(investmentValueWithCurrencyEffect),
|
||||
totalCurrentValue: (
|
||||
accumulatedValuesByDate[dateString]?.totalCurrentValue ?? new Big(0)
|
||||
).add(currentValue),
|
||||
@ -365,11 +367,11 @@ export class PortfolioCalculator {
|
||||
totalInvestmentValue: (
|
||||
accumulatedValuesByDate[dateString]?.totalInvestmentValue ??
|
||||
new Big(0)
|
||||
).add(investmentValue),
|
||||
).add(investmentValueAccumulated),
|
||||
totalInvestmentValueWithCurrencyEffect: (
|
||||
accumulatedValuesByDate[dateString]
|
||||
?.totalInvestmentValueWithCurrencyEffect ?? new Big(0)
|
||||
).add(investmentValueWithCurrencyEffect),
|
||||
).add(investmentValueAccumulatedWithCurrencyEffect),
|
||||
totalNetPerformanceValue: (
|
||||
accumulatedValuesByDate[dateString]?.totalNetPerformanceValue ??
|
||||
new Big(0)
|
||||
@ -392,6 +394,7 @@ export class PortfolioCalculator {
|
||||
|
||||
return Object.entries(accumulatedValuesByDate).map(([date, values]) => {
|
||||
const {
|
||||
investmentValueWithCurrencyEffect,
|
||||
totalCurrentValue,
|
||||
totalCurrentValueWithCurrencyEffect,
|
||||
totalInvestmentValue,
|
||||
@ -421,6 +424,8 @@ export class PortfolioCalculator {
|
||||
date,
|
||||
netPerformanceInPercentage,
|
||||
netPerformanceInPercentageWithCurrencyEffect,
|
||||
investmentValueWithCurrencyEffect:
|
||||
investmentValueWithCurrencyEffect.toNumber(),
|
||||
netPerformance: totalNetPerformanceValue.toNumber(),
|
||||
netPerformanceWithCurrencyEffect:
|
||||
totalNetPerformanceValueWithCurrencyEffect.toNumber(),
|
||||
@ -685,196 +690,27 @@ export class PortfolioCalculator {
|
||||
});
|
||||
}
|
||||
|
||||
public getInvestmentsByGroup(
|
||||
groupBy: GroupBy
|
||||
): { date: string; investment: Big }[] {
|
||||
if (this.orders.length === 0) {
|
||||
return [];
|
||||
}
|
||||
public getInvestmentsByGroup({
|
||||
data,
|
||||
groupBy
|
||||
}: {
|
||||
data: HistoricalDataItem[];
|
||||
groupBy: GroupBy;
|
||||
}): InvestmentItem[] {
|
||||
const groupedData: { [dateGroup: string]: Big } = {};
|
||||
|
||||
const investments: { date: string; investment: Big }[] = [];
|
||||
let currentDate: Date;
|
||||
let investmentByGroup = new Big(0);
|
||||
|
||||
for (const [index, order] of this.orders.entries()) {
|
||||
if (
|
||||
isSameYear(parseDate(order.date), currentDate) &&
|
||||
(groupBy === 'year' || isSameMonth(parseDate(order.date), currentDate))
|
||||
) {
|
||||
// Same group: Add up investments
|
||||
investmentByGroup = investmentByGroup.plus(
|
||||
order.quantity.mul(order.unitPrice).mul(this.getFactor(order.type))
|
||||
);
|
||||
} else {
|
||||
// New group: Store previous group and reset
|
||||
if (currentDate) {
|
||||
investments.push({
|
||||
date: format(
|
||||
set(currentDate, {
|
||||
date: 1,
|
||||
month: groupBy === 'year' ? 0 : currentDate.getMonth()
|
||||
}),
|
||||
DATE_FORMAT
|
||||
),
|
||||
investment: investmentByGroup
|
||||
});
|
||||
}
|
||||
|
||||
currentDate = parseDate(order.date);
|
||||
investmentByGroup = order.quantity
|
||||
.mul(order.unitPrice)
|
||||
.mul(this.getFactor(order.type));
|
||||
}
|
||||
|
||||
if (index === this.orders.length - 1) {
|
||||
// Store current group (latest order)
|
||||
investments.push({
|
||||
date: format(
|
||||
set(currentDate, {
|
||||
date: 1,
|
||||
month: groupBy === 'year' ? 0 : currentDate.getMonth()
|
||||
}),
|
||||
DATE_FORMAT
|
||||
),
|
||||
investment: investmentByGroup
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fill in the missing dates with investment = 0
|
||||
const startDate = parseDate(first(this.orders).date);
|
||||
const endDate = parseDate(last(this.orders).date);
|
||||
|
||||
const allDates: string[] = [];
|
||||
currentDate = startDate;
|
||||
|
||||
while (currentDate <= endDate) {
|
||||
allDates.push(
|
||||
format(
|
||||
set(currentDate, {
|
||||
date: 1,
|
||||
month: groupBy === 'year' ? 0 : currentDate.getMonth()
|
||||
}),
|
||||
DATE_FORMAT
|
||||
)
|
||||
for (const { date, investmentValueWithCurrencyEffect } of data) {
|
||||
const dateGroup =
|
||||
groupBy === 'month' ? date.substring(0, 7) : date.substring(0, 4);
|
||||
groupedData[dateGroup] = (groupedData[dateGroup] ?? new Big(0)).plus(
|
||||
investmentValueWithCurrencyEffect
|
||||
);
|
||||
currentDate.setMonth(currentDate.getMonth() + 1);
|
||||
}
|
||||
|
||||
for (const date of allDates) {
|
||||
const existingInvestment = investments.find((investment) => {
|
||||
return investment.date === date;
|
||||
});
|
||||
|
||||
if (!existingInvestment) {
|
||||
investments.push({ date, investment: new Big(0) });
|
||||
}
|
||||
}
|
||||
|
||||
return sortBy(investments, ({ date }) => {
|
||||
return date;
|
||||
});
|
||||
}
|
||||
|
||||
public async calculateTimeline(
|
||||
timelineSpecification: TimelineSpecification[],
|
||||
endDate: string
|
||||
): Promise<TimelineInfoInterface> {
|
||||
if (timelineSpecification.length === 0) {
|
||||
return {
|
||||
maxNetPerformance: new Big(0),
|
||||
minNetPerformance: new Big(0),
|
||||
timelinePeriods: []
|
||||
};
|
||||
}
|
||||
|
||||
const startDate = timelineSpecification[0].start;
|
||||
const start = parseDate(startDate);
|
||||
const end = parseDate(endDate);
|
||||
|
||||
const timelinePeriodPromises: Promise<TimelineInfoInterface>[] = [];
|
||||
let i = 0;
|
||||
let j = -1;
|
||||
for (
|
||||
let currentDate = start;
|
||||
!isAfter(currentDate, end);
|
||||
currentDate = this.addToDate(
|
||||
currentDate,
|
||||
timelineSpecification[i].accuracy
|
||||
)
|
||||
) {
|
||||
if (this.isNextItemActive(timelineSpecification, currentDate, i)) {
|
||||
i++;
|
||||
}
|
||||
while (
|
||||
j + 1 < this.transactionPoints.length &&
|
||||
!isAfter(parseDate(this.transactionPoints[j + 1].date), currentDate)
|
||||
) {
|
||||
j++;
|
||||
}
|
||||
|
||||
let periodEndDate = currentDate;
|
||||
if (timelineSpecification[i].accuracy === 'day') {
|
||||
let nextEndDate = end;
|
||||
if (j + 1 < this.transactionPoints.length) {
|
||||
nextEndDate = parseDate(this.transactionPoints[j + 1].date);
|
||||
}
|
||||
periodEndDate = min([
|
||||
addMonths(currentDate, 3),
|
||||
max([currentDate, nextEndDate])
|
||||
]);
|
||||
}
|
||||
const timePeriodForDates = this.getTimePeriodForDate(
|
||||
j,
|
||||
currentDate,
|
||||
endOfDay(periodEndDate)
|
||||
);
|
||||
currentDate = periodEndDate;
|
||||
if (timePeriodForDates != null) {
|
||||
timelinePeriodPromises.push(timePeriodForDates);
|
||||
}
|
||||
}
|
||||
|
||||
let minNetPerformance = new Big(0);
|
||||
let maxNetPerformance = new Big(0);
|
||||
|
||||
const timelineInfoInterfaces: TimelineInfoInterface[] = await Promise.all(
|
||||
timelinePeriodPromises
|
||||
);
|
||||
|
||||
try {
|
||||
minNetPerformance = timelineInfoInterfaces
|
||||
.map((timelineInfo) => timelineInfo.minNetPerformance)
|
||||
.filter((performance) => performance !== null)
|
||||
.reduce((minPerformance, current) => {
|
||||
if (minPerformance.lt(current)) {
|
||||
return minPerformance;
|
||||
} else {
|
||||
return current;
|
||||
}
|
||||
});
|
||||
|
||||
maxNetPerformance = timelineInfoInterfaces
|
||||
.map((timelineInfo) => timelineInfo.maxNetPerformance)
|
||||
.filter((performance) => performance !== null)
|
||||
.reduce((maxPerformance, current) => {
|
||||
if (maxPerformance.gt(current)) {
|
||||
return maxPerformance;
|
||||
} else {
|
||||
return current;
|
||||
}
|
||||
});
|
||||
} catch {}
|
||||
|
||||
const timelinePeriods = timelineInfoInterfaces.map(
|
||||
(timelineInfo) => timelineInfo.timelinePeriods
|
||||
);
|
||||
|
||||
return {
|
||||
maxNetPerformance,
|
||||
minNetPerformance,
|
||||
timelinePeriods: flatten(timelinePeriods)
|
||||
};
|
||||
return Object.keys(groupedData).map((dateGroup) => ({
|
||||
date: groupBy === 'month' ? `${dateGroup}-01` : `${dateGroup}-01-01`,
|
||||
investment: groupedData[dateGroup].toNumber()
|
||||
}));
|
||||
}
|
||||
|
||||
private calculateOverallPerformance(positions: TimelinePosition[]) {
|
||||
@ -983,123 +819,6 @@ export class PortfolioCalculator {
|
||||
};
|
||||
}
|
||||
|
||||
private async getTimePeriodForDate(
|
||||
j: number,
|
||||
startDate: Date,
|
||||
endDate: Date
|
||||
): Promise<TimelineInfoInterface> {
|
||||
let investment: Big = new Big(0);
|
||||
let fees: Big = new Big(0);
|
||||
|
||||
const marketSymbolMap: {
|
||||
[date: string]: { [symbol: string]: Big };
|
||||
} = {};
|
||||
if (j >= 0) {
|
||||
const currencies: { [name: string]: string } = {};
|
||||
const dataGatheringItems: IDataGatheringItem[] = [];
|
||||
|
||||
for (const item of this.transactionPoints[j].items) {
|
||||
currencies[item.symbol] = item.currency;
|
||||
dataGatheringItems.push({
|
||||
dataSource: item.dataSource,
|
||||
symbol: item.symbol
|
||||
});
|
||||
investment = investment.plus(item.investment);
|
||||
fees = fees.plus(item.fee);
|
||||
}
|
||||
|
||||
let marketSymbols: GetValueObject[] = [];
|
||||
if (dataGatheringItems.length > 0) {
|
||||
try {
|
||||
const { values } = await this.currentRateService.getValues({
|
||||
dataGatheringItems,
|
||||
dateQuery: {
|
||||
gte: startDate,
|
||||
lt: endOfDay(endDate)
|
||||
}
|
||||
});
|
||||
marketSymbols = values;
|
||||
} catch (error) {
|
||||
Logger.error(
|
||||
`Failed to fetch info for date ${startDate} with exception`,
|
||||
error,
|
||||
'PortfolioCalculator'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
for (const marketSymbol of marketSymbols) {
|
||||
const date = format(marketSymbol.date, DATE_FORMAT);
|
||||
if (!marketSymbolMap[date]) {
|
||||
marketSymbolMap[date] = {};
|
||||
}
|
||||
if (marketSymbol.marketPrice) {
|
||||
marketSymbolMap[date][marketSymbol.symbol] = new Big(
|
||||
marketSymbol.marketPrice
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const results: TimelinePeriod[] = [];
|
||||
let maxNetPerformance: Big = null;
|
||||
let minNetPerformance: Big = null;
|
||||
for (
|
||||
let currentDate = startDate;
|
||||
isBefore(currentDate, endDate);
|
||||
currentDate = addDays(currentDate, 1)
|
||||
) {
|
||||
let value = new Big(0);
|
||||
const currentDateAsString = format(currentDate, DATE_FORMAT);
|
||||
let invalid = false;
|
||||
if (j >= 0) {
|
||||
for (const item of this.transactionPoints[j].items) {
|
||||
if (
|
||||
!marketSymbolMap[currentDateAsString]?.hasOwnProperty(item.symbol)
|
||||
) {
|
||||
invalid = true;
|
||||
break;
|
||||
}
|
||||
value = value.plus(
|
||||
item.quantity.mul(marketSymbolMap[currentDateAsString][item.symbol])
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!invalid) {
|
||||
const grossPerformance = value.minus(investment);
|
||||
const netPerformance = grossPerformance.minus(fees);
|
||||
if (
|
||||
minNetPerformance === null ||
|
||||
minNetPerformance.gt(netPerformance)
|
||||
) {
|
||||
minNetPerformance = netPerformance;
|
||||
}
|
||||
if (
|
||||
maxNetPerformance === null ||
|
||||
maxNetPerformance.lt(netPerformance)
|
||||
) {
|
||||
maxNetPerformance = netPerformance;
|
||||
}
|
||||
|
||||
const result = {
|
||||
grossPerformance,
|
||||
investment,
|
||||
netPerformance,
|
||||
value,
|
||||
date: currentDateAsString
|
||||
};
|
||||
results.push(result);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
maxNetPerformance,
|
||||
minNetPerformance,
|
||||
timelinePeriods: results
|
||||
};
|
||||
}
|
||||
|
||||
private getFactor(type: TypeOfOrder) {
|
||||
let factor: number;
|
||||
|
||||
@ -1118,17 +837,6 @@ export class PortfolioCalculator {
|
||||
return factor;
|
||||
}
|
||||
|
||||
private addToDate(date: Date, accuracy: Accuracy): Date {
|
||||
switch (accuracy) {
|
||||
case 'day':
|
||||
return addDays(date, 1);
|
||||
case 'month':
|
||||
return addMonths(date, 1);
|
||||
case 'year':
|
||||
return addYears(date, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private getSymbolMetrics({
|
||||
end,
|
||||
exchangeRates,
|
||||
@ -1165,7 +873,10 @@ export class PortfolioCalculator {
|
||||
let initialValueWithCurrencyEffect: Big;
|
||||
let investmentAtStartDate: Big;
|
||||
let investmentAtStartDateWithCurrencyEffect: Big;
|
||||
const investmentValues: { [date: string]: Big } = {};
|
||||
const investmentValuesAccumulated: { [date: string]: Big } = {};
|
||||
const investmentValuesAccumulatedWithCurrencyEffect: {
|
||||
[date: string]: Big;
|
||||
} = {};
|
||||
const investmentValuesWithCurrencyEffect: { [date: string]: Big } = {};
|
||||
let lastAveragePrice = new Big(0);
|
||||
let lastAveragePriceWithCurrencyEffect = new Big(0);
|
||||
@ -1207,7 +918,8 @@ export class PortfolioCalculator {
|
||||
hasErrors: false,
|
||||
initialValue: new Big(0),
|
||||
initialValueWithCurrencyEffect: new Big(0),
|
||||
investmentValues: {},
|
||||
investmentValuesAccumulated: {},
|
||||
investmentValuesAccumulatedWithCurrencyEffect: {},
|
||||
investmentValuesWithCurrencyEffect: {},
|
||||
netPerformance: new Big(0),
|
||||
netPerformancePercentage: new Big(0),
|
||||
@ -1246,7 +958,8 @@ export class PortfolioCalculator {
|
||||
hasErrors: true,
|
||||
initialValue: new Big(0),
|
||||
initialValueWithCurrencyEffect: new Big(0),
|
||||
investmentValues: {},
|
||||
investmentValuesAccumulated: {},
|
||||
investmentValuesAccumulatedWithCurrencyEffect: {},
|
||||
investmentValuesWithCurrencyEffect: {},
|
||||
netPerformance: new Big(0),
|
||||
netPerformancePercentage: new Big(0),
|
||||
@ -1639,11 +1352,15 @@ export class PortfolioCalculator {
|
||||
feesWithCurrencyEffect.minus(feesAtStartDateWithCurrencyEffect)
|
||||
);
|
||||
|
||||
investmentValues[order.date] = totalInvestment;
|
||||
investmentValuesAccumulated[order.date] = totalInvestment;
|
||||
|
||||
investmentValuesWithCurrencyEffect[order.date] =
|
||||
investmentValuesAccumulatedWithCurrencyEffect[order.date] =
|
||||
totalInvestmentWithCurrencyEffect;
|
||||
|
||||
investmentValuesWithCurrencyEffect[order.date] = (
|
||||
investmentValuesWithCurrencyEffect[order.date] ?? new Big(0)
|
||||
).add(transactionInvestmentWithCurrencyEffect);
|
||||
|
||||
timeWeightedInvestmentValues[order.date] =
|
||||
totalInvestmentDays > 0
|
||||
? sumOfTimeWeightedInvestments.div(totalInvestmentDays)
|
||||
@ -1801,7 +1518,8 @@ export class PortfolioCalculator {
|
||||
grossPerformancePercentageWithCurrencyEffect,
|
||||
initialValue,
|
||||
initialValueWithCurrencyEffect,
|
||||
investmentValues,
|
||||
investmentValuesAccumulated,
|
||||
investmentValuesAccumulatedWithCurrencyEffect,
|
||||
investmentValuesWithCurrencyEffect,
|
||||
netPerformancePercentage,
|
||||
netPerformancePercentageWithCurrencyEffect,
|
||||
@ -1823,15 +1541,4 @@ export class PortfolioCalculator {
|
||||
timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect
|
||||
};
|
||||
}
|
||||
|
||||
private isNextItemActive(
|
||||
timelineSpecification: TimelineSpecification[],
|
||||
currentDate: Date,
|
||||
i: number
|
||||
) {
|
||||
return (
|
||||
i + 1 < timelineSpecification.length &&
|
||||
!isBefore(currentDate, parseDate(timelineSpecification[i + 1].start))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -74,6 +74,11 @@ export class PortfolioController {
|
||||
): Promise<PortfolioDetails & { hasError: boolean }> {
|
||||
let hasDetails = true;
|
||||
let hasError = false;
|
||||
const hasReadRestrictedAccessPermission =
|
||||
this.userService.hasReadRestrictedAccessPermission({
|
||||
impersonationId,
|
||||
user: this.request.user
|
||||
});
|
||||
|
||||
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
||||
hasDetails = this.request.user.subscription.type === 'Premium';
|
||||
@ -108,7 +113,7 @@ export class PortfolioController {
|
||||
let portfolioSummary = summary;
|
||||
|
||||
if (
|
||||
impersonationId ||
|
||||
hasReadRestrictedAccessPermission ||
|
||||
this.userService.isRestrictedView(this.request.user)
|
||||
) {
|
||||
const totalInvestment = Object.values(holdings)
|
||||
@ -148,7 +153,7 @@ export class PortfolioController {
|
||||
|
||||
if (
|
||||
hasDetails === false ||
|
||||
impersonationId ||
|
||||
hasReadRestrictedAccessPermission ||
|
||||
this.userService.isRestrictedView(this.request.user)
|
||||
) {
|
||||
portfolioSummary = nullifyValuesInObject(summary, [
|
||||
@ -164,6 +169,7 @@ export class PortfolioController {
|
||||
'excludedAccountsAndActivities',
|
||||
'fees',
|
||||
'fireWealth',
|
||||
'interest',
|
||||
'items',
|
||||
'liabilities',
|
||||
'netWorth',
|
||||
@ -216,6 +222,12 @@ export class PortfolioController {
|
||||
@Query('range') dateRange: DateRange = 'max',
|
||||
@Query('tags') filterByTags?: string
|
||||
): Promise<PortfolioDividends> {
|
||||
const hasReadRestrictedAccessPermission =
|
||||
this.userService.hasReadRestrictedAccessPermission({
|
||||
impersonationId,
|
||||
user: this.request.user
|
||||
});
|
||||
|
||||
const filters = this.apiService.buildFiltersFromQueryParams({
|
||||
filterByAccounts,
|
||||
filterByAssetClasses,
|
||||
@ -230,7 +242,7 @@ export class PortfolioController {
|
||||
});
|
||||
|
||||
if (
|
||||
impersonationId ||
|
||||
hasReadRestrictedAccessPermission ||
|
||||
this.userService.isRestrictedView(this.request.user)
|
||||
) {
|
||||
const maxDividend = dividends.reduce(
|
||||
@ -266,6 +278,12 @@ export class PortfolioController {
|
||||
@Query('range') dateRange: DateRange = 'max',
|
||||
@Query('tags') filterByTags?: string
|
||||
): Promise<PortfolioInvestments> {
|
||||
const hasReadRestrictedAccessPermission =
|
||||
this.userService.hasReadRestrictedAccessPermission({
|
||||
impersonationId,
|
||||
user: this.request.user
|
||||
});
|
||||
|
||||
const filters = this.apiService.buildFiltersFromQueryParams({
|
||||
filterByAccounts,
|
||||
filterByAssetClasses,
|
||||
@ -281,7 +299,7 @@ export class PortfolioController {
|
||||
});
|
||||
|
||||
if (
|
||||
impersonationId ||
|
||||
hasReadRestrictedAccessPermission ||
|
||||
this.userService.isRestrictedView(this.request.user)
|
||||
) {
|
||||
const maxInvestment = investments.reduce(
|
||||
@ -329,6 +347,12 @@ export class PortfolioController {
|
||||
@Query('tags') filterByTags?: string,
|
||||
@Query('withExcludedAccounts') withExcludedAccounts = false
|
||||
): Promise<PortfolioPerformanceResponse> {
|
||||
const hasReadRestrictedAccessPermission =
|
||||
this.userService.hasReadRestrictedAccessPermission({
|
||||
impersonationId,
|
||||
user: this.request.user
|
||||
});
|
||||
|
||||
const filters = this.apiService.buildFiltersFromQueryParams({
|
||||
filterByAccounts,
|
||||
filterByAssetClasses,
|
||||
@ -344,7 +368,7 @@ export class PortfolioController {
|
||||
});
|
||||
|
||||
if (
|
||||
impersonationId ||
|
||||
hasReadRestrictedAccessPermission ||
|
||||
this.request.user.Settings.settings.viewMode === 'ZEN' ||
|
||||
this.userService.isRestrictedView(this.request.user)
|
||||
) {
|
||||
|
@ -79,7 +79,7 @@ import {
|
||||
subDays,
|
||||
subYears
|
||||
} from 'date-fns';
|
||||
import { isEmpty, last, sortBy, uniq, uniqBy } from 'lodash';
|
||||
import { isEmpty, last, uniq, uniqBy } from 'lodash';
|
||||
|
||||
import {
|
||||
HistoricalDataContainer,
|
||||
@ -293,77 +293,32 @@ export class PortfolioService {
|
||||
|
||||
portfolioCalculator.setTransactionPoints(transactionPoints);
|
||||
|
||||
const { items } = await this.getChart({
|
||||
dateRange,
|
||||
impersonationId,
|
||||
portfolioOrders,
|
||||
transactionPoints,
|
||||
userId,
|
||||
userCurrency: this.request.user.Settings.settings.baseCurrency,
|
||||
withDataDecimation: false
|
||||
});
|
||||
|
||||
let investments: InvestmentItem[];
|
||||
|
||||
if (groupBy) {
|
||||
investments = portfolioCalculator
|
||||
.getInvestmentsByGroup(groupBy)
|
||||
.map((item) => {
|
||||
return {
|
||||
date: item.date,
|
||||
investment: item.investment.toNumber()
|
||||
};
|
||||
});
|
||||
|
||||
// Add investment of current group
|
||||
const dateOfCurrentGroup = format(
|
||||
set(new Date(), {
|
||||
date: 1,
|
||||
month: groupBy === 'year' ? 0 : new Date().getMonth()
|
||||
}),
|
||||
DATE_FORMAT
|
||||
);
|
||||
const investmentOfCurrentGroup = investments.filter(({ date }) => {
|
||||
return date === dateOfCurrentGroup;
|
||||
investments = portfolioCalculator.getInvestmentsByGroup({
|
||||
groupBy,
|
||||
data: items
|
||||
});
|
||||
|
||||
if (investmentOfCurrentGroup.length <= 0) {
|
||||
investments.push({
|
||||
date: dateOfCurrentGroup,
|
||||
investment: 0
|
||||
});
|
||||
}
|
||||
} else {
|
||||
investments = portfolioCalculator
|
||||
.getInvestments()
|
||||
.map(({ date, investment }) => {
|
||||
return {
|
||||
date,
|
||||
investment: investment.toNumber()
|
||||
};
|
||||
});
|
||||
|
||||
// Add investment of today
|
||||
const investmentOfToday = investments.filter(({ date }) => {
|
||||
return date === format(new Date(), DATE_FORMAT);
|
||||
investments = items.map(({ date, investmentValueWithCurrencyEffect }) => {
|
||||
return {
|
||||
date,
|
||||
investment: investmentValueWithCurrencyEffect
|
||||
};
|
||||
});
|
||||
|
||||
if (investmentOfToday.length <= 0) {
|
||||
const pastInvestments = investments.filter(({ date }) => {
|
||||
return isBefore(parseDate(date), new Date());
|
||||
});
|
||||
const lastInvestment = pastInvestments[pastInvestments.length - 1];
|
||||
|
||||
investments.push({
|
||||
date: format(new Date(), DATE_FORMAT),
|
||||
investment: lastInvestment?.investment ?? 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
investments = sortBy(investments, ({ date }) => {
|
||||
return date;
|
||||
});
|
||||
|
||||
const startDate = this.getStartDate(
|
||||
dateRange,
|
||||
parseDate(investments[0]?.date)
|
||||
);
|
||||
|
||||
investments = investments.filter(({ date }) => {
|
||||
return !isBefore(parseDate(date), startDate);
|
||||
});
|
||||
|
||||
let streaks: PortfolioInvestments['streaks'];
|
||||
|
||||
if (savingsRate) {
|
||||
@ -1448,7 +1403,8 @@ export class PortfolioService {
|
||||
portfolioOrders,
|
||||
transactionPoints,
|
||||
userCurrency,
|
||||
userId
|
||||
userId,
|
||||
withDataDecimation = true
|
||||
}: {
|
||||
dateRange?: DateRange;
|
||||
impersonationId: string;
|
||||
@ -1456,6 +1412,7 @@ export class PortfolioService {
|
||||
transactionPoints: TransactionPoint[];
|
||||
userCurrency: string;
|
||||
userId: string;
|
||||
withDataDecimation?: boolean;
|
||||
}): Promise<HistoricalDataContainer> {
|
||||
if (transactionPoints.length === 0) {
|
||||
return {
|
||||
@ -1481,16 +1438,18 @@ export class PortfolioService {
|
||||
const portfolioStart = parseDate(transactionPoints[0].date);
|
||||
const startDate = this.getStartDate(dateRange, portfolioStart);
|
||||
|
||||
const daysInMarket = differenceInDays(new Date(), startDate);
|
||||
const step = Math.round(
|
||||
daysInMarket / Math.min(daysInMarket, MAX_CHART_ITEMS)
|
||||
);
|
||||
let step = 1;
|
||||
|
||||
const items = await portfolioCalculator.getChartData(
|
||||
startDate,
|
||||
endDate,
|
||||
step
|
||||
);
|
||||
if (withDataDecimation) {
|
||||
const daysInMarket = differenceInDays(new Date(), startDate);
|
||||
step = Math.round(daysInMarket / Math.min(daysInMarket, MAX_CHART_ITEMS));
|
||||
}
|
||||
|
||||
const items = await portfolioCalculator.getChartData({
|
||||
step,
|
||||
end: endDate,
|
||||
start: startDate
|
||||
});
|
||||
|
||||
return {
|
||||
items,
|
||||
|
@ -38,6 +38,10 @@ export class UpdateUserSettingDto {
|
||||
@IsOptional()
|
||||
emergencyFund?: number;
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
'filters.accounts'?: string[];
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
'filters.tags'?: string[];
|
||||
|
@ -105,6 +105,24 @@ export class UserService {
|
||||
return usersWithAdminRole.length > 0;
|
||||
}
|
||||
|
||||
public hasReadRestrictedAccessPermission({
|
||||
impersonationId,
|
||||
user
|
||||
}: {
|
||||
impersonationId: string;
|
||||
user: UserWithSettings;
|
||||
}) {
|
||||
if (!impersonationId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const access = user.Access?.find(({ id }) => {
|
||||
return id === impersonationId;
|
||||
});
|
||||
|
||||
return access?.permissions?.includes('READ_RESTRICTED') ?? true;
|
||||
}
|
||||
|
||||
public isRestrictedView(aUser: UserWithSettings) {
|
||||
return aUser.Settings.settings.isRestrictedView ?? false;
|
||||
}
|
||||
@ -113,6 +131,7 @@ export class UserService {
|
||||
userWhereUniqueInput: Prisma.UserWhereUniqueInput
|
||||
): Promise<UserWithSettings | null> {
|
||||
const {
|
||||
Access,
|
||||
accessToken,
|
||||
Account,
|
||||
Analytics,
|
||||
@ -127,6 +146,7 @@ export class UserService {
|
||||
updatedAt
|
||||
} = await this.prismaService.user.findUnique({
|
||||
include: {
|
||||
Access: true,
|
||||
Account: {
|
||||
include: { Platform: true }
|
||||
},
|
||||
@ -138,6 +158,7 @@ export class UserService {
|
||||
});
|
||||
|
||||
const user: UserWithSettings = {
|
||||
Access,
|
||||
accessToken,
|
||||
Account,
|
||||
authChallenge,
|
||||
@ -198,18 +219,18 @@ export class UserService {
|
||||
new Date(),
|
||||
user.createdAt
|
||||
);
|
||||
let frequency = 15;
|
||||
let frequency = 10;
|
||||
|
||||
if (daysSinceRegistration > 365) {
|
||||
frequency = 2;
|
||||
} else if (daysSinceRegistration > 180) {
|
||||
frequency = 3;
|
||||
} else if (daysSinceRegistration > 60) {
|
||||
frequency = 5;
|
||||
frequency = 4;
|
||||
} else if (daysSinceRegistration > 30) {
|
||||
frequency = 8;
|
||||
frequency = 6;
|
||||
} else if (daysSinceRegistration > 15) {
|
||||
frequency = 12;
|
||||
frequency = 8;
|
||||
}
|
||||
|
||||
if (Analytics?.activityCount % frequency === 1) {
|
||||
|
@ -230,6 +230,10 @@
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-vyzer</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-wealthfolio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-wealthica</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -568,6 +572,10 @@
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-vyzer</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-wealthfolio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-wealthica</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -926,6 +934,10 @@
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-vyzer</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-wealthfolio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-wealthica</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -1130,6 +1142,10 @@
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-vyzer</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-wealthfolio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-wealthica</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||
import { redactAttributes } from '@ghostfolio/api/helper/object.helper';
|
||||
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
|
||||
import { UserWithSettings } from '@ghostfolio/common/types';
|
||||
import {
|
||||
CallHandler,
|
||||
ExecutionContext,
|
||||
@ -22,13 +23,20 @@ export class RedactValuesInResponseInterceptor<T>
|
||||
): Observable<any> {
|
||||
return next.handle().pipe(
|
||||
map((data: any) => {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const hasImpersonationId =
|
||||
!!request.headers?.[HEADER_KEY_IMPERSONATION.toLowerCase()];
|
||||
const { headers, user }: { headers: Headers; user: UserWithSettings } =
|
||||
context.switchToHttp().getRequest();
|
||||
|
||||
const impersonationId =
|
||||
headers?.[HEADER_KEY_IMPERSONATION.toLowerCase()];
|
||||
const hasReadRestrictedPermission =
|
||||
this.userService.hasReadRestrictedAccessPermission({
|
||||
impersonationId,
|
||||
user
|
||||
});
|
||||
|
||||
if (
|
||||
hasImpersonationId ||
|
||||
this.userService.isRestrictedView(request.user)
|
||||
hasReadRestrictedPermission ||
|
||||
this.userService.isRestrictedView(user)
|
||||
) {
|
||||
data = redactAttributes({
|
||||
object: data,
|
||||
|
@ -24,7 +24,7 @@ export class ApiService {
|
||||
const searchQuery = filterBySearchQuery?.toLowerCase();
|
||||
const tagIds = filterByTags?.split(',') ?? [];
|
||||
|
||||
return [
|
||||
const filters = [
|
||||
...accountIds.map((accountId) => {
|
||||
return <Filter>{
|
||||
id: accountId,
|
||||
@ -43,10 +43,6 @@ export class ApiService {
|
||||
type: 'ASSET_SUB_CLASS'
|
||||
};
|
||||
}),
|
||||
{
|
||||
id: searchQuery,
|
||||
type: 'SEARCH_QUERY'
|
||||
},
|
||||
...tagIds.map((tagId) => {
|
||||
return <Filter>{
|
||||
id: tagId,
|
||||
@ -54,5 +50,14 @@ export class ApiService {
|
||||
};
|
||||
})
|
||||
];
|
||||
|
||||
if (searchQuery) {
|
||||
filters.push({
|
||||
id: searchQuery,
|
||||
type: 'SEARCH_QUERY'
|
||||
});
|
||||
}
|
||||
|
||||
return filters;
|
||||
}
|
||||
}
|
||||
|
@ -62,9 +62,9 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
||||
}, this.configurationService.get('REQUEST_TIMEOUT'));
|
||||
|
||||
return got(
|
||||
`${TrackinsightDataEnhancerService.baseUrl}/funds/${symbol.split(
|
||||
'.'
|
||||
)?.[0]}.json`,
|
||||
`${TrackinsightDataEnhancerService.baseUrl}/funds/${
|
||||
symbol.split('.')?.[0]
|
||||
}.json`,
|
||||
{
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
@ -104,9 +104,9 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
||||
}, this.configurationService.get('REQUEST_TIMEOUT'));
|
||||
|
||||
return got(
|
||||
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${symbol.split(
|
||||
'.'
|
||||
)?.[0]}.json`,
|
||||
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${
|
||||
symbol.split('.')?.[0]
|
||||
}.json`,
|
||||
{
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
|
@ -1,7 +1,11 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
|
||||
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
|
||||
import { DEFAULT_CURRENCY, UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||
import {
|
||||
DEFAULT_CURRENCY,
|
||||
REPLACE_NAME_PARTS,
|
||||
UNKNOWN_KEY
|
||||
} from '@ghostfolio/common/config';
|
||||
import { isCurrency } from '@ghostfolio/common/helper';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import {
|
||||
@ -137,18 +141,11 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
|
||||
if (name) {
|
||||
name = name.replace('&', '&');
|
||||
|
||||
name = name.replace('Amundi Index Solutions - ', '');
|
||||
name = name.replace('iShares ETF (CH) - ', '');
|
||||
name = name.replace('iShares III Public Limited Company - ', '');
|
||||
name = name.replace('iShares V PLC - ', '');
|
||||
name = name.replace('iShares VI Public Limited Company - ', '');
|
||||
name = name.replace('iShares VII PLC - ', '');
|
||||
name = name.replace('Multi Units Luxembourg - ', '');
|
||||
name = name.replace('VanEck ETFs N.V. - ', '');
|
||||
name = name.replace('Vaneck Vectors Ucits Etfs Plc - ', '');
|
||||
name = name.replace('Vanguard Funds Public Limited Company - ', '');
|
||||
name = name.replace('Vanguard Index Funds - ', '');
|
||||
name = name.replace('Xtrackers (IE) Plc - ', '');
|
||||
for (const part of REPLACE_NAME_PARTS) {
|
||||
name = name.replace(part, '');
|
||||
}
|
||||
|
||||
name = name.trim();
|
||||
}
|
||||
|
||||
if (quoteType === 'FUTURE') {
|
||||
|
@ -11,7 +11,10 @@ import {
|
||||
IDataProviderHistoricalResponse,
|
||||
IDataProviderResponse
|
||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
|
||||
import {
|
||||
DEFAULT_CURRENCY,
|
||||
REPLACE_NAME_PARTS
|
||||
} from '@ghostfolio/common/config';
|
||||
import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import {
|
||||
@ -22,6 +25,7 @@ import {
|
||||
} from '@prisma/client';
|
||||
import { addDays, format, isSameDay, isToday } from 'date-fns';
|
||||
import got from 'got';
|
||||
import { isNumber } from 'lodash';
|
||||
|
||||
@Injectable()
|
||||
export class EodHistoricalDataService implements DataProviderInterface {
|
||||
@ -144,10 +148,17 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
).json<any>();
|
||||
|
||||
return response.reduce(
|
||||
(result, historicalItem, index, array) => {
|
||||
result[this.convertFromEodSymbol(symbol)][historicalItem.date] = {
|
||||
marketPrice: historicalItem.close
|
||||
};
|
||||
(result, { close, date }, index, array) => {
|
||||
if (isNumber(close)) {
|
||||
result[this.convertFromEodSymbol(symbol)][date] = {
|
||||
marketPrice: close
|
||||
};
|
||||
} else {
|
||||
Logger.error(
|
||||
`Could not get historical market data for ${symbol} (${this.getName()}) at ${date}`,
|
||||
'EodHistoricalDataService'
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
@ -232,14 +243,23 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
return lookupItem.symbol === code;
|
||||
})?.currency;
|
||||
|
||||
result[this.convertFromEodSymbol(code)] = {
|
||||
currency:
|
||||
currency ??
|
||||
this.convertFromEodSymbol(code)?.replace(DEFAULT_CURRENCY, ''),
|
||||
dataSource: DataSource.EOD_HISTORICAL_DATA,
|
||||
marketPrice: close,
|
||||
marketState: isToday(new Date(timestamp * 1000)) ? 'open' : 'closed'
|
||||
};
|
||||
if (isNumber(close)) {
|
||||
result[this.convertFromEodSymbol(code)] = {
|
||||
currency:
|
||||
currency ??
|
||||
this.convertFromEodSymbol(code)?.replace(DEFAULT_CURRENCY, ''),
|
||||
dataSource: this.getName(),
|
||||
marketPrice: close,
|
||||
marketState: isToday(new Date(timestamp * 1000))
|
||||
? 'open'
|
||||
: 'closed'
|
||||
};
|
||||
} else {
|
||||
Logger.error(
|
||||
`Could not get quote for ${this.convertFromEodSymbol(code)} (${this.getName()})`,
|
||||
'EodHistoricalDataService'
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
@ -345,6 +365,18 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
return aSymbol;
|
||||
}
|
||||
|
||||
private formatName({ name }: { name: string }) {
|
||||
if (name) {
|
||||
for (const part of REPLACE_NAME_PARTS) {
|
||||
name = name.replace(part, '');
|
||||
}
|
||||
|
||||
name = name.trim();
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
private async getSearchResult(aQuery: string): Promise<
|
||||
(LookupItem & {
|
||||
assetClass: AssetClass;
|
||||
@ -380,9 +412,9 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
assetClass,
|
||||
assetSubClass,
|
||||
isin,
|
||||
name,
|
||||
currency: this.convertCurrency(Currency),
|
||||
dataSource: this.getName(),
|
||||
name: this.formatName({ name }),
|
||||
symbol: `${Code}.${Exchange}`
|
||||
};
|
||||
}
|
||||
|
@ -42,10 +42,21 @@ export class ManualService implements DataProviderInterface {
|
||||
public async getAssetProfile(
|
||||
aSymbol: string
|
||||
): Promise<Partial<SymbolProfile>> {
|
||||
return {
|
||||
const assetProfile: Partial<SymbolProfile> = {
|
||||
dataSource: this.getName(),
|
||||
symbol: aSymbol
|
||||
};
|
||||
|
||||
const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles([
|
||||
{ dataSource: this.getName(), symbol: aSymbol }
|
||||
]);
|
||||
|
||||
if (symbolProfile) {
|
||||
assetProfile.currency = symbolProfile.currency;
|
||||
assetProfile.name = symbolProfile.name;
|
||||
}
|
||||
|
||||
return assetProfile;
|
||||
}
|
||||
|
||||
public async getDividends({}: GetDividendsParams) {
|
||||
|
@ -38,7 +38,7 @@
|
||||
[pageTitle]="pageTitle"
|
||||
[user]="user"
|
||||
(signOut)="onSignOut()"
|
||||
></gf-header>
|
||||
/>
|
||||
</header>
|
||||
|
||||
<main role="main">
|
||||
|
@ -17,8 +17,13 @@
|
||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Permission</th>
|
||||
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell>
|
||||
<div class="align-items-center d-flex">
|
||||
<ion-icon class="mr-1" name="lock-closed-outline" />
|
||||
<ng-container i18n>Restricted View</ng-container>
|
||||
@if (element.permissions.includes('READ')) {
|
||||
<ion-icon class="mr-1" name="lock-open-outline" />
|
||||
<ng-container i18n>View</ng-container>
|
||||
} @else if (element.permissions.includes('READ_RESTRICTED')) {
|
||||
<ion-icon class="mr-1" name="lock-closed-outline" />
|
||||
<ng-container i18n>Restricted view</ng-container>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
@ -178,7 +178,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
this.dataService
|
||||
.fetchExport(activityIds)
|
||||
.fetchExport({ activityIds })
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((data) => {
|
||||
downloadAsFile({
|
||||
|
@ -4,7 +4,7 @@
|
||||
[deviceType]="data.deviceType"
|
||||
[title]="name"
|
||||
(closeButtonClicked)="onClose()"
|
||||
></gf-dialog-header>
|
||||
/>
|
||||
|
||||
<div class="flex-grow-1" mat-dialog-content>
|
||||
<div class="container p-0">
|
||||
@ -16,7 +16,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[unit]="user?.settings?.baseCurrency"
|
||||
[value]="valueInBaseCurrency"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||
[isLoading]="isLoadingChart"
|
||||
[locale]="user?.settings?.locale"
|
||||
></gf-investment-chart>
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 row">
|
||||
@ -79,7 +79,7 @@
|
||||
[deviceType]="data.deviceType"
|
||||
[holdings]="holdings"
|
||||
[locale]="user?.settings?.locale"
|
||||
></gf-holdings-table>
|
||||
/>
|
||||
</mat-tab>
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
@ -102,7 +102,7 @@
|
||||
[totalItems]="totalItems"
|
||||
(export)="onExport()"
|
||||
(sortChanged)="onSortChanged($event)"
|
||||
></gf-activities-table-lazy>
|
||||
/>
|
||||
<gf-activities-table
|
||||
*ngIf="user?.settings?.isExperimentalFeatures !== true"
|
||||
[activities]="activities"
|
||||
@ -115,7 +115,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[showActions]="false"
|
||||
(export)="onExport()"
|
||||
></gf-activities-table>
|
||||
/>
|
||||
</mat-tab>
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
@ -128,7 +128,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[showActions]="!hasImpersonationId && hasPermissionToDeleteAccountBalance && !user.settings.isRestrictedView"
|
||||
(accountBalanceDeleted)="onDeleteAccountBalance($event)"
|
||||
></gf-account-balances>
|
||||
/>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
@ -138,4 +138,4 @@
|
||||
mat-dialog-actions
|
||||
[deviceType]="data.deviceType"
|
||||
(closeButtonClicked)="onClose()"
|
||||
></gf-dialog-footer>
|
||||
/>
|
||||
|
@ -39,7 +39,7 @@
|
||||
class="d-inline d-sm-none mr-1"
|
||||
[tooltip]="element.Platform?.name"
|
||||
[url]="element.Platform?.url"
|
||||
></gf-symbol-icon>
|
||||
/>
|
||||
<span>{{ element.name }} </span>
|
||||
<span
|
||||
*ngIf="element.isDefault"
|
||||
@ -83,7 +83,7 @@
|
||||
class="mr-1"
|
||||
[tooltip]="element.Platform?.name"
|
||||
[url]="element.Platform?.url"
|
||||
></gf-symbol-icon>
|
||||
/>
|
||||
<span>{{ element.Platform?.name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
@ -131,7 +131,7 @@
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="element.balance"
|
||||
></gf-value>
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
*matFooterCellDef
|
||||
@ -143,7 +143,7 @@
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="totalBalanceInBaseCurrency"
|
||||
></gf-value>
|
||||
/>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@ -166,7 +166,7 @@
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="element.value"
|
||||
></gf-value>
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
*matFooterCellDef
|
||||
@ -178,7 +178,7 @@
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="totalValueInBaseCurrency"
|
||||
></gf-value>
|
||||
/>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@ -201,7 +201,7 @@
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="element.valueInBaseCurrency"
|
||||
></gf-value>
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
*matFooterCellDef
|
||||
@ -213,7 +213,7 @@
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="totalValueInBaseCurrency"
|
||||
></gf-value>
|
||||
/>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@ -296,4 +296,4 @@
|
||||
height: '1.5rem',
|
||||
width: '100%'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
|
@ -8,7 +8,7 @@
|
||||
[showXAxis]="true"
|
||||
[showYAxis]="true"
|
||||
[symbol]="symbol"
|
||||
></gf-line-chart>
|
||||
/>
|
||||
<div
|
||||
*ngFor="let itemByMonth of marketDataByMonth | keyvalue"
|
||||
class="d-flex"
|
||||
|
@ -6,7 +6,7 @@
|
||||
[isLoading]="isLoading"
|
||||
[placeholder]="placeholder"
|
||||
(valueChanged)="filters$.next($event)"
|
||||
></gf-activities-filter>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@ -213,7 +213,7 @@
|
||||
height: '1.5rem',
|
||||
width: '100%'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -50,7 +50,7 @@
|
||||
[marketData]="marketDataDetails"
|
||||
[symbol]="data.symbol"
|
||||
(marketDataChanged)="onMarketDataChanged($event)"
|
||||
></gf-admin-market-data-detail>
|
||||
/>
|
||||
|
||||
<div class="mt-3" formGroupName="historicalData">
|
||||
<mat-form-field appearance="outline" class="w-100 without-hint">
|
||||
@ -162,7 +162,7 @@
|
||||
[keys]="['name']"
|
||||
[maxItems]="10"
|
||||
[positions]="sectors"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="h5" i18n>Countries</div>
|
||||
@ -172,7 +172,7 @@
|
||||
[keys]="['name']"
|
||||
[maxItems]="10"
|
||||
[positions]="countries"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
@ -16,7 +16,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[precision]="0"
|
||||
[value]="userCount"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex my-3">
|
||||
@ -26,7 +26,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[precision]="0"
|
||||
[value]="transactionCount"
|
||||
></gf-value>
|
||||
/>
|
||||
<div *ngIf="transactionCount && userCount">
|
||||
{{ transactionCount / userCount | number : '1.2-2' }}
|
||||
<span i18n>per User</span>
|
||||
@ -39,10 +39,7 @@
|
||||
<table>
|
||||
<tr *ngFor="let exchangeRate of exchangeRates">
|
||||
<td>
|
||||
<gf-value
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="1"
|
||||
></gf-value>
|
||||
<gf-value [locale]="user?.settings?.locale" [value]="1" />
|
||||
</td>
|
||||
<td class="pl-1">{{ exchangeRate.label1 }}</td>
|
||||
<td class="px-1">=</td>
|
||||
@ -52,7 +49,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[precision]="4"
|
||||
[value]="exchangeRate.value"
|
||||
></gf-value>
|
||||
/>
|
||||
</td>
|
||||
<td class="pl-1">{{ exchangeRate.label2 }}</td>
|
||||
<td>
|
||||
|
@ -35,7 +35,7 @@
|
||||
class="d-inline mr-1"
|
||||
[tooltip]="element.name"
|
||||
[url]="element.url"
|
||||
></gf-symbol-icon>
|
||||
/>
|
||||
<span>{{ element.name }}</span>
|
||||
</td></ng-container
|
||||
>
|
||||
|
@ -46,7 +46,7 @@
|
||||
class="ml-1"
|
||||
[enableLink]="false"
|
||||
[title]="'Expires ' + formatDistanceToNow(element.subscription.expiresAt) + ' (' + (element.subscription.expiresAt | date: defaultDateFormat) + ')'"
|
||||
></gf-premium-indicator>
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
@ -107,7 +107,7 @@
|
||||
class="d-inline-block justify-content-end"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="element.accountCount"
|
||||
></gf-value>
|
||||
/>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@ -128,7 +128,7 @@
|
||||
class="d-inline-block justify-content-end"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="element.transactionCount"
|
||||
></gf-value>
|
||||
/>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@ -153,7 +153,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[precision]="0"
|
||||
[value]="element.engagement"
|
||||
></gf-value>
|
||||
/>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
<gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-xs-12 d-flex justify-content-end">
|
||||
@ -50,7 +50,7 @@
|
||||
height: '100%',
|
||||
width: '100%'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
<canvas
|
||||
#chartCanvas
|
||||
class="h-100"
|
||||
|
@ -19,5 +19,5 @@
|
||||
[theme]="{
|
||||
height: '100%'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
</div>
|
||||
|
@ -7,7 +7,7 @@
|
||||
[ngClass]="{ 'w-100': hasTabs }"
|
||||
[routerLink]="['/']"
|
||||
>
|
||||
<gf-logo class="px-2" [label]="pageTitle"></gf-logo>
|
||||
<gf-logo class="px-2" [label]="pageTitle" />
|
||||
</a>
|
||||
</div>
|
||||
<span class="spacer"></span>
|
||||
@ -141,7 +141,7 @@
|
||||
[user]="user"
|
||||
(closed)="closeAssistant()"
|
||||
(dateRangeChanged)="onDateRangeChange($event)"
|
||||
(selectedTagChanged)="onSelectedTagChanged($event)"
|
||||
(filtersChanged)="onFiltersChanged($event)"
|
||||
/>
|
||||
</mat-menu>
|
||||
</li>
|
||||
@ -165,6 +165,32 @@
|
||||
/>
|
||||
</button>
|
||||
<mat-menu #accountMenu="matMenu" xPosition="before">
|
||||
<ng-container
|
||||
*ngIf="
|
||||
hasPermissionForSubscription &&
|
||||
user?.subscription?.type === 'Basic'
|
||||
"
|
||||
>
|
||||
<a class="d-flex" mat-menu-item [routerLink]="routerLinkPricing"
|
||||
><span class="align-items-center d-flex"
|
||||
><span
|
||||
><ng-container
|
||||
*ngIf="user.subscription.offer === 'default'"
|
||||
i18n
|
||||
>Upgrade Plan</ng-container
|
||||
>
|
||||
<ng-container
|
||||
*ngIf="user.subscription.offer === 'renewal'"
|
||||
i18n
|
||||
>Renew Plan</ng-container
|
||||
></span
|
||||
>
|
||||
<gf-premium-indicator
|
||||
class="d-inline-block ml-1"
|
||||
[enableLink]="false" /></span
|
||||
></a>
|
||||
<hr class="m-0" />
|
||||
</ng-container>
|
||||
<ng-container *ngIf="user?.access?.length > 0">
|
||||
<button mat-menu-item (click)="impersonateAccount(null)">
|
||||
<span class="align-items-center d-flex">
|
||||
@ -295,7 +321,7 @@
|
||||
class="px-2"
|
||||
[label]="pageTitle"
|
||||
[showLabel]="currentRoute !== 'register'"
|
||||
></gf-logo>
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<span class="spacer"></span>
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatMenuTrigger } from '@angular/material/menu';
|
||||
import { Router } from '@angular/router';
|
||||
import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto';
|
||||
import { LoginWithAccessTokenDialog } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.component';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
@ -20,11 +21,10 @@ import {
|
||||
} from '@ghostfolio/client/services/settings-storage.service';
|
||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { InfoItem, User } from '@ghostfolio/common/interfaces';
|
||||
import { Filter, InfoItem, User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { DateRange } from '@ghostfolio/common/types';
|
||||
import { AssistantComponent } from '@ghostfolio/ui/assistant/assistant.component';
|
||||
import { Tag } from '@prisma/client';
|
||||
import { EMPTY, Subject } from 'rxjs';
|
||||
import { catchError, takeUntil } from 'rxjs/operators';
|
||||
|
||||
@ -162,6 +162,34 @@ export class HeaderComponent implements OnChanges {
|
||||
});
|
||||
}
|
||||
|
||||
public onFiltersChanged(filters: Filter[]) {
|
||||
const userSetting: UpdateUserSettingDto = {};
|
||||
|
||||
for (const filter of filters) {
|
||||
let filtersType: string;
|
||||
|
||||
if (filter.type === 'ACCOUNT') {
|
||||
filtersType = 'accounts';
|
||||
} else if (filter.type === 'TAG') {
|
||||
filtersType = 'tags';
|
||||
}
|
||||
|
||||
userSetting[`filters.${filtersType}`] = filter.id ? [filter.id] : null;
|
||||
}
|
||||
|
||||
this.dataService
|
||||
.putUserSetting(userSetting)
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
this.userService.remove();
|
||||
|
||||
this.userService
|
||||
.get()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe();
|
||||
});
|
||||
}
|
||||
|
||||
public onMenuClosed() {
|
||||
this.isMenuOpen = false;
|
||||
}
|
||||
@ -174,20 +202,6 @@ export class HeaderComponent implements OnChanges {
|
||||
this.assistantElement.initialize();
|
||||
}
|
||||
|
||||
public onSelectedTagChanged(tag: Tag) {
|
||||
this.dataService
|
||||
.putUserSetting({ 'filters.tags': tag ? [tag.id] : null })
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
this.userService.remove();
|
||||
|
||||
this.userService
|
||||
.get()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe();
|
||||
});
|
||||
}
|
||||
|
||||
public onSignOut() {
|
||||
this.signOut.next();
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import { RouterModule } from '@angular/router';
|
||||
import { LoginWithAccessTokenDialogModule } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.module';
|
||||
import { GfAssistantModule } from '@ghostfolio/ui/assistant';
|
||||
import { GfLogoModule } from '@ghostfolio/ui/logo';
|
||||
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
||||
|
||||
import { HeaderComponent } from './header.component';
|
||||
|
||||
@ -17,6 +18,7 @@ import { HeaderComponent } from './header.component';
|
||||
CommonModule,
|
||||
GfAssistantModule,
|
||||
GfLogoModule,
|
||||
GfPremiumIndicatorModule,
|
||||
LoginWithAccessTokenDialogModule,
|
||||
MatButtonModule,
|
||||
MatMenuModule,
|
||||
|
@ -5,7 +5,7 @@
|
||||
[isLoading]="positions === undefined"
|
||||
[options]="dateRangeOptions"
|
||||
(change)="onChangeDateRange($event.value)"
|
||||
></gf-toggle>
|
||||
/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="align-items-center col-xs-12 col-md-8 offset-md-2">
|
||||
@ -18,7 +18,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[positions]="positions"
|
||||
[range]="user?.settings?.dateRange"
|
||||
></gf-positions>
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<div *ngIf="hasPermissionToCreateOrder" class="text-center">
|
||||
|
@ -18,11 +18,11 @@
|
||||
[yMaxLabel]="greedLabel"
|
||||
[yMin]="0"
|
||||
[yMinLabel]="fearLabel"
|
||||
></gf-line-chart>
|
||||
/>
|
||||
<gf-fear-and-greed-index
|
||||
class="d-flex justify-content-center"
|
||||
[fearAndGreedIndex]="fearAndGreedIndex"
|
||||
></gf-fear-and-greed-index>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -32,7 +32,7 @@
|
||||
[benchmarks]="benchmarks"
|
||||
[locale]="user?.settings?.locale"
|
||||
[user]="user"
|
||||
></gf-benchmark>
|
||||
/>
|
||||
<ngx-skeleton-loader
|
||||
*ngIf="isLoading"
|
||||
animation="pulse"
|
||||
@ -41,7 +41,7 @@
|
||||
height: '1.5rem',
|
||||
width: '100%'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -74,7 +74,6 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
|
||||
this.showDetails =
|
||||
!this.hasImpersonationId &&
|
||||
!this.user.settings.isRestrictedView &&
|
||||
this.user.settings.viewMode !== 'ZEN';
|
||||
|
||||
|
@ -78,7 +78,7 @@
|
||||
[showLoader]="false"
|
||||
[showXAxis]="false"
|
||||
[showYAxis]="false"
|
||||
></gf-line-chart>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -95,7 +95,7 @@
|
||||
[performance]="performance"
|
||||
[showDetails]="showDetails"
|
||||
[unit]="unit"
|
||||
></gf-portfolio-performance>
|
||||
/>
|
||||
<div
|
||||
*ngIf="showDetails && !user?.settings?.isExperimentalFeatures"
|
||||
class="text-center"
|
||||
@ -105,7 +105,7 @@
|
||||
[isLoading]="isLoadingPerformance"
|
||||
[options]="dateRangeOptions"
|
||||
(change)="onChangeDateRange($event.value)"
|
||||
></gf-toggle>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -12,7 +12,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[summary]="summary"
|
||||
(emergencyFundChanged)="onChangeEmergencyFund($event)"
|
||||
></gf-portfolio-summary>
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
@ -5,7 +5,7 @@
|
||||
height: '100%',
|
||||
width: '100%'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
<canvas
|
||||
#chartCanvas
|
||||
class="h-100"
|
||||
|
@ -2,7 +2,7 @@
|
||||
mat-dialog-title
|
||||
[title]="data.title"
|
||||
(closeButtonClicked)="onClose()"
|
||||
></gf-dialog-header>
|
||||
/>
|
||||
|
||||
<div class="py-3" mat-dialog-content>
|
||||
<div class="align-items-center d-flex flex-column">
|
||||
|
@ -1,14 +1,12 @@
|
||||
<div class="container p-0">
|
||||
<div class="no-gutters row">
|
||||
<div
|
||||
class="status-container text-muted text-right"
|
||||
(click)="onShowErrors()"
|
||||
>
|
||||
<div class="status-container text-muted text-right">
|
||||
@if (errors?.length > 0 && !isLoading) {
|
||||
<ion-icon
|
||||
i18n-title
|
||||
name="time-outline"
|
||||
title="Oops! Our data provider partner is experiencing the hiccups."
|
||||
title="Oops! A data provider is experiencing the hiccups."
|
||||
(click)="onShowErrors()"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
@ -20,7 +18,7 @@
|
||||
height: '4rem',
|
||||
width: '15rem'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="display-4 font-weight-bold m-0 text-center value-container"
|
||||
@ -43,7 +41,7 @@
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : performance?.currentNetPerformance"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<gf-value
|
||||
@ -53,7 +51,7 @@
|
||||
[value]="
|
||||
isLoading ? undefined : performance?.currentNetPerformancePercent
|
||||
"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -58,7 +58,7 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
|
||||
duration: 1,
|
||||
separator: getNumberFormatGroup(this.locale)
|
||||
}).start();
|
||||
} else if (this.performance?.currentValue === null) {
|
||||
} else if (this.showDetails === false) {
|
||||
new CountUp(
|
||||
'value',
|
||||
this.performance?.currentNetPerformancePercent * 100,
|
||||
@ -69,6 +69,8 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
|
||||
separator: getNumberFormatGroup(this.locale)
|
||||
}
|
||||
).start();
|
||||
} else {
|
||||
this.value.nativeElement.innerHTML = '*****';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
<div class="flex-grow-1 text-truncate" i18n>Time in Market</div>
|
||||
<div class="justify-content-end">
|
||||
<gf-value class="justify-content-end" [value]="timeInMarket"></gf-value>
|
||||
<gf-value class="justify-content-end" [value]="timeInMarket" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@ -10,8 +10,8 @@
|
||||
[hidden]="summary?.ordersCount === null"
|
||||
>
|
||||
<div class="flex-grow-1 ml-3 text-truncate" i18n>
|
||||
{{ summary?.ordersCount }} {summary?.ordersCount, plural, =1 {transaction}
|
||||
other {transactions}}
|
||||
{{ summary?.ordersCount }}
|
||||
{summary?.ordersCount, plural, =1 {transaction} other {transactions}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@ -26,7 +26,7 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.totalBuy"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
@ -38,7 +38,7 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.totalSell"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@ -53,7 +53,7 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.committedFunds"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
@ -65,13 +65,17 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.currentGrossPerformance"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
<div class="align-items-center d-flex flex-grow-1 ml-3 text-truncate">
|
||||
<ng-container i18n>Gross Performance</ng-container>
|
||||
<abbr class="initialism ml-2 text-muted" title="Time-Weighted Rate of Return">(TWR)</abbr>
|
||||
<abbr
|
||||
class="initialism ml-2 text-muted"
|
||||
title="Time-Weighted Rate of Return"
|
||||
>(TWR)</abbr
|
||||
>
|
||||
</div>
|
||||
<div class="flex-column flex-wrap justify-content-end">
|
||||
<gf-value
|
||||
@ -83,7 +87,7 @@
|
||||
[value]="
|
||||
isLoading ? undefined : summary?.currentGrossPerformancePercent
|
||||
"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
@ -96,7 +100,7 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.fees"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@ -111,13 +115,17 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.currentNetPerformance"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
<div class="flex-grow-1 text-truncate ml-3">
|
||||
<ng-container i18n>Net Performance</ng-container>
|
||||
<abbr class="initialism ml-2 text-muted" title="Time-Weighted Rate of Return">(TWR)</abbr>
|
||||
<abbr
|
||||
class="initialism ml-2 text-muted"
|
||||
title="Time-Weighted Rate of Return"
|
||||
>(TWR)</abbr
|
||||
>
|
||||
</div>
|
||||
<div class="flex-column flex-wrap justify-content-end">
|
||||
<gf-value
|
||||
@ -127,7 +135,7 @@
|
||||
[isPercent]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : summary?.currentNetPerformancePercent"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@ -143,7 +151,7 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.currentValue"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
@ -155,7 +163,7 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.items"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
@ -176,7 +184,7 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.emergencyFund?.total"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
@ -189,7 +197,7 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.emergencyFund?.cash"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
@ -202,7 +210,7 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.emergencyFund?.assets"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
@ -214,7 +222,7 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.cash"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
@ -226,7 +234,7 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.excludedAccountsAndActivities"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@ -246,7 +254,7 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.liabilities"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@ -261,7 +269,7 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.netWorth"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
@ -276,7 +284,7 @@
|
||||
[isPercent]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : summary?.annualizedPerformancePercent"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@ -291,7 +299,7 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.interest"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
@ -303,7 +311,7 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.dividend"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -281,7 +281,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
this.dataService
|
||||
.fetchExport(activityIds)
|
||||
.fetchExport({ activityIds })
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((data) => {
|
||||
downloadAsFile({
|
||||
|
@ -4,7 +4,7 @@
|
||||
[deviceType]="data.deviceType"
|
||||
[title]="SymbolProfile?.name ?? SymbolProfile?.symbol"
|
||||
(closeButtonClicked)="onClose()"
|
||||
></gf-dialog-header>
|
||||
/>
|
||||
|
||||
<div class="flex-grow-1" mat-dialog-content>
|
||||
<div class="container p-0">
|
||||
@ -16,7 +16,7 @@
|
||||
[locale]="data.locale"
|
||||
[unit]="data.baseCurrency"
|
||||
[value]="value"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -33,7 +33,7 @@
|
||||
[showXAxis]="true"
|
||||
[showYAxis]="true"
|
||||
[symbol]="data.symbol"
|
||||
></gf-line-chart>
|
||||
/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6 mb-3">
|
||||
@ -222,7 +222,7 @@
|
||||
[locale]="data.locale"
|
||||
[maxItems]="10"
|
||||
[positions]="sectors"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="h5" i18n>Countries</div>
|
||||
@ -234,7 +234,7 @@
|
||||
[locale]="data.locale"
|
||||
[maxItems]="10"
|
||||
[positions]="countries"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
@ -266,7 +266,7 @@
|
||||
[sortDisabled]="true"
|
||||
[totalItems]="totalItems"
|
||||
(export)="onExport()"
|
||||
></gf-activities-table-lazy>
|
||||
/>
|
||||
<gf-activities-table
|
||||
*ngIf="user?.settings?.isExperimentalFeatures !== true"
|
||||
[activities]="activities"
|
||||
@ -280,7 +280,7 @@
|
||||
[showActions]="false"
|
||||
[showNameColumn]="false"
|
||||
(export)="onExport()"
|
||||
></gf-activities-table>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -314,4 +314,4 @@
|
||||
mat-dialog-actions
|
||||
[deviceType]="data.deviceType"
|
||||
(closeButtonClicked)="onClose()"
|
||||
></gf-dialog-footer>
|
||||
/>
|
||||
|
@ -18,7 +18,7 @@
|
||||
[marketState]="position?.marketState"
|
||||
[range]="range"
|
||||
[value]="position?.netPerformancePercentage"
|
||||
></gf-trend-indicator>
|
||||
/>
|
||||
</div>
|
||||
<div *ngIf="isLoading" class="flex-grow-1">
|
||||
<ngx-skeleton-loader
|
||||
@ -28,14 +28,14 @@
|
||||
height: '1.2rem',
|
||||
width: '12rem'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
<ngx-skeleton-loader
|
||||
animation="pulse"
|
||||
[theme]="{
|
||||
height: '1rem',
|
||||
width: '8rem'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
</div>
|
||||
<div *ngIf="!isLoading" class="flex-grow-1 text-truncate">
|
||||
<div class="h6 m-0 text-truncate">{{ position?.name }}</div>
|
||||
@ -50,13 +50,13 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="position?.netPerformance"
|
||||
></gf-value>
|
||||
/>
|
||||
<gf-value
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="locale"
|
||||
[value]="position?.netPerformancePercentage"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="align-items-center d-flex">
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="row no-gutters">
|
||||
<div class="col">
|
||||
<ng-container *ngIf="positions === undefined">
|
||||
<gf-position [isLoading]="true"></gf-position>
|
||||
<gf-position [isLoading]="true" />
|
||||
</ng-container>
|
||||
<ng-container *ngIf="positions !== undefined">
|
||||
<ng-container *ngIf="hasPositions">
|
||||
@ -13,7 +13,7 @@
|
||||
[locale]="locale"
|
||||
[position]="position"
|
||||
[range]="range"
|
||||
></gf-position>
|
||||
/>
|
||||
<gf-position
|
||||
*ngFor="let position of positionsRest"
|
||||
[baseCurrency]="baseCurrency"
|
||||
@ -21,15 +21,13 @@
|
||||
[locale]="locale"
|
||||
[position]="position"
|
||||
[range]="range"
|
||||
></gf-position>
|
||||
/>
|
||||
</ng-container>
|
||||
<div
|
||||
*ngIf="hasPermissionToCreateOrder && !hasPositions"
|
||||
class="p-3 text-center"
|
||||
>
|
||||
<gf-no-transactions-info-indicator
|
||||
[hasBorder]="false"
|
||||
></gf-no-transactions-info-indicator>
|
||||
<gf-no-transactions-info-indicator [hasBorder]="false" />
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
@ -8,7 +8,7 @@
|
||||
height: '2rem',
|
||||
width: '2rem'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
*ngIf="!isLoading"
|
||||
@ -26,14 +26,14 @@
|
||||
height: '1rem',
|
||||
width: '10rem'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
<ngx-skeleton-loader
|
||||
animation="pulse"
|
||||
[theme]="{
|
||||
height: '1rem',
|
||||
width: '15rem'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
</div>
|
||||
<div *ngIf="!isLoading" class="flex-grow-1">
|
||||
<div class="h6 my-1">{{ rule?.name }}</div>
|
||||
|
@ -7,15 +7,13 @@
|
||||
class="my-2 text-center"
|
||||
>
|
||||
<mat-card-content>
|
||||
<gf-no-transactions-info-indicator
|
||||
[hasBorder]="false"
|
||||
></gf-no-transactions-info-indicator
|
||||
></mat-card-content>
|
||||
<gf-no-transactions-info-indicator [hasBorder]="false" />
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<gf-rule *ngIf="rules?.length === 0" [isLoading]="true"></gf-rule>
|
||||
<gf-rule *ngIf="rules?.length === 0" [isLoading]="true" />
|
||||
<ng-container *ngIf="rules !== null && rules !== undefined">
|
||||
<gf-rule *ngFor="let rule of rules" [rule]="rule"></gf-rule>
|
||||
<gf-rule *ngFor="let rule of rules" [rule]="rule" />
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -7,10 +7,7 @@
|
||||
<div>
|
||||
<h5 class="align-items-center d-flex justify-content-center mb-3">
|
||||
<span>Ghostfolio Premium</span>
|
||||
<gf-premium-indicator
|
||||
class="ml-1"
|
||||
[enableLink]="false"
|
||||
></gf-premium-indicator>
|
||||
<gf-premium-indicator class="ml-1" [enableLink]="false" />
|
||||
</h5>
|
||||
<div class="font-weight-normal h5 mb-3 text-center" i18n>
|
||||
Are you an ambitious investor who needs the full picture?
|
||||
|
@ -37,19 +37,23 @@ export class CreateOrUpdateAccessDialog implements OnDestroy {
|
||||
ngOnInit() {
|
||||
this.accessForm = this.formBuilder.group({
|
||||
alias: [this.data.access.alias],
|
||||
permissions: [this.data.access.permissions[0], Validators.required],
|
||||
type: [this.data.access.type, Validators.required],
|
||||
userId: [this.data.access.grantee, Validators.required]
|
||||
});
|
||||
|
||||
this.accessForm.get('type').valueChanges.subscribe((value) => {
|
||||
this.accessForm.get('type').valueChanges.subscribe((accessType) => {
|
||||
const permissionsControl = this.accessForm.get('permissions');
|
||||
const userIdControl = this.accessForm.get('userId');
|
||||
|
||||
if (value === 'PRIVATE') {
|
||||
if (accessType === 'PRIVATE') {
|
||||
permissionsControl.setValidators(Validators.required);
|
||||
userIdControl.setValidators(Validators.required);
|
||||
} else {
|
||||
userIdControl.clearValidators();
|
||||
}
|
||||
|
||||
permissionsControl.updateValueAndValidity();
|
||||
userIdControl.updateValueAndValidity();
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
@ -64,7 +68,7 @@ export class CreateOrUpdateAccessDialog implements OnDestroy {
|
||||
const access: CreateAccessDto = {
|
||||
alias: this.accessForm.controls['alias'].value,
|
||||
granteeUserId: this.accessForm.controls['userId'].value,
|
||||
type: this.accessForm.controls['type'].value
|
||||
permissions: [this.accessForm.controls['permissions'].value]
|
||||
};
|
||||
|
||||
this.dataService
|
||||
|
@ -30,9 +30,20 @@
|
||||
@if (accessForm.controls['type'].value === 'PRIVATE') {
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label
|
||||
>Ghostfolio <ng-container i18n>User ID</ng-container></mat-label
|
||||
>
|
||||
<mat-label i18n>Permission</mat-label>
|
||||
<mat-select formControlName="permissions">
|
||||
<mat-option i18n value="READ_RESTRICTED">Restricted view</mat-option>
|
||||
@if(data?.user?.settings?.isExperimentalFeatures) {
|
||||
<mat-option i18n value="READ">View</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label>
|
||||
Ghostfolio <ng-container i18n>User ID</ng-container>
|
||||
</mat-label>
|
||||
<input
|
||||
formControlName="userId"
|
||||
matInput
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Access } from '@ghostfolio/common/interfaces';
|
||||
import { Access, User } from '@ghostfolio/common/interfaces';
|
||||
|
||||
export interface CreateOrUpdateAccessDialogParams {
|
||||
access: Access;
|
||||
user: User;
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import {
|
||||
} from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { Access, User } from '@ghostfolio/common/interfaces';
|
||||
@ -105,8 +104,10 @@ export class UserAccountAccessComponent implements OnDestroy, OnInit {
|
||||
data: {
|
||||
access: {
|
||||
alias: '',
|
||||
permissions: ['READ_RESTRICTED'],
|
||||
type: 'PRIVATE'
|
||||
}
|
||||
},
|
||||
user: this.user
|
||||
},
|
||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||
|
@ -6,13 +6,13 @@
|
||||
<gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator>
|
||||
/>
|
||||
</h1>
|
||||
<gf-access-table
|
||||
[accesses]="accesses"
|
||||
[showActions]="hasPermissionToDeleteAccess"
|
||||
(accessDeleted)="onDeleteAccess($event)"
|
||||
></gf-access-table>
|
||||
/>
|
||||
<div *ngIf="hasPermissionToCreateAccess" class="fab-container">
|
||||
<a
|
||||
class="align-items-center d-flex justify-content-center"
|
||||
|
@ -5,7 +5,7 @@
|
||||
<gf-membership-card
|
||||
[expiresAt]="user?.subscription?.expiresAt | date: defaultDateFormat"
|
||||
[name]="user?.subscription?.type"
|
||||
></gf-membership-card>
|
||||
/>
|
||||
<div
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="d-flex flex-column mt-5"
|
||||
@ -15,10 +15,10 @@
|
||||
>
|
||||
<button color="primary" mat-flat-button (click)="onCheckout()">
|
||||
<ng-container *ngIf="user.subscription.offer === 'default'" i18n
|
||||
>Upgrade</ng-container
|
||||
>Upgrade Plan</ng-container
|
||||
>
|
||||
<ng-container *ngIf="user.subscription.offer === 'renewal'" i18n
|
||||
>Renew</ng-container
|
||||
>Renew Plan</ng-container
|
||||
>
|
||||
</button>
|
||||
<div *ngIf="price" class="mt-1 text-center">
|
||||
@ -43,8 +43,8 @@
|
||||
<gf-premium-indicator
|
||||
class="d-inline-block ml-1"
|
||||
[enableLink]="false"
|
||||
></gf-premium-indicator
|
||||
></a>
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
*ngIf="hasPermissionToUpdateUserSettings"
|
||||
class="mx-1"
|
||||
|
@ -5,6 +5,6 @@
|
||||
[theme]="{
|
||||
width: '100%'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
|
||||
<div class="align-items-center d-flex h-100 w-100" id="svgMap"></div>
|
||||
|
@ -15,7 +15,7 @@
|
||||
(accountDeleted)="onDeleteAccount($event)"
|
||||
(accountToUpdate)="onUpdateAccount($event)"
|
||||
(transferBalance)="onTransferBalance()"
|
||||
></gf-accounts-table>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -61,7 +61,7 @@
|
||||
class="mr-1"
|
||||
[tooltip]="platformEntry.name"
|
||||
[url]="platformEntry.url"
|
||||
></gf-symbol-icon>
|
||||
/>
|
||||
<span>{{ platformEntry.name }}</span>
|
||||
</span>
|
||||
</mat-option>
|
||||
|
@ -17,8 +17,7 @@
|
||||
class="mr-1"
|
||||
[tooltip]="account.Platform?.name"
|
||||
[url]="account.Platform?.url"
|
||||
></gf-symbol-icon
|
||||
><span>{{ account.name }}</span>
|
||||
/><span>{{ account.name }}</span>
|
||||
</div>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
@ -35,8 +34,7 @@
|
||||
class="mr-1"
|
||||
[tooltip]="account.Platform?.name"
|
||||
[url]="account.Platform?.url"
|
||||
></gf-symbol-icon
|
||||
><span>{{ account.name }}</span>
|
||||
/><span>{{ account.name }}</span>
|
||||
</div>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
|
@ -20,8 +20,8 @@
|
||||
<gf-premium-indicator
|
||||
class="d-inline-block ml-1"
|
||||
[enableLink]="false"
|
||||
></gf-premium-indicator
|
||||
></span>
|
||||
/>
|
||||
</span>
|
||||
annual plan for ambitious investors who need the full picture of
|
||||
their financial assets.
|
||||
</p>
|
||||
|
@ -21,8 +21,8 @@
|
||||
<gf-premium-indicator
|
||||
class="d-inline-block ml-1"
|
||||
[enableLink]="false"
|
||||
></gf-premium-indicator
|
||||
></span>
|
||||
/>
|
||||
</span>
|
||||
annual plan with our exclusive Black Week deal. Elevate your
|
||||
financial strategy with the power of Ghostfolio designed to give you
|
||||
the full picture of your assets.
|
||||
|
@ -50,11 +50,8 @@
|
||||
You can sign up via the “<a [routerLink]="routerLinkRegister"
|
||||
>Get Started</a
|
||||
>” button at the top of the page. You have multiple options to join
|
||||
Ghostfolio: Create an account with a security token, using
|
||||
<a href="../en/blog/2022/07/ghostfolio-meets-internet-identity"
|
||||
>Internet Identity</a
|
||||
>
|
||||
or <i>Google Sign</i>. We will guide you to set up your portfolio.
|
||||
Ghostfolio: Create an account with a security token or
|
||||
<i>Google Sign</i>. We will guide you to set up your portfolio.
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
@ -75,11 +72,9 @@
|
||||
></mat-card-header
|
||||
>
|
||||
<mat-card-content>
|
||||
Yes, the authentication systems (via security token or
|
||||
<a href="../en/blog/2022/07/ghostfolio-meets-internet-identity"
|
||||
>Internet Identity</a
|
||||
>) enable you to sign in securely and anonymously to Ghostfolio. There
|
||||
is no need for an e-mail address, phone number, or a username.
|
||||
Yes, the authentication system via security token enables you to sign
|
||||
in securely and anonymously to Ghostfolio. There is no need for an
|
||||
e-mail address, phone number, or a username.
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
@ -176,6 +171,19 @@
|
||||
your university e-mail address.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title
|
||||
>Does the Ghostfolio Premium subscription renew
|
||||
automatically?</mat-card-title
|
||||
>
|
||||
</mat-card-header>
|
||||
<mat-card-content
|
||||
>No, <a [routerLink]="routerLinkPricing">Ghostfolio Premium</a> does
|
||||
not include auto-renewal. Upon expiration, you can choose whether to
|
||||
start a new subscription.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Which devices are supported?</mat-card-title>
|
||||
|
@ -142,7 +142,7 @@
|
||||
<gf-premium-indicator
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator>
|
||||
/>
|
||||
</h4>
|
||||
<p class="m-0">
|
||||
Check the rate of return of your portfolio for
|
||||
@ -162,7 +162,7 @@
|
||||
<gf-premium-indicator
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator>
|
||||
/>
|
||||
</h4>
|
||||
<p class="m-0">
|
||||
Check the allocations of your portfolio by account, asset
|
||||
@ -207,7 +207,7 @@
|
||||
<div class="flex-grow-1">
|
||||
<h4 class="align-items-center d-flex">
|
||||
<span i18n>Market Mood</span>
|
||||
<gf-premium-indicator class="ml-1"></gf-premium-indicator>
|
||||
<gf-premium-indicator class="ml-1" />
|
||||
</h4>
|
||||
<p class="m-0">
|
||||
Check the current market mood (<a
|
||||
@ -228,7 +228,7 @@
|
||||
<gf-premium-indicator
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator>
|
||||
/>
|
||||
</h4>
|
||||
<p class="m-0">
|
||||
Identify potential risks in your portfolio with Ghostfolio
|
||||
|
@ -328,11 +328,7 @@
|
||||
<gf-carousel [aria-label]="'Testimonials'">
|
||||
<div *ngFor="let testimonial of testimonials" gf-carousel-item>
|
||||
<div class="d-flex px-4">
|
||||
<gf-logo
|
||||
class="mr-3 mt-2 pt-1"
|
||||
size="medium"
|
||||
[showLabel]="false"
|
||||
></gf-logo>
|
||||
<gf-logo class="mr-3 mt-2 pt-1" size="medium" [showLabel]="false" />
|
||||
<div>
|
||||
<div>{{ testimonial.quote }}</div>
|
||||
<div class="mt-2 text-muted">
|
||||
@ -361,10 +357,7 @@
|
||||
</h2>
|
||||
</div>
|
||||
<div class="col-md-8 customer-map-container offset-md-2">
|
||||
<gf-world-map-chart
|
||||
format="👻"
|
||||
[countries]="countriesOfSubscribersMap"
|
||||
></gf-world-map-chart>
|
||||
<gf-world-map-chart format="👻" [countries]="countriesOfSubscribersMap" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -450,7 +443,7 @@
|
||||
<div
|
||||
class="align-items-center d-flex flex-column justify-content-center w-100"
|
||||
>
|
||||
<gf-logo size="medium"></gf-logo>
|
||||
<gf-logo size="medium" />
|
||||
<div>Wealth Management Software</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<gf-home-market></gf-home-market>
|
||||
<gf-home-market />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -15,7 +15,7 @@ import { ImpersonationStorageService } from '@ghostfolio/client/services/imperso
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
|
||||
import { downloadAsFile } from '@ghostfolio/common/helper';
|
||||
import { Filter, User } from '@ghostfolio/common/interfaces';
|
||||
import { User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { DataSource, Order as OrderModel } from '@prisma/client';
|
||||
import { format, parseISO } from 'date-fns';
|
||||
@ -199,8 +199,14 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
public onExport(activityIds?: string[]) {
|
||||
let fetchExportParams: any = { activityIds };
|
||||
|
||||
if (!activityIds) {
|
||||
fetchExportParams = { filters: this.userService.getFilters() };
|
||||
}
|
||||
|
||||
this.dataService
|
||||
.fetchExport(activityIds)
|
||||
.fetchExport(fetchExportParams)
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((data) => {
|
||||
for (const activity of data.activities) {
|
||||
@ -220,7 +226,7 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
||||
|
||||
public onExportDrafts(activityIds?: string[]) {
|
||||
this.dataService
|
||||
.fetchExport(activityIds)
|
||||
.fetchExport({ activityIds })
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((data) => {
|
||||
downloadAsFile({
|
||||
|
@ -20,13 +20,13 @@
|
||||
(activityToClone)="onCloneActivity($event)"
|
||||
(activityToUpdate)="onUpdateActivity($event)"
|
||||
(deleteAllActivities)="onDeleteAllActivities()"
|
||||
(export)="onExport($event)"
|
||||
(export)="onExport()"
|
||||
(exportDrafts)="onExportDrafts($event)"
|
||||
(import)="onImport()"
|
||||
(importDividends)="onImportDividends()"
|
||||
(pageChanged)="onChangePage($event)"
|
||||
(sortChanged)="onSortChanged($event)"
|
||||
></gf-activities-table-lazy>
|
||||
/>
|
||||
<gf-activities-table
|
||||
*ngIf="user?.settings?.isExperimentalFeatures !== true"
|
||||
[activities]="activities"
|
||||
@ -44,7 +44,7 @@
|
||||
(exportDrafts)="onExportDrafts($event)"
|
||||
(import)="onImport()"
|
||||
(importDividends)="onImportDividends()"
|
||||
></gf-activities-table>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -82,8 +82,7 @@
|
||||
class="mr-1"
|
||||
[tooltip]="account.Platform?.name"
|
||||
[url]="account.Platform?.url"
|
||||
></gf-symbol-icon
|
||||
><span>{{ account.name }}</span>
|
||||
/><span>{{ account.name }}</span>
|
||||
</div>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
@ -357,7 +356,7 @@
|
||||
[locale]="data.user?.settings?.locale"
|
||||
[unit]="activityForm.controls['currency']?.value ?? data.user?.settings?.baseCurrency"
|
||||
[value]="total"
|
||||
></gf-value>
|
||||
/>
|
||||
<div>
|
||||
<button i18n mat-button type="button" (click)="onCancel()">Cancel</button>
|
||||
<button
|
||||
|
@ -3,7 +3,7 @@
|
||||
[deviceType]="data.deviceType"
|
||||
[title]="dialogTitle"
|
||||
(closeButtonClicked)="onCancel()"
|
||||
></gf-dialog-header>
|
||||
/>
|
||||
|
||||
<div class="flex-grow-1" mat-dialog-content>
|
||||
<mat-stepper
|
||||
@ -136,7 +136,7 @@
|
||||
[sortDisabled]="true"
|
||||
[totalItems]="totalItems"
|
||||
(selectedActivities)="updateSelection($event)"
|
||||
></gf-activities-table-lazy>
|
||||
/>
|
||||
<gf-activities-table
|
||||
*ngIf="importStep === 1 && data?.user?.settings?.isExperimentalFeatures !== true"
|
||||
[activities]="activities"
|
||||
@ -153,7 +153,7 @@
|
||||
[showFooter]="false"
|
||||
[showSymbolColumn]="false"
|
||||
(selectedActivities)="updateSelection($event)"
|
||||
></gf-activities-table>
|
||||
/>
|
||||
<div class="d-flex justify-content-end mt-3">
|
||||
<button mat-button (click)="onReset(stepper)">
|
||||
<ng-container i18n>Back</ng-container>
|
||||
@ -214,4 +214,4 @@
|
||||
mat-dialog-actions
|
||||
[deviceType]="data.deviceType"
|
||||
(closeButtonClicked)="onCancel()"
|
||||
></gf-dialog-footer>
|
||||
/>
|
||||
|
@ -148,9 +148,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
|
||||
this.initialize();
|
||||
|
||||
return this.dataService.fetchPortfolioDetails({
|
||||
filters: this.activeFilters
|
||||
});
|
||||
return this.fetchPortfolioDetails();
|
||||
}),
|
||||
takeUntil(this.unsubscribeSubject)
|
||||
)
|
||||
@ -159,7 +157,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
|
||||
this.portfolioDetails = portfolioDetails;
|
||||
|
||||
this.initializeAnalysisData();
|
||||
this.initializeAllocationsData();
|
||||
|
||||
this.isLoading = false;
|
||||
|
||||
@ -210,6 +208,26 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
? `{0}%`
|
||||
: `{0} ${this.user?.settings?.baseCurrency}`;
|
||||
|
||||
if (this.user?.settings?.isExperimentalFeatures === true) {
|
||||
this.isLoading = true;
|
||||
|
||||
this.initialize();
|
||||
|
||||
this.fetchPortfolioDetails()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((portfolioDetails) => {
|
||||
this.initialize();
|
||||
|
||||
this.portfolioDetails = portfolioDetails;
|
||||
|
||||
this.initializeAllocationsData();
|
||||
|
||||
this.isLoading = false;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
@ -217,7 +235,52 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
public initialize() {
|
||||
public onAccountChartClicked({ symbol }: UniqueAsset) {
|
||||
if (symbol && symbol !== UNKNOWN_KEY) {
|
||||
this.router.navigate([], {
|
||||
queryParams: { accountId: symbol, accountDetailDialog: true }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public onSymbolChartClicked({ dataSource, symbol }: UniqueAsset) {
|
||||
if (dataSource && symbol) {
|
||||
this.router.navigate([], {
|
||||
queryParams: { dataSource, symbol, positionDetailDialog: true }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
private extractEtfProvider({
|
||||
assetSubClass,
|
||||
name
|
||||
}: {
|
||||
assetSubClass: PortfolioPosition['assetSubClass'];
|
||||
name: string;
|
||||
}) {
|
||||
if (assetSubClass === 'ETF') {
|
||||
const [firstWord] = name.split(' ');
|
||||
return firstWord;
|
||||
}
|
||||
|
||||
return UNKNOWN_KEY;
|
||||
}
|
||||
|
||||
private fetchPortfolioDetails() {
|
||||
return this.dataService.fetchPortfolioDetails({
|
||||
filters:
|
||||
this.activeFilters.length > 0
|
||||
? this.activeFilters
|
||||
: this.userService.getFilters()
|
||||
});
|
||||
}
|
||||
|
||||
private initialize() {
|
||||
this.accounts = {};
|
||||
this.continents = {
|
||||
[UNKNOWN_KEY]: {
|
||||
@ -310,7 +373,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
};
|
||||
}
|
||||
|
||||
public initializeAnalysisData() {
|
||||
private initializeAllocationsData() {
|
||||
for (const [
|
||||
id,
|
||||
{ name, valueInBaseCurrency, valueInPercentage }
|
||||
@ -540,27 +603,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
this.markets[UNKNOWN_KEY].value / marketsTotal;
|
||||
}
|
||||
|
||||
public onAccountChartClicked({ symbol }: UniqueAsset) {
|
||||
if (symbol && symbol !== UNKNOWN_KEY) {
|
||||
this.router.navigate([], {
|
||||
queryParams: { accountId: symbol, accountDetailDialog: true }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public onSymbolChartClicked({ dataSource, symbol }: UniqueAsset) {
|
||||
if (dataSource && symbol) {
|
||||
this.router.navigate([], {
|
||||
queryParams: { dataSource, symbol, positionDetailDialog: true }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
private openAccountDetailDialog(aAccountId: string) {
|
||||
const dialogRef = this.dialog.open(AccountDetailDialog, {
|
||||
autoFocus: false,
|
||||
@ -621,19 +663,4 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private extractEtfProvider({
|
||||
assetSubClass,
|
||||
name
|
||||
}: {
|
||||
assetSubClass: PortfolioPosition['assetSubClass'];
|
||||
name: string;
|
||||
}) {
|
||||
if (assetSubClass === 'ETF') {
|
||||
const [firstWord] = name.split(' ');
|
||||
return firstWord;
|
||||
}
|
||||
|
||||
return UNKNOWN_KEY;
|
||||
}
|
||||
}
|
||||
|
@ -2,12 +2,14 @@
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Allocations</h1>
|
||||
@if (!user?.settings?.isExperimentalFeatures) {
|
||||
<gf-activities-filter
|
||||
[allFilters]="allFilters"
|
||||
[isLoading]="isLoading"
|
||||
[placeholder]="placeholder"
|
||||
(valueChanged)="filters$.next($event)"
|
||||
></gf-activities-filter>
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@ -22,7 +24,7 @@
|
||||
size="medium"
|
||||
[isPercent]="true"
|
||||
[value]="isLoading ? undefined : portfolioDetails?.filteredValueInPercentage"
|
||||
></gf-value>
|
||||
/>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<mat-progress-bar
|
||||
@ -50,7 +52,7 @@
|
||||
[keys]="['id']"
|
||||
[locale]="user?.settings?.locale"
|
||||
[positions]="platforms"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
@ -62,8 +64,8 @@
|
||||
<gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator
|
||||
></mat-card-title>
|
||||
/>
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<gf-portfolio-proportion-chart
|
||||
@ -73,7 +75,7 @@
|
||||
[keys]="['currency']"
|
||||
[locale]="user?.settings?.locale"
|
||||
[positions]="positions"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
@ -85,8 +87,8 @@
|
||||
><gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator
|
||||
></mat-card-title>
|
||||
/>
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<gf-portfolio-proportion-chart
|
||||
@ -96,7 +98,7 @@
|
||||
[keys]="['assetClassLabel', 'assetSubClassLabel']"
|
||||
[locale]="user?.settings?.locale"
|
||||
[positions]="positions"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
@ -119,7 +121,7 @@
|
||||
[positions]="symbols"
|
||||
[showLabels]="deviceType !== 'mobile'"
|
||||
(proportionChartClicked)="onSymbolChartClicked($event)"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
@ -131,8 +133,8 @@
|
||||
><gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator
|
||||
></mat-card-title>
|
||||
/>
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<gf-portfolio-proportion-chart
|
||||
@ -143,7 +145,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[maxItems]="10"
|
||||
[positions]="sectors"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
@ -155,8 +157,8 @@
|
||||
><gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator
|
||||
></mat-card-title>
|
||||
/>
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<gf-portfolio-proportion-chart
|
||||
@ -166,7 +168,7 @@
|
||||
[keys]="['name']"
|
||||
[locale]="user?.settings?.locale"
|
||||
[positions]="continents"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
@ -178,8 +180,8 @@
|
||||
><gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator
|
||||
></mat-card-title>
|
||||
/>
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<gf-portfolio-proportion-chart
|
||||
@ -188,7 +190,7 @@
|
||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||
[locale]="user?.settings?.locale"
|
||||
[positions]="marketsAdvanced"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
@ -202,8 +204,8 @@
|
||||
><gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator
|
||||
></mat-card-title>
|
||||
/>
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div class="world-map-chart-container">
|
||||
@ -212,7 +214,7 @@
|
||||
[format]="worldMapChartFormat"
|
||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||
[locale]="user?.settings?.locale"
|
||||
></gf-world-map-chart>
|
||||
/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md my-2">
|
||||
@ -275,7 +277,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[positions]="accounts"
|
||||
(proportionChartClicked)="onAccountChartClicked($event)"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
@ -287,8 +289,8 @@
|
||||
><gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator
|
||||
></mat-card-title>
|
||||
/>
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<gf-portfolio-proportion-chart
|
||||
@ -298,7 +300,7 @@
|
||||
[keys]="['etfProvider']"
|
||||
[locale]="user?.settings?.locale"
|
||||
[positions]="positions"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
@ -310,8 +312,8 @@
|
||||
><gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator
|
||||
></mat-card-title>
|
||||
/>
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<gf-portfolio-proportion-chart
|
||||
@ -322,7 +324,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[maxItems]="10"
|
||||
[positions]="countries"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
@ -379,7 +379,9 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ positions }) => {
|
||||
const positionsSorted = sortBy(
|
||||
positions,
|
||||
positions.filter(({ netPerformancePercentage }) => {
|
||||
return isNumber(netPerformancePercentage);
|
||||
}),
|
||||
'netPerformancePercentage'
|
||||
).reverse();
|
||||
|
||||
|
@ -7,14 +7,14 @@
|
||||
[isLoading]="isLoadingBenchmarkComparator || isLoadingInvestmentChart"
|
||||
[options]="dateRangeOptions"
|
||||
(change)="onChangeDateRange($event.value)"
|
||||
></gf-toggle>
|
||||
/>
|
||||
</div>
|
||||
<gf-activities-filter
|
||||
[allFilters]="allFilters"
|
||||
[isLoading]="isLoadingBenchmarkComparator || isLoadingInvestmentChart"
|
||||
[placeholder]="placeholder"
|
||||
(valueChanged)="filters$.next($event)"
|
||||
></gf-activities-filter>
|
||||
/>
|
||||
}
|
||||
<div class="mb-5 row">
|
||||
<div class="col-lg">
|
||||
@ -30,7 +30,7 @@
|
||||
[performanceDataItems]="performanceDataItemsInPercentage"
|
||||
[user]="user"
|
||||
(benchmarkChanged)="onChangeBenchmark($event)"
|
||||
></gf-benchmark-comparator>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -51,7 +51,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[unit]="user?.settings?.baseCurrency"
|
||||
[value]="isLoadingInvestmentChart ? undefined : performance?.currentNetPerformance"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex mb-3 ml-3 py-1">
|
||||
@ -66,7 +66,7 @@
|
||||
[isPercent]="true"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="isLoadingInvestmentChart ? undefined : performance?.currentNetPerformancePercent"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex py-1">
|
||||
@ -81,7 +81,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[unit]="user?.settings?.baseCurrency"
|
||||
[value]="isLoadingInvestmentChart ? undefined : (performance?.currentNetPerformanceWithCurrencyEffect === null ? null : performance?.currentNetPerformanceWithCurrencyEffect - performance?.currentNetPerformance)"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex ml-3 py-1">
|
||||
@ -96,7 +96,7 @@
|
||||
[isPercent]="true"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="isLoadingInvestmentChart ? undefined : performance?.currentNetPerformancePercentWithCurrencyEffect - performance?.currentNetPerformancePercent"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div><hr /></div>
|
||||
@ -112,7 +112,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[unit]="user?.settings?.baseCurrency"
|
||||
[value]="isLoadingInvestmentChart ? undefined : performance?.currentNetPerformanceWithCurrencyEffect"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex ml-3 py-1">
|
||||
@ -127,7 +127,7 @@
|
||||
[isPercent]="true"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="isLoadingInvestmentChart ? undefined : performance?.currentNetPerformancePercentWithCurrencyEffect"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
@ -167,7 +167,7 @@
|
||||
[isPercent]="true"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="position.netPerformancePercentage"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
@ -180,7 +180,7 @@
|
||||
height: '1.5rem',
|
||||
width: '100%'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
@ -215,7 +215,7 @@
|
||||
[isPercent]="true"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="position.netPerformancePercentage"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
@ -228,7 +228,7 @@
|
||||
height: '1.5rem',
|
||||
width: '100%'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
@ -245,7 +245,7 @@
|
||||
<gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
@ -260,7 +260,7 @@
|
||||
[isLoading]="isLoadingInvestmentChart"
|
||||
[locale]="user?.settings?.locale"
|
||||
[range]="user?.settings?.dateRange"
|
||||
></gf-investment-chart>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -275,7 +275,7 @@
|
||||
<gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator>
|
||||
/>
|
||||
</div>
|
||||
<gf-toggle
|
||||
class="d-none d-lg-block"
|
||||
@ -283,7 +283,7 @@
|
||||
[isLoading]="false"
|
||||
[options]="modeOptions"
|
||||
(change)="onChangeGroupBy($event.value)"
|
||||
></gf-toggle>
|
||||
/>
|
||||
</div>
|
||||
<div *ngIf="streaks" class="row">
|
||||
<div class="col-md-6 col-xs-12 my-2">
|
||||
@ -317,7 +317,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[range]="user?.settings?.dateRange"
|
||||
[savingsRate]="savingsRate"
|
||||
></gf-investment-chart>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -332,7 +332,7 @@
|
||||
<gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator>
|
||||
/>
|
||||
</div>
|
||||
<gf-toggle
|
||||
class="d-none d-lg-block"
|
||||
@ -340,7 +340,7 @@
|
||||
[isLoading]="false"
|
||||
[options]="modeOptions"
|
||||
(change)="onChangeGroupBy($event.value)"
|
||||
></gf-toggle>
|
||||
/>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<gf-investment-chart
|
||||
@ -353,7 +353,7 @@
|
||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||
[locale]="user?.settings?.locale"
|
||||
[range]="user?.settings?.dateRange"
|
||||
></gf-investment-chart>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -8,7 +8,7 @@
|
||||
><gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator>
|
||||
/>
|
||||
</h4>
|
||||
<gf-fire-calculator
|
||||
[annualInterestRate]="user?.settings?.annualInterestRate"
|
||||
@ -25,7 +25,7 @@
|
||||
(projectedTotalAmountChanged)="onProjectedTotalAmountChange($event)"
|
||||
(retirementDateChanged)="onRetirementDateChange($event)"
|
||||
(savingsRateChanged)="onSavingsRateChange($event)"
|
||||
></gf-fire-calculator>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -35,7 +35,7 @@
|
||||
><gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator>
|
||||
/>
|
||||
</h4>
|
||||
<div *ngIf="isLoading">
|
||||
<ngx-skeleton-loader
|
||||
@ -45,14 +45,14 @@
|
||||
height: '1rem',
|
||||
width: '100%'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
<ngx-skeleton-loader
|
||||
animation="pulse"
|
||||
[theme]="{
|
||||
height: '1rem',
|
||||
width: '10rem'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
</div>
|
||||
<div *ngIf="!isLoading" i18n>
|
||||
If you retire today, you would be able to withdraw
|
||||
@ -63,7 +63,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[unit]="user?.settings?.baseCurrency"
|
||||
[value]="withdrawalRatePerYear?.toNumber()"
|
||||
></gf-value>
|
||||
/>
|
||||
per year</span
|
||||
>
|
||||
or
|
||||
@ -74,7 +74,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[unit]="user?.settings?.baseCurrency"
|
||||
[value]="withdrawalRatePerMonth?.toNumber()"
|
||||
></gf-value>
|
||||
/>
|
||||
per month</span
|
||||
>, based on your total assets of
|
||||
<span class="font-weight-bold"
|
||||
@ -84,8 +84,8 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[unit]="user?.settings?.baseCurrency"
|
||||
[value]="fireWealth?.toNumber()"
|
||||
></gf-value
|
||||
></span>
|
||||
/>
|
||||
</span>
|
||||
and a withdrawal rate of 4%.
|
||||
</div>
|
||||
</div>
|
||||
@ -112,12 +112,12 @@
|
||||
><gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator>
|
||||
/>
|
||||
</h4>
|
||||
<gf-rules
|
||||
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
||||
[rules]="emergencyFundRules"
|
||||
></gf-rules>
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<h4 class="align-items-center d-flex m-0">
|
||||
@ -125,12 +125,12 @@
|
||||
><gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator>
|
||||
/>
|
||||
</h4>
|
||||
<gf-rules
|
||||
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
||||
[rules]="currencyClusterRiskRules"
|
||||
></gf-rules>
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<h4 class="align-items-center d-flex m-0">
|
||||
@ -138,12 +138,12 @@
|
||||
><gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator>
|
||||
/>
|
||||
</h4>
|
||||
<gf-rules
|
||||
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
||||
[rules]="accountClusterRiskRules"
|
||||
></gf-rules>
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="align-items-center d-flex m-0">
|
||||
@ -151,12 +151,12 @@
|
||||
><gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator>
|
||||
/>
|
||||
</h4>
|
||||
<gf-rules
|
||||
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
||||
[rules]="feeRules"
|
||||
></gf-rules>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -86,16 +86,14 @@ export class HoldingsPageComponent implements OnDestroy, OnInit {
|
||||
? $localize`Filter by account or tag...`
|
||||
: '';
|
||||
|
||||
return this.dataService.fetchPortfolioDetails({
|
||||
filters: this.activeFilters
|
||||
});
|
||||
return this.fetchPortfolioDetails();
|
||||
}),
|
||||
takeUntil(this.unsubscribeSubject)
|
||||
)
|
||||
.subscribe((portfolioDetails) => {
|
||||
this.portfolioDetails = portfolioDetails;
|
||||
|
||||
this.initializeAnalysisData();
|
||||
this.initialize();
|
||||
|
||||
this.isLoading = false;
|
||||
|
||||
@ -146,17 +144,41 @@ export class HoldingsPageComponent implements OnDestroy, OnInit {
|
||||
...tagFilters
|
||||
];
|
||||
|
||||
if (this.user?.settings?.isExperimentalFeatures === true) {
|
||||
this.holdings = undefined;
|
||||
|
||||
this.fetchPortfolioDetails()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((portfolioDetails) => {
|
||||
this.portfolioDetails = portfolioDetails;
|
||||
|
||||
this.initialize();
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public initialize() {
|
||||
this.holdings = [];
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
public initializeAnalysisData() {
|
||||
this.initialize();
|
||||
private fetchPortfolioDetails() {
|
||||
return this.dataService.fetchPortfolioDetails({
|
||||
filters:
|
||||
this.activeFilters.length > 0
|
||||
? this.activeFilters
|
||||
: this.userService.getFilters()
|
||||
});
|
||||
}
|
||||
|
||||
private initialize() {
|
||||
this.holdings = [];
|
||||
|
||||
for (const [symbol, holding] of Object.entries(
|
||||
this.portfolioDetails.holdings
|
||||
@ -165,11 +187,6 @@ export class HoldingsPageComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
private openPositionDialog({
|
||||
dataSource,
|
||||
symbol
|
||||
|
@ -2,12 +2,14 @@
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Holdings</h1>
|
||||
@if (!user?.settings?.isExperimentalFeatures) {
|
||||
<gf-activities-filter
|
||||
[allFilters]="allFilters"
|
||||
[isLoading]="isLoading"
|
||||
[placeholder]="placeholder"
|
||||
(valueChanged)="filters$.next($event)"
|
||||
></gf-activities-filter>
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@ -18,7 +20,7 @@
|
||||
[hasPermissionToCreateActivity]="hasPermissionToCreateOrder"
|
||||
[holdings]="holdings"
|
||||
[locale]="user?.settings?.locale"
|
||||
></gf-holdings-table>
|
||||
/>
|
||||
<div
|
||||
*ngIf="hasPermissionToCreateOrder && holdings?.length > 0"
|
||||
class="text-center"
|
||||
|
@ -171,10 +171,7 @@
|
||||
<div class="align-items-center d-flex mb-2">
|
||||
<h4 class="align-items-center d-flex flex-grow-1 m-0">
|
||||
<span>Premium</span>
|
||||
<gf-premium-indicator
|
||||
class="ml-1"
|
||||
[enableLink]="false"
|
||||
></gf-premium-indicator>
|
||||
<gf-premium-indicator class="ml-1" [enableLink]="false" />
|
||||
</h4>
|
||||
<div *ngIf="user?.subscription?.type === 'Premium'">
|
||||
<ion-icon class="mr-1" name="checkmark-outline" />
|
||||
|
@ -20,7 +20,7 @@
|
||||
[keys]="['symbol']"
|
||||
[positions]="symbols"
|
||||
[showLabels]="deviceType !== 'mobile'"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
@ -35,7 +35,7 @@
|
||||
[keys]="['currency']"
|
||||
[maxItems]="10"
|
||||
[positions]="positions"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
@ -50,7 +50,7 @@
|
||||
[keys]="['name']"
|
||||
[maxItems]="10"
|
||||
[positions]="sectors"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
@ -64,7 +64,7 @@
|
||||
[isInPercent]="true"
|
||||
[keys]="['name']"
|
||||
[positions]="continents"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
@ -81,7 +81,7 @@
|
||||
format="{0}%"
|
||||
[countries]="countries"
|
||||
[isInPercent]="true"
|
||||
></gf-world-map-chart>
|
||||
/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md my-2">
|
||||
@ -135,7 +135,7 @@
|
||||
[hasPermissionToShowValues]="false"
|
||||
[holdings]="holdings"
|
||||
[pageSize]="7"
|
||||
></gf-holdings-table>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row my-5">
|
||||
|
@ -9,7 +9,7 @@
|
||||
<div
|
||||
class="align-items-center d-flex flex-column justify-content-center w-100"
|
||||
>
|
||||
<gf-logo size="large"></gf-logo>
|
||||
<gf-logo size="large" />
|
||||
<p class="lead m-0">Wealth Management Software</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -44,6 +44,7 @@ import { SumioPageComponent } from './products/sumio-page.component';
|
||||
import { TillerPageComponent } from './products/tiller-page.component';
|
||||
import { UtlunaPageComponent } from './products/utluna-page.component';
|
||||
import { VyzerPageComponent } from './products/vyzer-page.component';
|
||||
import { WealthfolioPageComponent } from './products/wealthfolio-page.component';
|
||||
import { WealthicaPageComponent } from './products/wealthica-page.component';
|
||||
import { WhalPageComponent } from './products/whal-page.component';
|
||||
import { YeekateePageComponent } from './products/yeekatee-page.component';
|
||||
@ -528,6 +529,14 @@ export const products: Product[] = [
|
||||
pricingPerYear: '$348',
|
||||
slogan: 'Virtual Family Office for Smart Wealth Management'
|
||||
},
|
||||
{
|
||||
component: WealthfolioPageComponent,
|
||||
hasSelfHostingAbility: true,
|
||||
key: 'wealthfolio',
|
||||
languages: ['English'],
|
||||
name: 'Wealthfolio',
|
||||
slogan: 'Desktop Investment Tracker'
|
||||
},
|
||||
{
|
||||
component: WealthicaPageComponent,
|
||||
founded: 2015,
|
||||
|
@ -0,0 +1,32 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { products } from '../products';
|
||||
import { BaseProductPageComponent } from './base-page.component';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
imports: [CommonModule, MatButtonModule, RouterModule],
|
||||
selector: 'gf-wealthfolio-page',
|
||||
standalone: true,
|
||||
styleUrls: ['../product-page-template.scss'],
|
||||
templateUrl: '../product-page-template.html'
|
||||
})
|
||||
export class WealthfolioPageComponent extends BaseProductPageComponent {
|
||||
public product1 = products.find(({ key }) => {
|
||||
return key === 'ghostfolio';
|
||||
});
|
||||
|
||||
public product2 = products.find(({ key }) => {
|
||||
return key === 'wealthfolio';
|
||||
});
|
||||
|
||||
public routerLinkAbout = ['/' + $localize`about`];
|
||||
public routerLinkFeatures = ['/' + $localize`features`];
|
||||
public routerLinkResourcesPersonalFinanceTools = [
|
||||
'/' + $localize`resources`,
|
||||
'personal-finance-tools'
|
||||
];
|
||||
}
|
@ -4,7 +4,7 @@
|
||||
<div
|
||||
class="align-items-center d-flex flex-column justify-content-center mb-4 w-100"
|
||||
>
|
||||
<gf-logo size="medium"></gf-logo>
|
||||
<gf-logo size="medium" />
|
||||
</div>
|
||||
|
||||
<div *ngIf="!hasError" class="col d-flex justify-content-center">
|
||||
|
@ -279,8 +279,14 @@ export class DataService {
|
||||
return this.http.get<BenchmarkResponse>('/api/v1/benchmark');
|
||||
}
|
||||
|
||||
public fetchExport(activityIds?: string[]) {
|
||||
let params = new HttpParams();
|
||||
public fetchExport({
|
||||
activityIds,
|
||||
filters
|
||||
}: {
|
||||
activityIds?: string[];
|
||||
filters?: Filter[];
|
||||
} = {}) {
|
||||
let params = this.buildFiltersAsQueryParams({ filters });
|
||||
|
||||
if (activityIds) {
|
||||
params = params.append('activityIds', activityIds.join(','));
|
||||
|
@ -47,18 +47,26 @@ export class UserService extends ObservableStore<UserStoreState> {
|
||||
}
|
||||
|
||||
public getFilters() {
|
||||
const filters: Filter[] = [];
|
||||
const user = this.getState().user;
|
||||
|
||||
return user?.settings?.isExperimentalFeatures === true
|
||||
? user.settings['filters.tags']
|
||||
? <Filter[]>[
|
||||
{
|
||||
id: user.settings['filters.tags'][0],
|
||||
type: 'TAG'
|
||||
}
|
||||
]
|
||||
: []
|
||||
: [];
|
||||
if (user?.settings?.isExperimentalFeatures === true) {
|
||||
if (user.settings['filters.accounts']) {
|
||||
filters.push({
|
||||
id: user.settings['filters.accounts'][0],
|
||||
type: 'ACCOUNT'
|
||||
});
|
||||
}
|
||||
|
||||
if (user.settings['filters.tags']) {
|
||||
filters.push({
|
||||
id: user.settings['filters.tags'][0],
|
||||
type: 'TAG'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
public remove() {
|
||||
|
@ -1,6 +1,16 @@
|
||||
{
|
||||
"createdAt": "2023-12-30T00:00:00.000Z",
|
||||
"createdAt": "2024-01-29T00:00:00.000Z",
|
||||
"data": [
|
||||
{
|
||||
"name": "Aptabase",
|
||||
"description": "Analytics for Apps, open source, simple and privacy-friendly. SDKs for Swift, React Native, Electron, Flutter and many others.",
|
||||
"href": "https://aptabase.com"
|
||||
},
|
||||
{
|
||||
"name": "Argos",
|
||||
"description": "Argos provides the developer tools to debug tests and detect visual regressions..",
|
||||
"href": "https://argos-ci.com"
|
||||
},
|
||||
{
|
||||
"name": "BoxyHQ",
|
||||
"description": "BoxyHQ’s suite of APIs for security and privacy helps engineering teams build and ship compliant cloud applications faster.",
|
||||
@ -81,6 +91,16 @@
|
||||
"description": "Open source, end-to-end encrypted platform that lets you securely manage secrets and configs across your team, devices, and infrastructure.",
|
||||
"href": "https://infisical.com"
|
||||
},
|
||||
{
|
||||
"name": "Langfuse",
|
||||
"description": "Open source LLM engineering platform. Debug, analyze and iterate together.",
|
||||
"href": "https://langfuse.com"
|
||||
},
|
||||
{
|
||||
"name": "Lost Pixel",
|
||||
"description": "Open source visual regression testing alternative to Percy & Chromatic",
|
||||
"href": "https://lost-pixel.com"
|
||||
},
|
||||
{
|
||||
"name": "Mockoon",
|
||||
"description": "Mockoon is the easiest and quickest way to design and run mock REST APIs.",
|
||||
|
@ -48,7 +48,7 @@
|
||||
<link href="../assets/site.webmanifest" rel="manifest" />
|
||||
</head>
|
||||
<body>
|
||||
<gf-root></gf-root>
|
||||
<gf-root />
|
||||
|
||||
<script src="../ionicons/ionicons.esm.js" type="module"></script>
|
||||
<script nomodule="" src="ionicons.js"></script>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user