Compare commits
53 Commits
Author | SHA1 | Date | |
---|---|---|---|
90dc34380e | |||
286e41eb21 | |||
4973d0261d | |||
c4a62dfd68 | |||
4d6be0a507 | |||
b259ab7b0c | |||
e1ac5245c7 | |||
d4fea075af | |||
cef7fa79de | |||
ca05397dcd | |||
2a11977001 | |||
fb1a5c93ef | |||
77e9791e03 | |||
efd9e7a5c7 | |||
d9ced885e1 | |||
5fe07cb85f | |||
af008aa74f | |||
ca7bf27c20 | |||
0866587cab | |||
622bb8b0cf | |||
16b9fbe00e | |||
c9353d0a39 | |||
ea101dd3bd | |||
cd67ce82fa | |||
d5b3c52602 | |||
bdf72164b1 | |||
455a2d2e92 | |||
9c0f46b587 | |||
8533606177 | |||
6728e04ff7 | |||
2bf4f1237a | |||
4857b2e620 | |||
68a9a7f6f9 | |||
81ef95e13e | |||
b633132757 | |||
2b0f961370 | |||
30f1a3514a | |||
ed735e0b29 | |||
b89ccd2dde | |||
df6d39377f | |||
d5d14497d6 | |||
09c300661a | |||
92382e0b4d | |||
c25f532487 | |||
5d26d94586 | |||
73b6784e9f | |||
6159f48a62 | |||
7d34fba7c1 | |||
c434b730a8 | |||
2d23c566f1 | |||
ba220eaee9 | |||
09023214ce | |||
1ceabb6e6b |
107
CHANGELOG.md
107
CHANGELOG.md
@ -5,6 +5,113 @@ 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/),
|
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).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## 1.296.0 - 2023-08-01
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Optimized the validation in the activities import by reducing the list to unique asset profiles
|
||||||
|
- Optimized the data gathering in the activities import
|
||||||
|
|
||||||
|
## 1.295.0 - 2023-07-30
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added a step by step introduction for new users
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Removed the _Stay signed in_ setting on _Sign in with fingerprint_ activation
|
||||||
|
|
||||||
|
## 1.294.0 - 2023-07-29
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Extended the allocations by market chart on the allocations page by unavailable data
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Considered liabilities in the total account value calculation
|
||||||
|
|
||||||
|
## 1.293.0 - 2023-07-26
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added error handling for the _Redis_ connections to keep the app running if the connection fails
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Set the `lastmod` dates of `sitemap.xml` dynamically
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the missing values in the holdings table
|
||||||
|
- Fixed the `no such file or directory` error caused by the missing `favicon.ico` file
|
||||||
|
|
||||||
|
## 1.292.0 - 2023-07-24
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Introduced the allocations by market chart on the allocations page
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Upgraded `yahoo-finance2` from version `2.4.2` to `2.4.3`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue in the public page
|
||||||
|
|
||||||
|
## 1.291.0 - 2023-07-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Broken down the emergency fund by cash and assets
|
||||||
|
- Added support for account balance time series
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Renamed queries to presets in the historical market data table of the admin control panel
|
||||||
|
|
||||||
|
## 1.290.0 - 2023-07-16
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added hints to the activity types in the create or edit activity dialog
|
||||||
|
- Added queries to the historical market data table of the admin control panel
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the usability of the login dialog
|
||||||
|
- Disabled the caching in the health check endpoints for data providers
|
||||||
|
- Improved the content of the Frequently Asked Questions (FAQ) page
|
||||||
|
- Upgraded `prisma` from version `4.15.0` to `4.16.2`
|
||||||
|
|
||||||
|
## 1.289.0 - 2023-07-14
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Upgraded `yahoo-finance2` from version `2.4.1` to `2.4.2`
|
||||||
|
|
||||||
|
## 1.288.0 - 2023-07-12
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the loading state during filtering on the allocations page
|
||||||
|
- Beautified the names with ampersand (`&`) in the asset profile
|
||||||
|
- Improved the language localization for German (`de`)
|
||||||
|
|
||||||
|
## 1.287.0 - 2023-07-09
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Hid the average buy price in the position detail chart if there is no holding
|
||||||
|
- Improved the language localization for French (`fr`)
|
||||||
|
- Refactored the blog articles to standalone components
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the sorting by currency in the activities table
|
||||||
|
|
||||||
## 1.286.0 - 2023-07-03
|
## 1.286.0 - 2023-07-03
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
@ -263,7 +263,9 @@ Deprecated: `GET http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TO
|
|||||||
|
|
||||||
## Community Projects
|
## Community Projects
|
||||||
|
|
||||||
- [ghostfolio-cli](https://github.com/DerAndereJohannes/ghostfolio-cli): Command-line interface to access your portfolio
|
Discover a variety of community projects for Ghostfolio: https://github.com/topics/ghostfolio
|
||||||
|
|
||||||
|
Are you building your own project? Add the `ghostfolio` topic to your _GitHub_ repository to get listed as well. [Learn more →](https://docs.github.com/en/articles/classifying-your-repository-with-topics)
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module';
|
import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module';
|
||||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
||||||
|
import { AccountBalanceModule } from '@ghostfolio/api/services/account-balance/account-balance.module';
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
||||||
@ -15,6 +16,7 @@ import { AccountService } from './account.service';
|
|||||||
controllers: [AccountController],
|
controllers: [AccountController],
|
||||||
exports: [AccountService],
|
exports: [AccountService],
|
||||||
imports: [
|
imports: [
|
||||||
|
AccountBalanceModule,
|
||||||
ConfigurationModule,
|
ConfigurationModule,
|
||||||
DataProviderModule,
|
DataProviderModule,
|
||||||
ExchangeRateDataModule,
|
ExchangeRateDataModule,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { AccountBalanceService } from '@ghostfolio/api/services/account-balance/account-balance.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||||
import { Filter } from '@ghostfolio/common/interfaces';
|
import { Filter } from '@ghostfolio/common/interfaces';
|
||||||
@ -11,16 +12,21 @@ import { CashDetails } from './interfaces/cash-details.interface';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class AccountService {
|
export class AccountService {
|
||||||
public constructor(
|
public constructor(
|
||||||
|
private readonly accountBalanceService: AccountBalanceService,
|
||||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||||
private readonly prismaService: PrismaService
|
private readonly prismaService: PrismaService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async account(
|
public async account({
|
||||||
accountWhereUniqueInput: Prisma.AccountWhereUniqueInput
|
id_userId
|
||||||
): Promise<Account | null> {
|
}: Prisma.AccountWhereUniqueInput): Promise<Account | null> {
|
||||||
return this.prismaService.account.findUnique({
|
const { id, userId } = id_userId;
|
||||||
where: accountWhereUniqueInput
|
|
||||||
|
const [account] = await this.accounts({
|
||||||
|
where: { id, userId }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return account;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async accountWithOrders(
|
public async accountWithOrders(
|
||||||
@ -50,9 +56,11 @@ export class AccountService {
|
|||||||
Platform?: Platform;
|
Platform?: Platform;
|
||||||
})[]
|
})[]
|
||||||
> {
|
> {
|
||||||
const { include, skip, take, cursor, where, orderBy } = params;
|
const { include = {}, skip, take, cursor, where, orderBy } = params;
|
||||||
|
|
||||||
return this.prismaService.account.findMany({
|
include.balances = { orderBy: { date: 'desc' }, take: 1 };
|
||||||
|
|
||||||
|
const accounts = await this.prismaService.account.findMany({
|
||||||
cursor,
|
cursor,
|
||||||
include,
|
include,
|
||||||
orderBy,
|
orderBy,
|
||||||
@ -60,15 +68,36 @@ export class AccountService {
|
|||||||
take,
|
take,
|
||||||
where
|
where
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return accounts.map((account) => {
|
||||||
|
account = { ...account, balance: account.balances[0]?.value ?? 0 };
|
||||||
|
|
||||||
|
delete account.balances;
|
||||||
|
|
||||||
|
return account;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createAccount(
|
public async createAccount(
|
||||||
data: Prisma.AccountCreateInput,
|
data: Prisma.AccountCreateInput,
|
||||||
aUserId: string
|
aUserId: string
|
||||||
): Promise<Account> {
|
): Promise<Account> {
|
||||||
return this.prismaService.account.create({
|
const account = await this.prismaService.account.create({
|
||||||
data
|
data
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await this.prismaService.accountBalance.create({
|
||||||
|
data: {
|
||||||
|
Account: {
|
||||||
|
connect: {
|
||||||
|
id_userId: { id: account.id, userId: aUserId }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
value: data.balance
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return account;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteAccount(
|
public async deleteAccount(
|
||||||
@ -167,6 +196,18 @@ export class AccountService {
|
|||||||
aUserId: string
|
aUserId: string
|
||||||
): Promise<Account> {
|
): Promise<Account> {
|
||||||
const { data, where } = params;
|
const { data, where } = params;
|
||||||
|
|
||||||
|
await this.prismaService.accountBalance.create({
|
||||||
|
data: {
|
||||||
|
Account: {
|
||||||
|
connect: {
|
||||||
|
id_userId: where.id_userId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
value: <number>data.balance
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return this.prismaService.account.update({
|
return this.prismaService.account.update({
|
||||||
data,
|
data,
|
||||||
where
|
where
|
||||||
@ -202,16 +243,17 @@ export class AccountService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (amountInCurrencyOfAccount) {
|
if (amountInCurrencyOfAccount) {
|
||||||
await this.prismaService.account.update({
|
await this.accountBalanceService.createAccountBalance({
|
||||||
data: {
|
date,
|
||||||
balance: new Big(balance).plus(amountInCurrencyOfAccount).toNumber()
|
Account: {
|
||||||
},
|
connect: {
|
||||||
where: {
|
id_userId: {
|
||||||
id_userId: {
|
userId,
|
||||||
userId,
|
id: accountId
|
||||||
id: accountId
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
value: new Big(balance).plus(amountInCurrencyOfAccount).toNumber()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
GATHER_ASSET_PROFILE_PROCESS,
|
GATHER_ASSET_PROFILE_PROCESS,
|
||||||
GATHER_ASSET_PROFILE_PROCESS_OPTIONS
|
GATHER_ASSET_PROFILE_PROCESS_OPTIONS
|
||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
|
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
|
||||||
import {
|
import {
|
||||||
AdminData,
|
AdminData,
|
||||||
AdminMarketData,
|
AdminMarketData,
|
||||||
@ -15,7 +16,10 @@ import {
|
|||||||
Filter
|
Filter
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
import type {
|
||||||
|
MarketDataPreset,
|
||||||
|
RequestWithUser
|
||||||
|
} from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
@ -113,7 +117,7 @@ export class AdminController {
|
|||||||
name: GATHER_ASSET_PROFILE_PROCESS,
|
name: GATHER_ASSET_PROFILE_PROCESS,
|
||||||
opts: {
|
opts: {
|
||||||
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
|
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
|
||||||
jobId: `${dataSource}-${symbol}`
|
jobId: getAssetProfileIdentifier({ dataSource, symbol })
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@ -149,7 +153,7 @@ export class AdminController {
|
|||||||
name: GATHER_ASSET_PROFILE_PROCESS,
|
name: GATHER_ASSET_PROFILE_PROCESS,
|
||||||
opts: {
|
opts: {
|
||||||
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
|
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
|
||||||
jobId: `${dataSource}-${symbol}`
|
jobId: getAssetProfileIdentifier({ dataSource, symbol })
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@ -182,7 +186,7 @@ export class AdminController {
|
|||||||
name: GATHER_ASSET_PROFILE_PROCESS,
|
name: GATHER_ASSET_PROFILE_PROCESS,
|
||||||
opts: {
|
opts: {
|
||||||
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
|
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
|
||||||
jobId: `${dataSource}-${symbol}`
|
jobId: getAssetProfileIdentifier({ dataSource, symbol })
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -249,6 +253,7 @@ export class AdminController {
|
|||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
public async getMarketData(
|
public async getMarketData(
|
||||||
@Query('assetSubClasses') filterByAssetSubClasses?: string,
|
@Query('assetSubClasses') filterByAssetSubClasses?: string,
|
||||||
|
@Query('presetId') presetId?: MarketDataPreset,
|
||||||
@Query('skip') skip?: number,
|
@Query('skip') skip?: number,
|
||||||
@Query('sortColumn') sortColumn?: string,
|
@Query('sortColumn') sortColumn?: string,
|
||||||
@Query('sortDirection') sortDirection?: Prisma.SortOrder,
|
@Query('sortDirection') sortDirection?: Prisma.SortOrder,
|
||||||
@ -279,6 +284,7 @@ export class AdminController {
|
|||||||
|
|
||||||
return this.adminService.getMarketData({
|
return this.adminService.getMarketData({
|
||||||
filters,
|
filters,
|
||||||
|
presetId,
|
||||||
sortColumn,
|
sortColumn,
|
||||||
sortDirection,
|
sortDirection,
|
||||||
skip: isNaN(skip) ? undefined : skip,
|
skip: isNaN(skip) ? undefined : skip,
|
||||||
|
@ -17,6 +17,7 @@ import {
|
|||||||
Filter,
|
Filter,
|
||||||
UniqueAsset
|
UniqueAsset
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
|
import { MarketDataPreset } from '@ghostfolio/common/types';
|
||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
import { AssetSubClass, Prisma, Property, SymbolProfile } from '@prisma/client';
|
import { AssetSubClass, Prisma, Property, SymbolProfile } from '@prisma/client';
|
||||||
import { differenceInDays } from 'date-fns';
|
import { differenceInDays } from 'date-fns';
|
||||||
@ -103,12 +104,14 @@ export class AdminService {
|
|||||||
|
|
||||||
public async getMarketData({
|
public async getMarketData({
|
||||||
filters,
|
filters,
|
||||||
|
presetId,
|
||||||
sortColumn,
|
sortColumn,
|
||||||
sortDirection,
|
sortDirection,
|
||||||
skip,
|
skip,
|
||||||
take = DEFAULT_PAGE_SIZE
|
take = Number.MAX_SAFE_INTEGER
|
||||||
}: {
|
}: {
|
||||||
filters?: Filter[];
|
filters?: Filter[];
|
||||||
|
presetId?: MarketDataPreset;
|
||||||
skip?: number;
|
skip?: number;
|
||||||
sortColumn?: string;
|
sortColumn?: string;
|
||||||
sortDirection?: Prisma.SortOrder;
|
sortDirection?: Prisma.SortOrder;
|
||||||
@ -118,6 +121,13 @@ export class AdminService {
|
|||||||
[{ symbol: 'asc' }];
|
[{ symbol: 'asc' }];
|
||||||
const where: Prisma.SymbolProfileWhereInput = {};
|
const where: Prisma.SymbolProfileWhereInput = {};
|
||||||
|
|
||||||
|
if (
|
||||||
|
presetId === 'ETF_WITHOUT_COUNTRIES' ||
|
||||||
|
presetId === 'ETF_WITHOUT_SECTORS'
|
||||||
|
) {
|
||||||
|
filters = [{ id: 'ETF', type: 'ASSET_SUB_CLASS' }];
|
||||||
|
}
|
||||||
|
|
||||||
const { ASSET_SUB_CLASS: filtersByAssetSubClass } = groupBy(
|
const { ASSET_SUB_CLASS: filtersByAssetSubClass } = groupBy(
|
||||||
filters,
|
filters,
|
||||||
(filter) => {
|
(filter) => {
|
||||||
@ -146,7 +156,7 @@ export class AdminService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [assetProfiles, count] = await Promise.all([
|
let [assetProfiles, count] = await Promise.all([
|
||||||
this.prismaService.symbolProfile.findMany({
|
this.prismaService.symbolProfile.findMany({
|
||||||
orderBy,
|
orderBy,
|
||||||
skip,
|
skip,
|
||||||
@ -174,44 +184,60 @@ export class AdminService {
|
|||||||
this.prismaService.symbolProfile.count({ where })
|
this.prismaService.symbolProfile.count({ where })
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
let marketData = assetProfiles.map(
|
||||||
count,
|
({
|
||||||
marketData: assetProfiles.map(
|
_count,
|
||||||
({
|
assetClass,
|
||||||
_count,
|
assetSubClass,
|
||||||
|
comment,
|
||||||
|
countries,
|
||||||
|
dataSource,
|
||||||
|
Order,
|
||||||
|
sectors,
|
||||||
|
symbol
|
||||||
|
}) => {
|
||||||
|
const countriesCount = countries ? Object.keys(countries).length : 0;
|
||||||
|
const marketDataItemCount =
|
||||||
|
marketDataItems.find((marketDataItem) => {
|
||||||
|
return (
|
||||||
|
marketDataItem.dataSource === dataSource &&
|
||||||
|
marketDataItem.symbol === symbol
|
||||||
|
);
|
||||||
|
})?._count ?? 0;
|
||||||
|
const sectorsCount = sectors ? Object.keys(sectors).length : 0;
|
||||||
|
|
||||||
|
return {
|
||||||
assetClass,
|
assetClass,
|
||||||
assetSubClass,
|
assetSubClass,
|
||||||
comment,
|
comment,
|
||||||
countries,
|
countriesCount,
|
||||||
dataSource,
|
dataSource,
|
||||||
Order,
|
symbol,
|
||||||
sectors,
|
marketDataItemCount,
|
||||||
symbol
|
sectorsCount,
|
||||||
}) => {
|
activitiesCount: _count.Order,
|
||||||
const countriesCount = countries ? Object.keys(countries).length : 0;
|
date: Order?.[0]?.date
|
||||||
const marketDataItemCount =
|
};
|
||||||
marketDataItems.find((marketDataItem) => {
|
}
|
||||||
return (
|
);
|
||||||
marketDataItem.dataSource === dataSource &&
|
|
||||||
marketDataItem.symbol === symbol
|
|
||||||
);
|
|
||||||
})?._count ?? 0;
|
|
||||||
const sectorsCount = sectors ? Object.keys(sectors).length : 0;
|
|
||||||
|
|
||||||
return {
|
if (presetId) {
|
||||||
assetClass,
|
if (presetId === 'ETF_WITHOUT_COUNTRIES') {
|
||||||
assetSubClass,
|
marketData = marketData.filter(({ countriesCount }) => {
|
||||||
comment,
|
return countriesCount === 0;
|
||||||
countriesCount,
|
});
|
||||||
dataSource,
|
} else if (presetId === 'ETF_WITHOUT_SECTORS') {
|
||||||
symbol,
|
marketData = marketData.filter(({ sectorsCount }) => {
|
||||||
marketDataItemCount,
|
return sectorsCount === 0;
|
||||||
sectorsCount,
|
});
|
||||||
activitiesCount: _count.Order,
|
}
|
||||||
date: Order?.[0]?.date
|
|
||||||
};
|
count = marketData.length;
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
return {
|
||||||
|
count,
|
||||||
|
marketData
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,11 +66,11 @@ export class BenchmarkService {
|
|||||||
|
|
||||||
const promises: Promise<number>[] = [];
|
const promises: Promise<number>[] = [];
|
||||||
|
|
||||||
const quotes = await this.dataProviderService.getQuotes(
|
const quotes = await this.dataProviderService.getQuotes({
|
||||||
benchmarkAssetProfiles.map(({ dataSource, symbol }) => {
|
items: benchmarkAssetProfiles.map(({ dataSource, symbol }) => {
|
||||||
return { dataSource, symbol };
|
return { dataSource, symbol };
|
||||||
})
|
})
|
||||||
);
|
});
|
||||||
|
|
||||||
for (const { dataSource, symbol } of benchmarkAssetProfiles) {
|
for (const { dataSource, symbol } of benchmarkAssetProfiles) {
|
||||||
promises.push(this.marketDataService.getMax({ dataSource, symbol }));
|
promises.push(this.marketDataService.getMax({ dataSource, symbol }));
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
|
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 { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { ExportController } from './export.controller';
|
import { ExportController } from './export.controller';
|
||||||
@ -10,10 +11,11 @@ import { ExportService } from './export.service';
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
AccountModule,
|
||||||
ConfigurationModule,
|
ConfigurationModule,
|
||||||
DataGatheringModule,
|
DataGatheringModule,
|
||||||
DataProviderModule,
|
DataProviderModule,
|
||||||
PrismaModule,
|
OrderModule,
|
||||||
RedisCacheModule
|
RedisCacheModule
|
||||||
],
|
],
|
||||||
controllers: [ExportController],
|
controllers: [ExportController],
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
|
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 { environment } from '@ghostfolio/api/environments/environment';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
|
||||||
import { Export } from '@ghostfolio/common/interfaces';
|
import { Export } from '@ghostfolio/common/interfaces';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ExportService {
|
export class ExportService {
|
||||||
public constructor(private readonly prismaService: PrismaService) {}
|
public constructor(
|
||||||
|
private readonly accountService: AccountService,
|
||||||
|
private readonly orderService: OrderService
|
||||||
|
) {}
|
||||||
|
|
||||||
public async export({
|
public async export({
|
||||||
activityIds,
|
activityIds,
|
||||||
@ -14,36 +18,40 @@ export class ExportService {
|
|||||||
activityIds?: string[];
|
activityIds?: string[];
|
||||||
userId: string;
|
userId: string;
|
||||||
}): Promise<Export> {
|
}): Promise<Export> {
|
||||||
const accounts = await this.prismaService.account.findMany({
|
const accounts = (
|
||||||
orderBy: {
|
await this.accountService.accounts({
|
||||||
name: 'asc'
|
orderBy: {
|
||||||
},
|
name: 'asc'
|
||||||
select: {
|
},
|
||||||
accountType: true,
|
where: { userId }
|
||||||
balance: true,
|
})
|
||||||
comment: true,
|
).map(
|
||||||
currency: true,
|
({
|
||||||
id: true,
|
accountType,
|
||||||
isExcluded: true,
|
balance,
|
||||||
name: true,
|
comment,
|
||||||
platformId: true
|
currency,
|
||||||
},
|
id,
|
||||||
where: { userId }
|
isExcluded,
|
||||||
});
|
name,
|
||||||
|
platformId
|
||||||
|
}) => {
|
||||||
|
return {
|
||||||
|
accountType,
|
||||||
|
balance,
|
||||||
|
comment,
|
||||||
|
currency,
|
||||||
|
id,
|
||||||
|
isExcluded,
|
||||||
|
name,
|
||||||
|
platformId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
let activities = await this.prismaService.order.findMany({
|
let activities = await this.orderService.orders({
|
||||||
|
include: { SymbolProfile: true },
|
||||||
orderBy: { date: 'desc' },
|
orderBy: { date: 'desc' },
|
||||||
select: {
|
|
||||||
accountId: true,
|
|
||||||
comment: true,
|
|
||||||
date: true,
|
|
||||||
fee: true,
|
|
||||||
id: true,
|
|
||||||
quantity: true,
|
|
||||||
SymbolProfile: true,
|
|
||||||
type: true,
|
|
||||||
unitPrice: true
|
|
||||||
},
|
|
||||||
where: { userId }
|
where: { userId }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import * as path from 'path';
|
|||||||
import { environment } from '@ghostfolio/api/environments/environment';
|
import { environment } from '@ghostfolio/api/environments/environment';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
|
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
|
||||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { NextFunction, Request, Response } from 'express';
|
import { NextFunction, Request, Response } from 'express';
|
||||||
@ -18,6 +18,7 @@ export class FrontendMiddleware implements NestMiddleware {
|
|||||||
public indexHtmlIt = '';
|
public indexHtmlIt = '';
|
||||||
public indexHtmlNl = '';
|
public indexHtmlNl = '';
|
||||||
public indexHtmlPt = '';
|
public indexHtmlPt = '';
|
||||||
|
public sitemapXml = '';
|
||||||
|
|
||||||
private static readonly DEFAULT_DESCRIPTION =
|
private static readonly DEFAULT_DESCRIPTION =
|
||||||
'Ghostfolio is a personal finance dashboard to keep track of your assets like stocks, ETFs or cryptocurrencies across multiple platforms.';
|
'Ghostfolio is a personal finance dashboard to keep track of your assets like stocks, ETFs or cryptocurrencies across multiple platforms.';
|
||||||
@ -54,6 +55,10 @@ export class FrontendMiddleware implements NestMiddleware {
|
|||||||
this.getPathOfIndexHtmlFile('pt'),
|
this.getPathOfIndexHtmlFile('pt'),
|
||||||
'utf8'
|
'utf8'
|
||||||
);
|
);
|
||||||
|
this.sitemapXml = fs.readFileSync(
|
||||||
|
path.join(__dirname, 'assets', 'sitemap.xml'),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,6 +123,13 @@ export class FrontendMiddleware implements NestMiddleware {
|
|||||||
) {
|
) {
|
||||||
// Skip
|
// Skip
|
||||||
next();
|
next();
|
||||||
|
} else if (request.path === '/sitemap.xml') {
|
||||||
|
response.setHeader('content-type', 'application/xml');
|
||||||
|
response.send(
|
||||||
|
this.interpolate(this.sitemapXml, {
|
||||||
|
currentDate: format(getYesterday(), DATE_FORMAT)
|
||||||
|
})
|
||||||
|
);
|
||||||
} else if (request.path === '/de' || request.path.startsWith('/de/')) {
|
} else if (request.path === '/de' || request.path.startsWith('/de/')) {
|
||||||
response.send(
|
response.send(
|
||||||
this.interpolate(this.indexHtmlDe, {
|
this.interpolate(this.indexHtmlDe, {
|
||||||
@ -228,7 +240,13 @@ export class FrontendMiddleware implements NestMiddleware {
|
|||||||
private isFileRequest(filename: string) {
|
private isFileRequest(filename: string) {
|
||||||
if (filename === '/assets/LICENSE') {
|
if (filename === '/assets/LICENSE') {
|
||||||
return true;
|
return true;
|
||||||
} else if (filename.includes('auth/ey')) {
|
} else if (
|
||||||
|
filename === '/sitemap.xml' ||
|
||||||
|
filename.includes('auth/ey') ||
|
||||||
|
filename.includes(
|
||||||
|
'personal-finance-tools/open-source-alternative-to-markets.sh'
|
||||||
|
)
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,10 +8,14 @@ import {
|
|||||||
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
||||||
import { PlatformService } from '@ghostfolio/api/app/platform/platform.service';
|
import { PlatformService } from '@ghostfolio/api/app/platform/platform.service';
|
||||||
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
|
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
|
||||||
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service';
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
||||||
import { parseDate } from '@ghostfolio/common/helper';
|
import {
|
||||||
|
getAssetProfileIdentifier,
|
||||||
|
parseDate
|
||||||
|
} from '@ghostfolio/common/helper';
|
||||||
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
import {
|
import {
|
||||||
AccountWithPlatform,
|
AccountWithPlatform,
|
||||||
@ -21,12 +25,14 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { DataSource, Prisma, SymbolProfile } from '@prisma/client';
|
import { DataSource, Prisma, SymbolProfile } from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import { endOfToday, isAfter, isSameDay, parseISO } from 'date-fns';
|
import { endOfToday, isAfter, isSameDay, parseISO } from 'date-fns';
|
||||||
|
import { uniqBy } from 'lodash';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ImportService {
|
export class ImportService {
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly accountService: AccountService,
|
private readonly accountService: AccountService,
|
||||||
|
private readonly dataGatheringService: DataGatheringService,
|
||||||
private readonly dataProviderService: DataProviderService,
|
private readonly dataProviderService: DataProviderService,
|
||||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||||
private readonly orderService: OrderService,
|
private readonly orderService: OrderService,
|
||||||
@ -220,8 +226,7 @@ export class ImportService {
|
|||||||
|
|
||||||
const assetProfiles = await this.validateActivities({
|
const assetProfiles = await this.validateActivities({
|
||||||
activitiesDto,
|
activitiesDto,
|
||||||
maxActivitiesToImport,
|
maxActivitiesToImport
|
||||||
userId
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const activitiesExtendedWithErrors = await this.extendActivitiesWithErrors({
|
const activitiesExtendedWithErrors = await this.extendActivitiesWithErrors({
|
||||||
@ -250,10 +255,37 @@ export class ImportService {
|
|||||||
error,
|
error,
|
||||||
fee,
|
fee,
|
||||||
quantity,
|
quantity,
|
||||||
SymbolProfile: assetProfile,
|
SymbolProfile,
|
||||||
type,
|
type,
|
||||||
unitPrice
|
unitPrice
|
||||||
} of activitiesExtendedWithErrors) {
|
} of activitiesExtendedWithErrors) {
|
||||||
|
const assetProfile = assetProfiles[
|
||||||
|
getAssetProfileIdentifier({
|
||||||
|
dataSource: SymbolProfile.dataSource,
|
||||||
|
symbol: SymbolProfile.symbol
|
||||||
|
})
|
||||||
|
] ?? {
|
||||||
|
currency: SymbolProfile.currency,
|
||||||
|
dataSource: SymbolProfile.dataSource,
|
||||||
|
symbol: SymbolProfile.symbol
|
||||||
|
};
|
||||||
|
const {
|
||||||
|
assetClass,
|
||||||
|
assetSubClass,
|
||||||
|
countries,
|
||||||
|
createdAt,
|
||||||
|
currency,
|
||||||
|
dataSource,
|
||||||
|
id,
|
||||||
|
isin,
|
||||||
|
name,
|
||||||
|
scraperConfiguration,
|
||||||
|
sectors,
|
||||||
|
symbol,
|
||||||
|
symbolMapping,
|
||||||
|
url,
|
||||||
|
updatedAt
|
||||||
|
} = assetProfile;
|
||||||
const validatedAccount = accounts.find(({ id }) => {
|
const validatedAccount = accounts.find(({ id }) => {
|
||||||
return id === accountId;
|
return id === accountId;
|
||||||
});
|
});
|
||||||
@ -279,23 +311,22 @@ export class ImportService {
|
|||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
isDraft: isAfter(date, endOfToday()),
|
isDraft: isAfter(date, endOfToday()),
|
||||||
SymbolProfile: {
|
SymbolProfile: {
|
||||||
assetClass: assetProfile.assetClass,
|
assetClass,
|
||||||
assetSubClass: assetProfile.assetSubClass,
|
assetSubClass,
|
||||||
comment: assetProfile.comment,
|
countries,
|
||||||
countries: assetProfile.countries,
|
createdAt,
|
||||||
createdAt: assetProfile.createdAt,
|
currency,
|
||||||
currency: assetProfile.currency,
|
dataSource,
|
||||||
dataSource: assetProfile.dataSource,
|
id,
|
||||||
id: assetProfile.id,
|
isin,
|
||||||
isin: assetProfile.isin,
|
name,
|
||||||
name: assetProfile.name,
|
scraperConfiguration,
|
||||||
scraperConfiguration: assetProfile.scraperConfiguration,
|
sectors,
|
||||||
sectors: assetProfile.sectors,
|
symbol,
|
||||||
symbol: assetProfile.currency,
|
symbolMapping,
|
||||||
symbolMapping: assetProfile.symbolMapping,
|
updatedAt,
|
||||||
updatedAt: assetProfile.updatedAt,
|
url,
|
||||||
url: assetProfile.url,
|
comment: assetProfile.comment
|
||||||
...assetProfiles[assetProfile.symbol]
|
|
||||||
},
|
},
|
||||||
Account: validatedAccount,
|
Account: validatedAccount,
|
||||||
symbolProfileId: undefined,
|
symbolProfileId: undefined,
|
||||||
@ -318,14 +349,14 @@ export class ImportService {
|
|||||||
SymbolProfile: {
|
SymbolProfile: {
|
||||||
connectOrCreate: {
|
connectOrCreate: {
|
||||||
create: {
|
create: {
|
||||||
currency: assetProfile.currency,
|
currency,
|
||||||
dataSource: assetProfile.dataSource,
|
dataSource,
|
||||||
symbol: assetProfile.symbol
|
symbol
|
||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
dataSource_symbol: {
|
dataSource_symbol: {
|
||||||
dataSource: assetProfile.dataSource,
|
dataSource,
|
||||||
symbol: assetProfile.symbol
|
symbol
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -337,24 +368,49 @@ export class ImportService {
|
|||||||
|
|
||||||
const value = new Big(quantity).mul(unitPrice).toNumber();
|
const value = new Big(quantity).mul(unitPrice).toNumber();
|
||||||
|
|
||||||
//@ts-ignore
|
|
||||||
activities.push({
|
activities.push({
|
||||||
...order,
|
...order,
|
||||||
error,
|
error,
|
||||||
value,
|
value,
|
||||||
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
||||||
fee,
|
fee,
|
||||||
assetProfile.currency,
|
currency,
|
||||||
userCurrency
|
userCurrency
|
||||||
),
|
),
|
||||||
|
//@ts-ignore
|
||||||
|
SymbolProfile: assetProfile,
|
||||||
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
||||||
value,
|
value,
|
||||||
assetProfile.currency,
|
currency,
|
||||||
userCurrency
|
userCurrency
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
activities.sort((activity1, activity2) => {
|
||||||
|
return Number(activity1.date) - Number(activity2.date);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isDryRun) {
|
||||||
|
// Gather symbol data in the background, if not dry run
|
||||||
|
const uniqueActivities = uniqBy(activities, ({ SymbolProfile }) => {
|
||||||
|
return getAssetProfileIdentifier({
|
||||||
|
dataSource: SymbolProfile.dataSource,
|
||||||
|
symbol: SymbolProfile.symbol
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.dataGatheringService.gatherSymbols(
|
||||||
|
uniqueActivities.map(({ date, SymbolProfile }) => {
|
||||||
|
return {
|
||||||
|
date,
|
||||||
|
dataSource: SymbolProfile.dataSource,
|
||||||
|
symbol: SymbolProfile.symbol
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return activities;
|
return activities;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -446,25 +502,30 @@ export class ImportService {
|
|||||||
|
|
||||||
private async validateActivities({
|
private async validateActivities({
|
||||||
activitiesDto,
|
activitiesDto,
|
||||||
maxActivitiesToImport,
|
maxActivitiesToImport
|
||||||
userId
|
|
||||||
}: {
|
}: {
|
||||||
activitiesDto: Partial<CreateOrderDto>[];
|
activitiesDto: Partial<CreateOrderDto>[];
|
||||||
maxActivitiesToImport: number;
|
maxActivitiesToImport: number;
|
||||||
userId: string;
|
|
||||||
}) {
|
}) {
|
||||||
if (activitiesDto?.length > maxActivitiesToImport) {
|
if (activitiesDto?.length > maxActivitiesToImport) {
|
||||||
throw new Error(`Too many activities (${maxActivitiesToImport} at most)`);
|
throw new Error(`Too many activities (${maxActivitiesToImport} at most)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const assetProfiles: {
|
const assetProfiles: {
|
||||||
[symbol: string]: Partial<SymbolProfile>;
|
[assetProfileIdentifier: string]: Partial<SymbolProfile>;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
|
const uniqueActivitiesDto = uniqBy(
|
||||||
|
activitiesDto,
|
||||||
|
({ dataSource, symbol }) => {
|
||||||
|
return getAssetProfileIdentifier({ dataSource, symbol });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
for (const [
|
for (const [
|
||||||
index,
|
index,
|
||||||
{ currency, dataSource, symbol }
|
{ currency, dataSource, symbol }
|
||||||
] of activitiesDto.entries()) {
|
] of uniqueActivitiesDto.entries()) {
|
||||||
if (dataSource !== 'MANUAL') {
|
if (dataSource !== 'MANUAL') {
|
||||||
const assetProfile = (
|
const assetProfile = (
|
||||||
await this.dataProviderService.getAssetProfiles([
|
await this.dataProviderService.getAssetProfiles([
|
||||||
@ -484,7 +545,8 @@ export class ImportService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
assetProfiles[symbol] = assetProfile;
|
assetProfiles[getAssetProfileIdentifier({ dataSource, symbol })] =
|
||||||
|
assetProfile;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/
|
|||||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
||||||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
||||||
import { ApiService } from '@ghostfolio/api/services/api/api.service';
|
import { ApiService } from '@ghostfolio/api/services/api/api.service';
|
||||||
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service';
|
||||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
|
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
|
||||||
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
|
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
@ -36,6 +37,7 @@ import { UpdateOrderDto } from './update-order.dto';
|
|||||||
export class OrderController {
|
export class OrderController {
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly apiService: ApiService,
|
private readonly apiService: ApiService,
|
||||||
|
private readonly dataGatheringService: DataGatheringService,
|
||||||
private readonly impersonationService: ImpersonationService,
|
private readonly impersonationService: ImpersonationService,
|
||||||
private readonly orderService: OrderService,
|
private readonly orderService: OrderService,
|
||||||
@Inject(REQUEST) private readonly request: RequestWithUser
|
@Inject(REQUEST) private readonly request: RequestWithUser
|
||||||
@ -123,7 +125,7 @@ export class OrderController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.orderService.createOrder({
|
const order = await this.orderService.createOrder({
|
||||||
...data,
|
...data,
|
||||||
date: parseISO(data.date),
|
date: parseISO(data.date),
|
||||||
SymbolProfile: {
|
SymbolProfile: {
|
||||||
@ -144,6 +146,19 @@ export class OrderController {
|
|||||||
User: { connect: { id: this.request.user.id } },
|
User: { connect: { id: this.request.user.id } },
|
||||||
userId: this.request.user.id
|
userId: this.request.user.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!order.isDraft) {
|
||||||
|
// Gather symbol data in the background, if not draft
|
||||||
|
this.dataGatheringService.gatherSymbols([
|
||||||
|
{
|
||||||
|
dataSource: data.dataSource,
|
||||||
|
date: order.date,
|
||||||
|
symbol: data.symbol
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return order;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put(':id')
|
@Put(':id')
|
||||||
|
@ -2,6 +2,7 @@ import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
|||||||
import { CacheModule } from '@ghostfolio/api/app/cache/cache.module';
|
import { CacheModule } from '@ghostfolio/api/app/cache/cache.module';
|
||||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
||||||
|
import { AccountBalanceService } from '@ghostfolio/api/services/account-balance/account-balance.service';
|
||||||
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
|
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
||||||
@ -31,6 +32,6 @@ import { OrderService } from './order.service';
|
|||||||
SymbolProfileModule,
|
SymbolProfileModule,
|
||||||
UserModule
|
UserModule
|
||||||
],
|
],
|
||||||
providers: [AccountService, OrderService]
|
providers: [AccountBalanceService, AccountService, OrderService]
|
||||||
})
|
})
|
||||||
export class OrderModule {}
|
export class OrderModule {}
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
GATHER_ASSET_PROFILE_PROCESS,
|
GATHER_ASSET_PROFILE_PROCESS,
|
||||||
GATHER_ASSET_PROFILE_PROCESS_OPTIONS
|
GATHER_ASSET_PROFILE_PROCESS_OPTIONS
|
||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
|
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
|
||||||
import { Filter } from '@ghostfolio/common/interfaces';
|
import { Filter } from '@ghostfolio/common/interfaces';
|
||||||
import { OrderWithAccount } from '@ghostfolio/common/types';
|
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
@ -117,7 +118,7 @@ export class OrderService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.dataGatheringService.addJobToQueue({
|
this.dataGatheringService.addJobToQueue({
|
||||||
data: {
|
data: {
|
||||||
dataSource: data.SymbolProfile.connectOrCreate.create.dataSource,
|
dataSource: data.SymbolProfile.connectOrCreate.create.dataSource,
|
||||||
symbol: data.SymbolProfile.connectOrCreate.create.symbol
|
symbol: data.SymbolProfile.connectOrCreate.create.symbol
|
||||||
@ -125,26 +126,13 @@ export class OrderService {
|
|||||||
name: GATHER_ASSET_PROFILE_PROCESS,
|
name: GATHER_ASSET_PROFILE_PROCESS,
|
||||||
opts: {
|
opts: {
|
||||||
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
|
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
|
||||||
jobId: `${data.SymbolProfile.connectOrCreate.create.dataSource}-${data.SymbolProfile.connectOrCreate.create.symbol}`
|
jobId: getAssetProfileIdentifier({
|
||||||
|
dataSource: data.SymbolProfile.connectOrCreate.create.dataSource,
|
||||||
|
symbol: data.SymbolProfile.connectOrCreate.create.symbol
|
||||||
|
})
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const isDraft =
|
|
||||||
data.type === 'LIABILITY'
|
|
||||||
? false
|
|
||||||
: isAfter(data.date as Date, endOfToday());
|
|
||||||
|
|
||||||
if (!isDraft) {
|
|
||||||
// Gather symbol data of order in the background, if not draft
|
|
||||||
this.dataGatheringService.gatherSymbols([
|
|
||||||
{
|
|
||||||
dataSource: data.SymbolProfile.connectOrCreate.create.dataSource,
|
|
||||||
date: <Date>data.date,
|
|
||||||
symbol: data.SymbolProfile.connectOrCreate.create.symbol
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
delete data.accountId;
|
delete data.accountId;
|
||||||
delete data.assetClass;
|
delete data.assetClass;
|
||||||
delete data.assetSubClass;
|
delete data.assetSubClass;
|
||||||
@ -162,6 +150,11 @@ export class OrderService {
|
|||||||
|
|
||||||
const orderData: Prisma.OrderCreateInput = data;
|
const orderData: Prisma.OrderCreateInput = data;
|
||||||
|
|
||||||
|
const isDraft =
|
||||||
|
data.type === 'LIABILITY'
|
||||||
|
? false
|
||||||
|
: isAfter(data.date as Date, endOfToday());
|
||||||
|
|
||||||
const order = await this.prismaService.order.create({
|
const order = await this.prismaService.order.create({
|
||||||
data: {
|
data: {
|
||||||
...orderData,
|
...orderData,
|
||||||
|
@ -38,7 +38,7 @@ export class CurrentRateService {
|
|||||||
if (includeToday) {
|
if (includeToday) {
|
||||||
promises.push(
|
promises.push(
|
||||||
this.dataProviderService
|
this.dataProviderService
|
||||||
.getQuotes(dataGatheringItems)
|
.getQuotes({ items: dataGatheringItems })
|
||||||
.then((dataResultProvider) => {
|
.then((dataResultProvider) => {
|
||||||
const result: GetValueObject[] = [];
|
const result: GetValueObject[] = [];
|
||||||
for (const dataGatheringItem of dataGatheringItems) {
|
for (const dataGatheringItem of dataGatheringItems) {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { DataSource, Type as TypeOfOrder } from '@prisma/client';
|
import { DataSource, Tag, Type as TypeOfOrder } from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
|
|
||||||
export interface PortfolioOrder {
|
export interface PortfolioOrder {
|
||||||
@ -9,6 +9,7 @@ export interface PortfolioOrder {
|
|||||||
name: string;
|
name: string;
|
||||||
quantity: Big;
|
quantity: Big;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
|
tags?: Tag[];
|
||||||
type: TypeOfOrder;
|
type: TypeOfOrder;
|
||||||
unitPrice: Big;
|
unitPrice: Big;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { DataSource } from '@prisma/client';
|
import { DataSource, Tag } from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
|
|
||||||
export interface TransactionPointSymbol {
|
export interface TransactionPointSymbol {
|
||||||
@ -9,5 +9,6 @@ export interface TransactionPointSymbol {
|
|||||||
investment: Big;
|
investment: Big;
|
||||||
quantity: Big;
|
quantity: Big;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
|
tags?: Tag[];
|
||||||
transactionCount: number;
|
transactionCount: number;
|
||||||
}
|
}
|
||||||
|
@ -114,6 +114,7 @@ export class PortfolioCalculator {
|
|||||||
firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
|
firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
|
||||||
quantity: newQuantity,
|
quantity: newQuantity,
|
||||||
symbol: order.symbol,
|
symbol: order.symbol,
|
||||||
|
tags: order.tags,
|
||||||
transactionCount: oldAccumulatedSymbol.transactionCount + 1
|
transactionCount: oldAccumulatedSymbol.transactionCount + 1
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
@ -125,6 +126,7 @@ export class PortfolioCalculator {
|
|||||||
investment: unitPrice.mul(order.quantity).mul(factor),
|
investment: unitPrice.mul(order.quantity).mul(factor),
|
||||||
quantity: order.quantity.mul(factor),
|
quantity: order.quantity.mul(factor),
|
||||||
symbol: order.symbol,
|
symbol: order.symbol,
|
||||||
|
tags: order.tags,
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -492,6 +494,7 @@ export class PortfolioCalculator {
|
|||||||
: null,
|
: null,
|
||||||
quantity: item.quantity,
|
quantity: item.quantity,
|
||||||
symbol: item.symbol,
|
symbol: item.symbol,
|
||||||
|
tags: item.tags,
|
||||||
transactionCount: item.transactionCount
|
transactionCount: item.transactionCount
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -134,7 +134,7 @@ export class PortfolioController {
|
|||||||
portfolioPosition.netPerformance = null;
|
portfolioPosition.netPerformance = null;
|
||||||
portfolioPosition.quantity = null;
|
portfolioPosition.quantity = null;
|
||||||
portfolioPosition.valueInPercentage =
|
portfolioPosition.valueInPercentage =
|
||||||
portfolioPosition.value / totalValue;
|
portfolioPosition.valueInBaseCurrency / totalValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [name, { valueInBaseCurrency }] of Object.entries(accounts)) {
|
for (const [name, { valueInBaseCurrency }] of Object.entries(accounts)) {
|
||||||
@ -161,10 +161,12 @@ export class PortfolioController {
|
|||||||
'emergencyFund',
|
'emergencyFund',
|
||||||
'excludedAccountsAndActivities',
|
'excludedAccountsAndActivities',
|
||||||
'fees',
|
'fees',
|
||||||
|
'fireWealth',
|
||||||
'items',
|
'items',
|
||||||
'liabilities',
|
'liabilities',
|
||||||
'netWorth',
|
'netWorth',
|
||||||
'totalBuy',
|
'totalBuy',
|
||||||
|
'totalInvestment',
|
||||||
'totalSell'
|
'totalSell'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -177,6 +179,9 @@ export class PortfolioController {
|
|||||||
countries: hasDetails ? portfolioPosition.countries : [],
|
countries: hasDetails ? portfolioPosition.countries : [],
|
||||||
currency: hasDetails ? portfolioPosition.currency : undefined,
|
currency: hasDetails ? portfolioPosition.currency : undefined,
|
||||||
markets: hasDetails ? portfolioPosition.markets : undefined,
|
markets: hasDetails ? portfolioPosition.markets : undefined,
|
||||||
|
marketsAdvanced: hasDetails
|
||||||
|
? portfolioPosition.marketsAdvanced
|
||||||
|
: undefined,
|
||||||
sectors: hasDetails ? portfolioPosition.sectors : []
|
sectors: hasDetails ? portfolioPosition.sectors : []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -445,7 +450,8 @@ export class PortfolioController {
|
|||||||
|
|
||||||
for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
|
for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
|
||||||
portfolioPublicDetails.holdings[symbol] = {
|
portfolioPublicDetails.holdings[symbol] = {
|
||||||
allocationInPercentage: portfolioPosition.value / totalValue,
|
allocationInPercentage:
|
||||||
|
portfolioPosition.valueInBaseCurrency / totalValue,
|
||||||
countries: hasDetails ? portfolioPosition.countries : [],
|
countries: hasDetails ? portfolioPosition.countries : [],
|
||||||
currency: hasDetails ? portfolioPosition.currency : undefined,
|
currency: hasDetails ? portfolioPosition.currency : undefined,
|
||||||
dataSource: portfolioPosition.dataSource,
|
dataSource: portfolioPosition.dataSource,
|
||||||
@ -456,7 +462,7 @@ export class PortfolioController {
|
|||||||
sectors: hasDetails ? portfolioPosition.sectors : [],
|
sectors: hasDetails ? portfolioPosition.sectors : [],
|
||||||
symbol: portfolioPosition.symbol,
|
symbol: portfolioPosition.symbol,
|
||||||
url: portfolioPosition.url,
|
url: portfolioPosition.url,
|
||||||
valueInPercentage: portfolioPosition.value / totalValue
|
valueInPercentage: portfolioPosition.valueInBaseCurrency / totalValue
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import { AccessModule } from '@ghostfolio/api/app/access/access.module';
|
|||||||
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||||
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
|
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
|
||||||
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
||||||
|
import { AccountBalanceService } from '@ghostfolio/api/services/account-balance/account-balance.service';
|
||||||
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
|
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
||||||
@ -36,6 +37,7 @@ import { RulesService } from './rules.service';
|
|||||||
UserModule
|
UserModule
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
AccountBalanceService,
|
||||||
AccountService,
|
AccountService,
|
||||||
CurrentRateService,
|
CurrentRateService,
|
||||||
PortfolioService,
|
PortfolioService,
|
||||||
|
@ -42,7 +42,6 @@ import type {
|
|||||||
AccountWithValue,
|
AccountWithValue,
|
||||||
DateRange,
|
DateRange,
|
||||||
GroupBy,
|
GroupBy,
|
||||||
Market,
|
|
||||||
OrderWithAccount,
|
OrderWithAccount,
|
||||||
RequestWithUser,
|
RequestWithUser,
|
||||||
UserWithSettings
|
UserWithSettings
|
||||||
@ -84,8 +83,10 @@ import {
|
|||||||
import { PortfolioCalculator } from './portfolio-calculator';
|
import { PortfolioCalculator } from './portfolio-calculator';
|
||||||
import { RulesService } from './rules.service';
|
import { RulesService } from './rules.service';
|
||||||
|
|
||||||
|
const asiaPacificMarkets = require('../../assets/countries/asia-pacific-markets.json');
|
||||||
const developedMarkets = require('../../assets/countries/developed-markets.json');
|
const developedMarkets = require('../../assets/countries/developed-markets.json');
|
||||||
const emergingMarkets = require('../../assets/countries/emerging-markets.json');
|
const emergingMarkets = require('../../assets/countries/emerging-markets.json');
|
||||||
|
const europeMarkets = require('../../assets/countries/europe-markets.json');
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PortfolioService {
|
export class PortfolioService {
|
||||||
@ -504,15 +505,17 @@ export class PortfolioService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataGatheringItems = currentPositions.positions.map((position) => {
|
const dataGatheringItems = currentPositions.positions.map(
|
||||||
return {
|
({ dataSource, symbol }) => {
|
||||||
dataSource: position.dataSource,
|
return {
|
||||||
symbol: position.symbol
|
dataSource,
|
||||||
};
|
symbol
|
||||||
});
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
||||||
this.dataProviderService.getQuotes(dataGatheringItems),
|
this.dataProviderService.getQuotes({ items: dataGatheringItems }),
|
||||||
this.symbolProfileService.getSymbolProfiles(dataGatheringItems)
|
this.symbolProfileService.getSymbolProfiles(dataGatheringItems)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -536,30 +539,79 @@ export class PortfolioService {
|
|||||||
const symbolProfile = symbolProfileMap[item.symbol];
|
const symbolProfile = symbolProfileMap[item.symbol];
|
||||||
const dataProviderResponse = dataProviderResponses[item.symbol];
|
const dataProviderResponse = dataProviderResponses[item.symbol];
|
||||||
|
|
||||||
const markets: { [key in Market]: number } = {
|
const markets: PortfolioPosition['markets'] = {
|
||||||
|
[UNKNOWN_KEY]: 0,
|
||||||
developedMarkets: 0,
|
developedMarkets: 0,
|
||||||
emergingMarkets: 0,
|
emergingMarkets: 0,
|
||||||
otherMarkets: 0
|
otherMarkets: 0
|
||||||
};
|
};
|
||||||
|
const marketsAdvanced: PortfolioPosition['marketsAdvanced'] = {
|
||||||
|
[UNKNOWN_KEY]: 0,
|
||||||
|
asiaPacific: 0,
|
||||||
|
emergingMarkets: 0,
|
||||||
|
europe: 0,
|
||||||
|
japan: 0,
|
||||||
|
northAmerica: 0,
|
||||||
|
otherMarkets: 0
|
||||||
|
};
|
||||||
|
|
||||||
for (const country of symbolProfile.countries) {
|
if (symbolProfile.countries.length > 0) {
|
||||||
if (developedMarkets.includes(country.code)) {
|
for (const country of symbolProfile.countries) {
|
||||||
markets.developedMarkets = new Big(markets.developedMarkets)
|
if (developedMarkets.includes(country.code)) {
|
||||||
.plus(country.weight)
|
markets.developedMarkets = new Big(markets.developedMarkets)
|
||||||
.toNumber();
|
.plus(country.weight)
|
||||||
} else if (emergingMarkets.includes(country.code)) {
|
.toNumber();
|
||||||
markets.emergingMarkets = new Big(markets.emergingMarkets)
|
} else if (emergingMarkets.includes(country.code)) {
|
||||||
.plus(country.weight)
|
markets.emergingMarkets = new Big(markets.emergingMarkets)
|
||||||
.toNumber();
|
.plus(country.weight)
|
||||||
} else {
|
.toNumber();
|
||||||
markets.otherMarkets = new Big(markets.otherMarkets)
|
} else {
|
||||||
.plus(country.weight)
|
markets.otherMarkets = new Big(markets.otherMarkets)
|
||||||
.toNumber();
|
.plus(country.weight)
|
||||||
|
.toNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (country.code === 'JP') {
|
||||||
|
marketsAdvanced.japan = new Big(marketsAdvanced.japan)
|
||||||
|
.plus(country.weight)
|
||||||
|
.toNumber();
|
||||||
|
} else if (country.code === 'CA' || country.code === 'US') {
|
||||||
|
marketsAdvanced.northAmerica = new Big(marketsAdvanced.northAmerica)
|
||||||
|
.plus(country.weight)
|
||||||
|
.toNumber();
|
||||||
|
} else if (asiaPacificMarkets.includes(country.code)) {
|
||||||
|
marketsAdvanced.asiaPacific = new Big(marketsAdvanced.asiaPacific)
|
||||||
|
.plus(country.weight)
|
||||||
|
.toNumber();
|
||||||
|
} else if (emergingMarkets.includes(country.code)) {
|
||||||
|
marketsAdvanced.emergingMarkets = new Big(
|
||||||
|
marketsAdvanced.emergingMarkets
|
||||||
|
)
|
||||||
|
.plus(country.weight)
|
||||||
|
.toNumber();
|
||||||
|
} else if (europeMarkets.includes(country.code)) {
|
||||||
|
marketsAdvanced.europe = new Big(marketsAdvanced.europe)
|
||||||
|
.plus(country.weight)
|
||||||
|
.toNumber();
|
||||||
|
} else {
|
||||||
|
marketsAdvanced.otherMarkets = new Big(marketsAdvanced.otherMarkets)
|
||||||
|
.plus(country.weight)
|
||||||
|
.toNumber();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
markets[UNKNOWN_KEY] = new Big(markets[UNKNOWN_KEY])
|
||||||
|
.plus(value)
|
||||||
|
.toNumber();
|
||||||
|
|
||||||
|
marketsAdvanced[UNKNOWN_KEY] = new Big(marketsAdvanced[UNKNOWN_KEY])
|
||||||
|
.plus(value)
|
||||||
|
.toNumber();
|
||||||
}
|
}
|
||||||
|
|
||||||
holdings[item.symbol] = {
|
holdings[item.symbol] = {
|
||||||
markets,
|
markets,
|
||||||
|
marketsAdvanced,
|
||||||
allocationInPercentage: filteredValueInBaseCurrency.eq(0)
|
allocationInPercentage: filteredValueInBaseCurrency.eq(0)
|
||||||
? 0
|
? 0
|
||||||
: value.div(filteredValueInBaseCurrency).toNumber(),
|
: value.div(filteredValueInBaseCurrency).toNumber(),
|
||||||
@ -581,9 +633,10 @@ export class PortfolioService {
|
|||||||
quantity: item.quantity.toNumber(),
|
quantity: item.quantity.toNumber(),
|
||||||
sectors: symbolProfile.sectors,
|
sectors: symbolProfile.sectors,
|
||||||
symbol: item.symbol,
|
symbol: item.symbol,
|
||||||
|
tags: item.tags,
|
||||||
transactionCount: item.transactionCount,
|
transactionCount: item.transactionCount,
|
||||||
url: symbolProfile.url,
|
url: symbolProfile.url,
|
||||||
value: value.toNumber()
|
valueInBaseCurrency: value.toNumber()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -626,7 +679,7 @@ export class PortfolioService {
|
|||||||
const emergencyFundInCash = emergencyFund
|
const emergencyFundInCash = emergencyFund
|
||||||
.minus(
|
.minus(
|
||||||
this.getEmergencyFundPositionsValueInBaseCurrency({
|
this.getEmergencyFundPositionsValueInBaseCurrency({
|
||||||
activities: orders
|
holdings
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.toNumber();
|
.toNumber();
|
||||||
@ -643,7 +696,7 @@ export class PortfolioService {
|
|||||||
holdings[userCurrency] = {
|
holdings[userCurrency] = {
|
||||||
...emergencyFundCashPositions[userCurrency],
|
...emergencyFundCashPositions[userCurrency],
|
||||||
investment: emergencyFundInCash,
|
investment: emergencyFundInCash,
|
||||||
value: emergencyFundInCash
|
valueInBaseCurrency: emergencyFundInCash
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -654,7 +707,7 @@ export class PortfolioService {
|
|||||||
balanceInBaseCurrency: cashDetails.balanceInBaseCurrency,
|
balanceInBaseCurrency: cashDetails.balanceInBaseCurrency,
|
||||||
emergencyFundPositionsValueInBaseCurrency:
|
emergencyFundPositionsValueInBaseCurrency:
|
||||||
this.getEmergencyFundPositionsValueInBaseCurrency({
|
this.getEmergencyFundPositionsValueInBaseCurrency({
|
||||||
activities: orders
|
holdings
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -740,6 +793,7 @@ export class PortfolioService {
|
|||||||
name: order.SymbolProfile?.name,
|
name: order.SymbolProfile?.name,
|
||||||
quantity: new Big(order.quantity),
|
quantity: new Big(order.quantity),
|
||||||
symbol: order.SymbolProfile.symbol,
|
symbol: order.SymbolProfile.symbol,
|
||||||
|
tags: order.tags,
|
||||||
type: order.type,
|
type: order.type,
|
||||||
unitPrice: new Big(order.unitPrice)
|
unitPrice: new Big(order.unitPrice)
|
||||||
}));
|
}));
|
||||||
@ -897,9 +951,9 @@ export class PortfolioService {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const currentData = await this.dataProviderService.getQuotes([
|
const currentData = await this.dataProviderService.getQuotes({
|
||||||
{ dataSource: DataSource.YAHOO, symbol: aSymbol }
|
items: [{ dataSource: DataSource.YAHOO, symbol: aSymbol }]
|
||||||
]);
|
});
|
||||||
const marketPrice = currentData[aSymbol]?.marketPrice;
|
const marketPrice = currentData[aSymbol]?.marketPrice;
|
||||||
|
|
||||||
let historicalData = await this.dataProviderService.getHistorical(
|
let historicalData = await this.dataProviderService.getHistorical(
|
||||||
@ -1000,15 +1054,15 @@ export class PortfolioService {
|
|||||||
(item) => !item.quantity.eq(0)
|
(item) => !item.quantity.eq(0)
|
||||||
);
|
);
|
||||||
|
|
||||||
const dataGatheringItem = positions.map((position) => {
|
const dataGatheringItems = positions.map(({ dataSource, symbol }) => {
|
||||||
return {
|
return {
|
||||||
dataSource: position.dataSource,
|
dataSource,
|
||||||
symbol: position.symbol
|
symbol
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
||||||
this.dataProviderService.getQuotes(dataGatheringItem),
|
this.dataProviderService.getQuotes({ items: dataGatheringItems }),
|
||||||
this.symbolProfileService.getSymbolProfiles(
|
this.symbolProfileService.getSymbolProfiles(
|
||||||
positions.map(({ dataSource, symbol }) => {
|
positions.map(({ dataSource, symbol }) => {
|
||||||
return { dataSource, symbol };
|
return { dataSource, symbol };
|
||||||
@ -1276,7 +1330,7 @@ export class PortfolioService {
|
|||||||
|
|
||||||
if (cashPositions[account.currency]) {
|
if (cashPositions[account.currency]) {
|
||||||
cashPositions[account.currency].investment += convertedBalance;
|
cashPositions[account.currency].investment += convertedBalance;
|
||||||
cashPositions[account.currency].value += convertedBalance;
|
cashPositions[account.currency].valueInBaseCurrency += convertedBalance;
|
||||||
} else {
|
} else {
|
||||||
cashPositions[account.currency] = this.getInitialCashPosition({
|
cashPositions[account.currency] = this.getInitialCashPosition({
|
||||||
balance: convertedBalance,
|
balance: convertedBalance,
|
||||||
@ -1288,7 +1342,9 @@ export class PortfolioService {
|
|||||||
for (const symbol of Object.keys(cashPositions)) {
|
for (const symbol of Object.keys(cashPositions)) {
|
||||||
// Calculate allocations for each currency
|
// Calculate allocations for each currency
|
||||||
cashPositions[symbol].allocationInPercentage = value.gt(0)
|
cashPositions[symbol].allocationInPercentage = value.gt(0)
|
||||||
? new Big(cashPositions[symbol].value).div(value).toNumber()
|
? new Big(cashPositions[symbol].valueInBaseCurrency)
|
||||||
|
.div(value)
|
||||||
|
.toNumber()
|
||||||
: 0;
|
: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1388,13 +1444,13 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getEmergencyFundPositionsValueInBaseCurrency({
|
private getEmergencyFundPositionsValueInBaseCurrency({
|
||||||
activities
|
holdings
|
||||||
}: {
|
}: {
|
||||||
activities: Activity[];
|
holdings: PortfolioDetails['holdings'];
|
||||||
}) {
|
}) {
|
||||||
const emergencyFundOrders = activities.filter((activity) => {
|
const emergencyFundHoldings = Object.values(holdings).filter(({ tags }) => {
|
||||||
return (
|
return (
|
||||||
activity.tags?.some(({ id }) => {
|
tags?.some(({ id }) => {
|
||||||
return id === EMERGENCY_FUND_TAG_ID;
|
return id === EMERGENCY_FUND_TAG_ID;
|
||||||
}) ?? false
|
}) ?? false
|
||||||
);
|
);
|
||||||
@ -1402,18 +1458,9 @@ export class PortfolioService {
|
|||||||
|
|
||||||
let valueInBaseCurrencyOfEmergencyFundPositions = new Big(0);
|
let valueInBaseCurrencyOfEmergencyFundPositions = new Big(0);
|
||||||
|
|
||||||
for (const order of emergencyFundOrders) {
|
for (const { valueInBaseCurrency } of emergencyFundHoldings) {
|
||||||
if (order.type === 'BUY') {
|
valueInBaseCurrencyOfEmergencyFundPositions =
|
||||||
valueInBaseCurrencyOfEmergencyFundPositions =
|
valueInBaseCurrencyOfEmergencyFundPositions.plus(valueInBaseCurrency);
|
||||||
valueInBaseCurrencyOfEmergencyFundPositions.plus(
|
|
||||||
order.valueInBaseCurrency
|
|
||||||
);
|
|
||||||
} else if (order.type === 'SELL') {
|
|
||||||
valueInBaseCurrencyOfEmergencyFundPositions =
|
|
||||||
valueInBaseCurrencyOfEmergencyFundPositions.minus(
|
|
||||||
order.valueInBaseCurrency
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return valueInBaseCurrencyOfEmergencyFundPositions.toNumber();
|
return valueInBaseCurrencyOfEmergencyFundPositions.toNumber();
|
||||||
@ -1472,8 +1519,9 @@ export class PortfolioService {
|
|||||||
quantity: 0,
|
quantity: 0,
|
||||||
sectors: [],
|
sectors: [],
|
||||||
symbol: currency,
|
symbol: currency,
|
||||||
|
tags: [],
|
||||||
transactionCount: 0,
|
transactionCount: 0,
|
||||||
value: balance
|
valueInBaseCurrency: balance
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1499,7 +1547,13 @@ export class PortfolioService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getLiabilities(activities: OrderWithAccount[]) {
|
private getLiabilities({
|
||||||
|
activities,
|
||||||
|
userCurrency
|
||||||
|
}: {
|
||||||
|
activities: OrderWithAccount[];
|
||||||
|
userCurrency: string;
|
||||||
|
}) {
|
||||||
return activities
|
return activities
|
||||||
.filter(({ type }) => {
|
.filter(({ type }) => {
|
||||||
return type === TypeOfOrder.LIABILITY;
|
return type === TypeOfOrder.LIABILITY;
|
||||||
@ -1508,7 +1562,7 @@ export class PortfolioService {
|
|||||||
return this.exchangeRateDataService.toCurrency(
|
return this.exchangeRateDataService.toCurrency(
|
||||||
new Big(quantity).mul(unitPrice).toNumber(),
|
new Big(quantity).mul(unitPrice).toNumber(),
|
||||||
SymbolProfile.currency,
|
SymbolProfile.currency,
|
||||||
this.request.user.Settings.settings.baseCurrency
|
userCurrency
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.reduce(
|
.reduce(
|
||||||
@ -1618,7 +1672,10 @@ export class PortfolioService {
|
|||||||
const fees = this.getFees({ activities, userCurrency }).toNumber();
|
const fees = this.getFees({ activities, userCurrency }).toNumber();
|
||||||
const firstOrderDate = activities[0]?.date;
|
const firstOrderDate = activities[0]?.date;
|
||||||
const items = this.getItems(activities).toNumber();
|
const items = this.getItems(activities).toNumber();
|
||||||
const liabilities = this.getLiabilities(activities).toNumber();
|
const liabilities = this.getLiabilities({
|
||||||
|
activities,
|
||||||
|
userCurrency
|
||||||
|
}).toNumber();
|
||||||
|
|
||||||
const totalBuy = this.getTotalByType(activities, userCurrency, 'BUY');
|
const totalBuy = this.getTotalByType(activities, userCurrency, 'BUY');
|
||||||
const totalSell = this.getTotalByType(activities, userCurrency, 'SELL');
|
const totalSell = this.getTotalByType(activities, userCurrency, 'SELL');
|
||||||
@ -1683,7 +1740,16 @@ export class PortfolioService {
|
|||||||
totalBuy,
|
totalBuy,
|
||||||
totalSell,
|
totalSell,
|
||||||
committedFunds: committedFunds.toNumber(),
|
committedFunds: committedFunds.toNumber(),
|
||||||
emergencyFund: emergencyFund.toNumber(),
|
emergencyFund: {
|
||||||
|
assets: emergencyFundPositionsValueInBaseCurrency,
|
||||||
|
cash: emergencyFund
|
||||||
|
.minus(emergencyFundPositionsValueInBaseCurrency)
|
||||||
|
.toNumber(),
|
||||||
|
total: emergencyFund.toNumber()
|
||||||
|
},
|
||||||
|
fireWealth: new Big(performanceInformation.performance.currentValue)
|
||||||
|
.minus(emergencyFundPositionsValueInBaseCurrency)
|
||||||
|
.toNumber(),
|
||||||
ordersCount: activities.filter(({ type }) => {
|
ordersCount: activities.filter(({ type }) => {
|
||||||
return type === 'BUY' || type === 'SELL';
|
return type === 'BUY' || type === 'SELL';
|
||||||
}).length
|
}).length
|
||||||
@ -1735,6 +1801,7 @@ export class PortfolioService {
|
|||||||
name: order.SymbolProfile?.name,
|
name: order.SymbolProfile?.name,
|
||||||
quantity: new Big(order.quantity),
|
quantity: new Big(order.quantity),
|
||||||
symbol: order.SymbolProfile.symbol,
|
symbol: order.SymbolProfile.symbol,
|
||||||
|
tags: order.tags,
|
||||||
type: order.type,
|
type: order.type,
|
||||||
unitPrice: new Big(
|
unitPrice: new Big(
|
||||||
this.exchangeRateDataService.toCurrency(
|
this.exchangeRateDataService.toCurrency(
|
||||||
@ -1775,12 +1842,12 @@ export class PortfolioService {
|
|||||||
userId: string;
|
userId: string;
|
||||||
withExcludedAccounts?: boolean;
|
withExcludedAccounts?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const ordersOfTypeItem = await this.orderService.getOrders({
|
const ordersOfTypeItemOrLiability = await this.orderService.getOrders({
|
||||||
filters,
|
filters,
|
||||||
userCurrency,
|
userCurrency,
|
||||||
userId,
|
userId,
|
||||||
withExcludedAccounts,
|
withExcludedAccounts,
|
||||||
types: ['ITEM']
|
types: ['ITEM', 'LIABILITY']
|
||||||
});
|
});
|
||||||
|
|
||||||
const accounts: PortfolioDetails['accounts'] = {};
|
const accounts: PortfolioDetails['accounts'] = {};
|
||||||
@ -1820,13 +1887,14 @@ export class PortfolioService {
|
|||||||
return accountId === account.id;
|
return accountId === account.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
const ordersOfTypeItemByAccount = ordersOfTypeItem.filter(
|
const ordersOfTypeItemOrLiabilityByAccount =
|
||||||
({ accountId }) => {
|
ordersOfTypeItemOrLiability.filter(({ accountId }) => {
|
||||||
return accountId === account.id;
|
return accountId === account.id;
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
ordersByAccount = ordersByAccount.concat(ordersOfTypeItemByAccount);
|
ordersByAccount = ordersByAccount.concat(
|
||||||
|
ordersOfTypeItemOrLiabilityByAccount
|
||||||
|
);
|
||||||
|
|
||||||
accounts[account.id] = {
|
accounts[account.id] = {
|
||||||
balance: account.balance,
|
balance: account.balance,
|
||||||
@ -1866,7 +1934,7 @@ export class PortfolioService {
|
|||||||
order.unitPrice ??
|
order.unitPrice ??
|
||||||
0);
|
0);
|
||||||
|
|
||||||
if (order.type === 'SELL') {
|
if (order.type === 'LIABILITY' || order.type === 'SELL') {
|
||||||
currentValueOfSymbolInBaseCurrency *= -1;
|
currentValueOfSymbolInBaseCurrency *= -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
import { Cache } from 'cache-manager';
|
||||||
|
|
||||||
|
import type { RedisStore } from './redis-store.interface';
|
||||||
|
|
||||||
|
export interface RedisCache extends Cache {
|
||||||
|
store: RedisStore;
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
import { Store } from 'cache-manager';
|
||||||
|
import { RedisClient } from 'redis';
|
||||||
|
|
||||||
|
export interface RedisStore extends Store {
|
||||||
|
getClient: () => RedisClient;
|
||||||
|
isCacheableValue: (value: any) => boolean;
|
||||||
|
name: 'redis';
|
||||||
|
}
|
@ -1,21 +1,29 @@
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
|
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
|
||||||
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
import { CACHE_MANAGER, Inject, Injectable } from '@nestjs/common';
|
import { CACHE_MANAGER, Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
import { Cache } from 'cache-manager';
|
|
||||||
|
import type { RedisCache } from './interfaces/redis-cache.interface';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RedisCacheService {
|
export class RedisCacheService {
|
||||||
public constructor(
|
public constructor(
|
||||||
@Inject(CACHE_MANAGER) private readonly cache: Cache,
|
@Inject(CACHE_MANAGER) private readonly cache: RedisCache,
|
||||||
private readonly configurationService: ConfigurationService
|
private readonly configurationService: ConfigurationService
|
||||||
) {}
|
) {
|
||||||
|
const client = cache.store.getClient();
|
||||||
|
|
||||||
|
client.on('error', (error) => {
|
||||||
|
Logger.error(error, 'RedisCacheService');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public async get(key: string): Promise<string> {
|
public async get(key: string): Promise<string> {
|
||||||
return await this.cache.get(key);
|
return await this.cache.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getQuoteKey({ dataSource, symbol }: UniqueAsset) {
|
public getQuoteKey({ dataSource, symbol }: UniqueAsset) {
|
||||||
return `quote-${dataSource}-${symbol}`;
|
return `quote-${getAssetProfileIdentifier({ dataSource, symbol })}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async remove(key: string) {
|
public async remove(key: string) {
|
||||||
|
@ -27,9 +27,9 @@ export class SymbolService {
|
|||||||
dataGatheringItem: IDataGatheringItem;
|
dataGatheringItem: IDataGatheringItem;
|
||||||
includeHistoricalData?: number;
|
includeHistoricalData?: number;
|
||||||
}): Promise<SymbolItem> {
|
}): Promise<SymbolItem> {
|
||||||
const quotes = await this.dataProviderService.getQuotes([
|
const quotes = await this.dataProviderService.getQuotes({
|
||||||
dataGatheringItem
|
items: [dataGatheringItem]
|
||||||
]);
|
});
|
||||||
const { currency, marketPrice } = quotes[dataGatheringItem.symbol] ?? {};
|
const { currency, marketPrice } = quotes[dataGatheringItem.symbol] ?? {};
|
||||||
|
|
||||||
if (dataGatheringItem.dataSource && marketPrice >= 0) {
|
if (dataGatheringItem.dataSource && marketPrice >= 0) {
|
||||||
|
@ -14,6 +14,7 @@ import {
|
|||||||
import { UserWithSettings } from '@ghostfolio/common/types';
|
import { UserWithSettings } from '@ghostfolio/common/types';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Prisma, Role, User } from '@prisma/client';
|
import { Prisma, Role, User } from '@prisma/client';
|
||||||
|
import { differenceInDays } from 'date-fns';
|
||||||
import { sortBy } from 'lodash';
|
import { sortBy } from 'lodash';
|
||||||
|
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
@ -123,7 +124,7 @@ export class UserService {
|
|||||||
id,
|
id,
|
||||||
provider,
|
provider,
|
||||||
role,
|
role,
|
||||||
Settings,
|
Settings: Settings as UserWithSettings['Settings'],
|
||||||
thirdPartyId,
|
thirdPartyId,
|
||||||
updatedAt,
|
updatedAt,
|
||||||
activityCount: Analytics?.activityCount
|
activityCount: Analytics?.activityCount
|
||||||
@ -165,11 +166,26 @@ export class UserService {
|
|||||||
user.subscription =
|
user.subscription =
|
||||||
this.subscriptionService.getSubscription(Subscription);
|
this.subscriptionService.getSubscription(Subscription);
|
||||||
|
|
||||||
if (
|
if (user.subscription?.type === 'Basic') {
|
||||||
Analytics?.activityCount % 10 === 0 &&
|
const daysSinceRegistration = differenceInDays(
|
||||||
user.subscription?.type === 'Basic'
|
new Date(),
|
||||||
) {
|
user.createdAt
|
||||||
currentPermissions.push(permissions.enableSubscriptionInterstitial);
|
);
|
||||||
|
let frequency = 20;
|
||||||
|
|
||||||
|
if (daysSinceRegistration > 180) {
|
||||||
|
frequency = 3;
|
||||||
|
} else if (daysSinceRegistration > 60) {
|
||||||
|
frequency = 5;
|
||||||
|
} else if (daysSinceRegistration > 30) {
|
||||||
|
frequency = 10;
|
||||||
|
} else if (daysSinceRegistration > 15) {
|
||||||
|
frequency = 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Analytics?.activityCount % frequency === 1) {
|
||||||
|
currentPermissions.push(permissions.enableSubscriptionInterstitial);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.subscription?.type === 'Premium') {
|
if (user.subscription?.type === 'Premium') {
|
||||||
|
1
apps/api/src/assets/countries/asia-pacific-markets.json
Normal file
1
apps/api/src/assets/countries/asia-pacific-markets.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
["AU", "HK", "NZ", "SG"]
|
19
apps/api/src/assets/countries/europe-markets.json
Normal file
19
apps/api/src/assets/countries/europe-markets.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
[
|
||||||
|
"AT",
|
||||||
|
"BE",
|
||||||
|
"CH",
|
||||||
|
"DE",
|
||||||
|
"DK",
|
||||||
|
"ES",
|
||||||
|
"FI",
|
||||||
|
"FR",
|
||||||
|
"GB",
|
||||||
|
"IE",
|
||||||
|
"IL",
|
||||||
|
"IT",
|
||||||
|
"LU",
|
||||||
|
"NL",
|
||||||
|
"NO",
|
||||||
|
"PT",
|
||||||
|
"SE"
|
||||||
|
]
|
519
apps/api/src/assets/sitemap.xml
Normal file
519
apps/api/src/assets/sitemap.xml
Normal file
@ -0,0 +1,519 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset
|
||||||
|
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
|
||||||
|
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/blog</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/blog/2021/07/hallo-ghostfolio</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/features</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/haeufig-gestellte-fragen</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/maerkte</loc>
|
||||||
|
<changefreq>daily</changefreq>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/open</loc>
|
||||||
|
<changefreq>daily</changefreq>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/preise</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/registrierung</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ueber-uns</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ueber-uns/changelog</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ueber-uns/datenschutzbestimmungen</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ueber-uns/lizenz</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/about</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/about/changelog</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/about/license</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/blog</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/blog/2021/07/hello-ghostfolio</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/blog/2022/01/ghostfolio-first-months-in-open-source</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/blog/2022/07/ghostfolio-meets-internet-identity</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/blog/2022/07/how-do-i-get-my-finances-in-order</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/blog/2022/08/500-stars-on-github</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/blog/2022/10/hacktoberfest-2022</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/blog/2022/11/black-friday-2022</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/blog/2022/12/the-importance-of-tracking-your-personal-finances</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/blog/2023/02/ghostfolio-meets-umbrel</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/blog/2023/03/ghostfolio-reaches-1000-stars-on-github</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/blog/2023/05/unlock-your-financial-potential-with-ghostfolio</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/blog/2023/07/exploring-the-path-to-fire</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/faq</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/features</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/markets</loc>
|
||||||
|
<changefreq>daily</changefreq>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/open</loc>
|
||||||
|
<changefreq>daily</changefreq>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/pricing</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/register</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-altoo</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-copilot-money</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-delta</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-divvydiary</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-exirio</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-folishare</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-getquin</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-gospatz</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-justetf</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-kubera</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-markets.sh</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-maybe-finance</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-monse</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-parqet</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-plannix</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-portfolio-dividend-tracker</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-portseido</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-projectionlab</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-seeking-alpha</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-sharesight</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-simple-portfolio</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-snowball-analytics</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-sumio</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-utluna</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-yeekatee</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/es</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/es/funcionalidades</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/es/mercados</loc>
|
||||||
|
<changefreq>daily</changefreq>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/es/open</loc>
|
||||||
|
<changefreq>daily</changefreq>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/es/precios</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/es/preguntas-mas-frecuentes</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/es/recursos</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/es/registro</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/es/sobre</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/es/sobre/changelog</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/es/sobre/licencia</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/es/sobre/politica-de-privacidad</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/fr</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/fr/a-propos</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/fr/a-propos/changelog</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/fr/a-propos/licence</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/fr/a-propos/politique-de-confidentialite</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/fr/enregistrement</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/fr/fonctionnalites</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/fr/foire-aux-questions</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/fr/marches</loc>
|
||||||
|
<changefreq>daily</changefreq>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/fr/open</loc>
|
||||||
|
<changefreq>daily</changefreq>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/fr/prix</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/fr/ressources</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/domande-piu-frequenti</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/funzionalita</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/informazioni-su</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/informazioni-su/changelog</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/informazioni-su/licenza</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/informazioni-su/informativa-sulla-privacy</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/iscrizione</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/mercati</loc>
|
||||||
|
<changefreq>daily</changefreq>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/open</loc>
|
||||||
|
<changefreq>daily</changefreq>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/prezzi</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/bronnen</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/kenmerken</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/markten</loc>
|
||||||
|
<changefreq>daily</changefreq>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/open</loc>
|
||||||
|
<changefreq>daily</changefreq>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/over</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/over/changelog</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/over/licentie</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/over/privacybeleid</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/prijzen</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/registratie</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/vaak-gestelde-vragen</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/pt/blog</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/pt/funcionalidades</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/pt/mercados</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/pt/open</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/pt/perguntas-mais-frequentes</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/pt/precos</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/pt/recursos</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/pt/registo</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/pt/sobre</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/pt/sobre/changelog</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/pt/sobre/licenca</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/pt/sobre/politica-de-privacidade</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
</urlset>
|
@ -0,0 +1,10 @@
|
|||||||
|
import { AccountBalanceService } from '@ghostfolio/api/services/account-balance/account-balance.service';
|
||||||
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
exports: [AccountBalanceService],
|
||||||
|
imports: [PrismaModule],
|
||||||
|
providers: [AccountBalanceService]
|
||||||
|
})
|
||||||
|
export class AccountBalanceModule {}
|
@ -0,0 +1,16 @@
|
|||||||
|
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { AccountBalance, Prisma } from '@prisma/client';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AccountBalanceService {
|
||||||
|
public constructor(private readonly prismaService: PrismaService) {}
|
||||||
|
|
||||||
|
public async createAccountBalance(
|
||||||
|
data: Prisma.AccountBalanceCreateInput
|
||||||
|
): Promise<AccountBalance> {
|
||||||
|
return this.prismaService.accountBalance.create({
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ import {
|
|||||||
GATHER_ASSET_PROFILE_PROCESS,
|
GATHER_ASSET_PROFILE_PROCESS,
|
||||||
GATHER_ASSET_PROFILE_PROCESS_OPTIONS
|
GATHER_ASSET_PROFILE_PROCESS_OPTIONS
|
||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
|
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||||
|
|
||||||
@ -48,7 +49,7 @@ export class CronService {
|
|||||||
name: GATHER_ASSET_PROFILE_PROCESS,
|
name: GATHER_ASSET_PROFILE_PROCESS,
|
||||||
opts: {
|
opts: {
|
||||||
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
|
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
|
||||||
jobId: `${dataSource}-${symbol}`
|
jobId: getAssetProfileIdentifier({ dataSource, symbol })
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
@ -10,7 +10,11 @@ import {
|
|||||||
GATHER_HISTORICAL_MARKET_DATA_PROCESS,
|
GATHER_HISTORICAL_MARKET_DATA_PROCESS,
|
||||||
GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS
|
GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS
|
||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT, resetHours } from '@ghostfolio/common/helper';
|
import {
|
||||||
|
DATE_FORMAT,
|
||||||
|
getAssetProfileIdentifier,
|
||||||
|
resetHours
|
||||||
|
} from '@ghostfolio/common/helper';
|
||||||
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
import { InjectQueue } from '@nestjs/bull';
|
import { InjectQueue } from '@nestjs/bull';
|
||||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
@ -221,7 +225,10 @@ export class DataGatheringService {
|
|||||||
name: GATHER_HISTORICAL_MARKET_DATA_PROCESS,
|
name: GATHER_HISTORICAL_MARKET_DATA_PROCESS,
|
||||||
opts: {
|
opts: {
|
||||||
...GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS,
|
...GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS,
|
||||||
jobId: `${dataSource}-${symbol}-${format(date, DATE_FORMAT)}`
|
jobId: `${getAssetProfileIdentifier({
|
||||||
|
dataSource,
|
||||||
|
symbol
|
||||||
|
})}-${format(date, DATE_FORMAT)}`
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
@ -135,6 +135,8 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
|
|||||||
let name = longName;
|
let name = longName;
|
||||||
|
|
||||||
if (name) {
|
if (name) {
|
||||||
|
name = name.replace('&', '&');
|
||||||
|
|
||||||
name = name.replace('Amundi Index Solutions - ', '');
|
name = name.replace('Amundi Index Solutions - ', '');
|
||||||
name = name.replace('iShares ETF (CH) - ', '');
|
name = name.replace('iShares ETF (CH) - ', '');
|
||||||
name = name.replace('iShares III Public Limited Company - ', '');
|
name = name.replace('iShares III Public Limited Company - ', '');
|
||||||
|
@ -3,7 +3,6 @@ import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.in
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
|
import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
|
||||||
import {
|
import {
|
||||||
IDataGatheringItem,
|
|
||||||
IDataProviderHistoricalResponse,
|
IDataProviderHistoricalResponse,
|
||||||
IDataProviderResponse
|
IDataProviderResponse
|
||||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
@ -12,6 +11,7 @@ import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
|||||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||||
import { PROPERTY_DATA_SOURCE_MAPPING } from '@ghostfolio/common/config';
|
import { PROPERTY_DATA_SOURCE_MAPPING } from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT, getStartOfUtcDate } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, getStartOfUtcDate } from '@ghostfolio/common/helper';
|
||||||
|
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
import type { Granularity, UserWithSettings } from '@ghostfolio/common/types';
|
import type { Granularity, UserWithSettings } from '@ghostfolio/common/types';
|
||||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
import { DataSource, MarketData, SymbolProfile } from '@prisma/client';
|
import { DataSource, MarketData, SymbolProfile } from '@prisma/client';
|
||||||
@ -45,12 +45,15 @@ export class DataProviderService {
|
|||||||
const dataProvider = this.getDataProvider(dataSource);
|
const dataProvider = this.getDataProvider(dataSource);
|
||||||
const symbol = dataProvider.getTestSymbol();
|
const symbol = dataProvider.getTestSymbol();
|
||||||
|
|
||||||
const quotes = await this.getQuotes([
|
const quotes = await this.getQuotes({
|
||||||
{
|
items: [
|
||||||
dataSource,
|
{
|
||||||
symbol
|
dataSource,
|
||||||
}
|
symbol
|
||||||
]);
|
}
|
||||||
|
],
|
||||||
|
useCache: false
|
||||||
|
});
|
||||||
|
|
||||||
if (quotes[symbol]?.marketPrice > 0) {
|
if (quotes[symbol]?.marketPrice > 0) {
|
||||||
return true;
|
return true;
|
||||||
@ -59,14 +62,16 @@ export class DataProviderService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAssetProfiles(items: IDataGatheringItem[]): Promise<{
|
public async getAssetProfiles(items: UniqueAsset[]): Promise<{
|
||||||
[symbol: string]: Partial<SymbolProfile>;
|
[symbol: string]: Partial<SymbolProfile>;
|
||||||
}> {
|
}> {
|
||||||
const response: {
|
const response: {
|
||||||
[symbol: string]: Partial<SymbolProfile>;
|
[symbol: string]: Partial<SymbolProfile>;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
const itemsGroupedByDataSource = groupBy(items, (item) => item.dataSource);
|
const itemsGroupedByDataSource = groupBy(items, ({ dataSource }) => {
|
||||||
|
return dataSource;
|
||||||
|
});
|
||||||
|
|
||||||
const promises = [];
|
const promises = [];
|
||||||
|
|
||||||
@ -127,7 +132,7 @@ export class DataProviderService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getHistorical(
|
public async getHistorical(
|
||||||
aItems: IDataGatheringItem[],
|
aItems: UniqueAsset[],
|
||||||
aGranularity: Granularity = 'month',
|
aGranularity: Granularity = 'month',
|
||||||
from: Date,
|
from: Date,
|
||||||
to: Date
|
to: Date
|
||||||
@ -155,11 +160,11 @@ export class DataProviderService {
|
|||||||
)}'`
|
)}'`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
const dataSources = aItems.map((item) => {
|
const dataSources = aItems.map(({ dataSource }) => {
|
||||||
return item.dataSource;
|
return dataSource;
|
||||||
});
|
});
|
||||||
const symbols = aItems.map((item) => {
|
const symbols = aItems.map(({ symbol }) => {
|
||||||
return item.symbol;
|
return symbol;
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -192,7 +197,7 @@ export class DataProviderService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getHistoricalRaw(
|
public async getHistoricalRaw(
|
||||||
aDataGatheringItems: IDataGatheringItem[],
|
aDataGatheringItems: UniqueAsset[],
|
||||||
from: Date,
|
from: Date,
|
||||||
to: Date
|
to: Date
|
||||||
): Promise<{
|
): Promise<{
|
||||||
@ -229,7 +234,13 @@ export class DataProviderService {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getQuotes(items: IDataGatheringItem[]): Promise<{
|
public async getQuotes({
|
||||||
|
items,
|
||||||
|
useCache = true
|
||||||
|
}: {
|
||||||
|
items: UniqueAsset[];
|
||||||
|
useCache?: boolean;
|
||||||
|
}): Promise<{
|
||||||
[symbol: string]: IDataProviderResponse;
|
[symbol: string]: IDataProviderResponse;
|
||||||
}> {
|
}> {
|
||||||
const response: {
|
const response: {
|
||||||
@ -238,23 +249,24 @@ export class DataProviderService {
|
|||||||
const startTimeTotal = performance.now();
|
const startTimeTotal = performance.now();
|
||||||
|
|
||||||
// Get items from cache
|
// Get items from cache
|
||||||
const itemsToFetch: IDataGatheringItem[] = [];
|
const itemsToFetch: UniqueAsset[] = [];
|
||||||
|
|
||||||
for (const { dataSource, symbol } of items) {
|
for (const { dataSource, symbol } of items) {
|
||||||
const quoteString = await this.redisCacheService.get(
|
if (useCache) {
|
||||||
this.redisCacheService.getQuoteKey({ dataSource, symbol })
|
const quoteString = await this.redisCacheService.get(
|
||||||
);
|
this.redisCacheService.getQuoteKey({ dataSource, symbol })
|
||||||
|
);
|
||||||
|
|
||||||
if (quoteString) {
|
if (quoteString) {
|
||||||
try {
|
try {
|
||||||
const cachedDataProviderResponse = JSON.parse(quoteString);
|
const cachedDataProviderResponse = JSON.parse(quoteString);
|
||||||
response[symbol] = cachedDataProviderResponse;
|
response[symbol] = cachedDataProviderResponse;
|
||||||
} catch {}
|
continue;
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!quoteString) {
|
itemsToFetch.push({ dataSource, symbol });
|
||||||
itemsToFetch.push({ dataSource, symbol });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const numberOfItemsInCache = Object.keys(response)?.length;
|
const numberOfItemsInCache = Object.keys(response)?.length;
|
||||||
|
@ -64,11 +64,11 @@ export class ExchangeRateDataService {
|
|||||||
if (Object.keys(result).length !== this.currencyPairs.length) {
|
if (Object.keys(result).length !== this.currencyPairs.length) {
|
||||||
// Load currencies directly from data provider as a fallback
|
// Load currencies directly from data provider as a fallback
|
||||||
// if historical data is not fully available
|
// if historical data is not fully available
|
||||||
const quotes = await this.dataProviderService.getQuotes(
|
const quotes = await this.dataProviderService.getQuotes({
|
||||||
this.currencyPairs.map(({ dataSource, symbol }) => {
|
items: this.currencyPairs.map(({ dataSource, symbol }) => {
|
||||||
return { dataSource, symbol };
|
return { dataSource, symbol };
|
||||||
})
|
})
|
||||||
);
|
});
|
||||||
|
|
||||||
for (const symbol of Object.keys(quotes)) {
|
for (const symbol of Object.keys(quotes)) {
|
||||||
if (isNumber(quotes[symbol].marketPrice)) {
|
if (isNumber(quotes[symbol].marketPrice)) {
|
||||||
@ -125,9 +125,11 @@ export class ExchangeRateDataService {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let factor = 1;
|
let factor: number;
|
||||||
|
|
||||||
if (aFromCurrency !== aToCurrency) {
|
if (aFromCurrency === aToCurrency) {
|
||||||
|
factor = 1;
|
||||||
|
} else {
|
||||||
if (this.exchangeRates[`${aFromCurrency}${aToCurrency}`]) {
|
if (this.exchangeRates[`${aFromCurrency}${aToCurrency}`]) {
|
||||||
factor = this.exchangeRates[`${aFromCurrency}${aToCurrency}`];
|
factor = this.exchangeRates[`${aFromCurrency}${aToCurrency}`];
|
||||||
} else {
|
} else {
|
||||||
@ -171,7 +173,9 @@ export class ExchangeRateDataService {
|
|||||||
|
|
||||||
let factor: number;
|
let factor: number;
|
||||||
|
|
||||||
if (aFromCurrency !== aToCurrency) {
|
if (aFromCurrency === aToCurrency) {
|
||||||
|
factor = 1;
|
||||||
|
} else {
|
||||||
const dataSource =
|
const dataSource =
|
||||||
this.dataProviderService.getDataSourceForExchangeRates();
|
this.dataProviderService.getDataSourceForExchangeRates();
|
||||||
const symbol = `${aFromCurrency}${aToCurrency}`;
|
const symbol = `${aFromCurrency}${aToCurrency}`;
|
||||||
|
@ -29,6 +29,11 @@
|
|||||||
"input": "",
|
"input": "",
|
||||||
"output": "./../assets"
|
"output": "./../assets"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"glob": "favicon.ico",
|
||||||
|
"input": "apps/client/src/assets",
|
||||||
|
"output": "./../"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"glob": "LICENSE",
|
"glob": "LICENSE",
|
||||||
"input": "",
|
"input": "",
|
||||||
|
@ -47,104 +47,6 @@ const routes: Routes = [
|
|||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./pages/blog/blog-page.module').then((m) => m.BlogPageModule)
|
import('./pages/blog/blog-page.module').then((m) => m.BlogPageModule)
|
||||||
})),
|
})),
|
||||||
{
|
|
||||||
path: 'blog/2021/07/hallo-ghostfolio',
|
|
||||||
loadChildren: () =>
|
|
||||||
import(
|
|
||||||
'./pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.module'
|
|
||||||
).then((m) => m.HalloGhostfolioPageModule)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'blog/2021/07/hello-ghostfolio',
|
|
||||||
loadChildren: () =>
|
|
||||||
import(
|
|
||||||
'./pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.module'
|
|
||||||
).then((m) => m.HelloGhostfolioPageModule)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'blog/2022/01/ghostfolio-first-months-in-open-source',
|
|
||||||
loadChildren: () =>
|
|
||||||
import(
|
|
||||||
'./pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.module'
|
|
||||||
).then((m) => m.FirstMonthsInOpenSourcePageModule)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'blog/2022/07/ghostfolio-meets-internet-identity',
|
|
||||||
loadChildren: () =>
|
|
||||||
import(
|
|
||||||
'./pages/blog/2022/07/ghostfolio-meets-internet-identity/ghostfolio-meets-internet-identity-page.module'
|
|
||||||
).then((m) => m.GhostfolioMeetsInternetIdentityPageModule)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'blog/2022/07/how-do-i-get-my-finances-in-order',
|
|
||||||
loadChildren: () =>
|
|
||||||
import(
|
|
||||||
'./pages/blog/2022/07/how-do-i-get-my-finances-in-order/how-do-i-get-my-finances-in-order-page.module'
|
|
||||||
).then((m) => m.HowDoIGetMyFinancesInOrderPageModule)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'blog/2022/08/500-stars-on-github',
|
|
||||||
loadChildren: () =>
|
|
||||||
import(
|
|
||||||
'./pages/blog/2022/08/500-stars-on-github/500-stars-on-github-page.module'
|
|
||||||
).then((m) => m.FiveHundredStarsOnGitHubPageModule)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'blog/2022/10/hacktoberfest-2022',
|
|
||||||
loadChildren: () =>
|
|
||||||
import(
|
|
||||||
'./pages/blog/2022/10/hacktoberfest-2022/hacktoberfest-2022-page.module'
|
|
||||||
).then((m) => m.Hacktoberfest2022PageModule)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'blog/2022/11/black-friday-2022',
|
|
||||||
loadChildren: () =>
|
|
||||||
import(
|
|
||||||
'./pages/blog/2022/11/black-friday-2022/black-friday-2022-page.module'
|
|
||||||
).then((m) => m.BlackFriday2022PageModule)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'blog/2022/12/the-importance-of-tracking-your-personal-finances',
|
|
||||||
loadChildren: () =>
|
|
||||||
import(
|
|
||||||
'./pages/blog/2022/12/the-importance-of-tracking-your-personal-finances/the-importance-of-tracking-your-personal-finances-page.module'
|
|
||||||
).then((m) => m.TheImportanceOfTrackingYourPersonalFinancesPageModule)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt',
|
|
||||||
loadChildren: () =>
|
|
||||||
import(
|
|
||||||
'./pages/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt/ghostfolio-auf-sackgeld-vorgestellt-page.module'
|
|
||||||
).then((m) => m.GhostfolioAufSackgeldVorgestelltPageModule)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'blog/2023/02/ghostfolio-meets-umbrel',
|
|
||||||
loadChildren: () =>
|
|
||||||
import(
|
|
||||||
'./pages/blog/2023/02/ghostfolio-meets-umbrel/ghostfolio-meets-umbrel-page.module'
|
|
||||||
).then((m) => m.GhostfolioMeetsUmbrelPageModule)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'blog/2023/03/ghostfolio-reaches-1000-stars-on-github',
|
|
||||||
loadChildren: () =>
|
|
||||||
import(
|
|
||||||
'./pages/blog/2023/03/1000-stars-on-github/1000-stars-on-github-page.module'
|
|
||||||
).then((m) => m.ThousandStarsOnGitHubPageModule)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'blog/2023/05/unlock-your-financial-potential-with-ghostfolio',
|
|
||||||
loadChildren: () =>
|
|
||||||
import(
|
|
||||||
'./pages/blog/2023/05/unlock-your-financial-potential-with-ghostfolio/unlock-your-financial-potential-with-ghostfolio-page.module'
|
|
||||||
).then((m) => m.UnlockYourFinancialPotentialWithGhostfolioPageModule)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'blog/2023/07/exploring-the-path-to-fire',
|
|
||||||
loadChildren: () =>
|
|
||||||
import(
|
|
||||||
'./pages/blog/2023/07/exploring-the-path-to-fire/exploring-the-path-to-fire-page.module'
|
|
||||||
).then((m) => m.ExploringThePathToFirePageModule)
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'demo',
|
path: 'demo',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
|
@ -8,11 +8,13 @@ import {
|
|||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { MatPaginator, PageEvent } from '@angular/material/paginator';
|
||||||
import { MatSort, Sort } from '@angular/material/sort';
|
import { MatSort, Sort } from '@angular/material/sort';
|
||||||
import { MatTableDataSource } from '@angular/material/table';
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
|
import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
|
||||||
import { getDateFormatString } from '@ghostfolio/common/helper';
|
import { getDateFormatString } from '@ghostfolio/common/helper';
|
||||||
import { Filter, UniqueAsset, User } from '@ghostfolio/common/interfaces';
|
import { Filter, UniqueAsset, User } from '@ghostfolio/common/interfaces';
|
||||||
import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface';
|
import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface';
|
||||||
@ -26,8 +28,6 @@ import { AssetProfileDialog } from './asset-profile-dialog/asset-profile-dialog.
|
|||||||
import { AssetProfileDialogParams } from './asset-profile-dialog/interfaces/interfaces';
|
import { AssetProfileDialogParams } from './asset-profile-dialog/interfaces/interfaces';
|
||||||
import { CreateAssetProfileDialog } from './create-asset-profile-dialog/create-asset-profile-dialog.component';
|
import { CreateAssetProfileDialog } from './create-asset-profile-dialog/create-asset-profile-dialog.component';
|
||||||
import { CreateAssetProfileDialogParams } from './create-asset-profile-dialog/interfaces/interfaces';
|
import { CreateAssetProfileDialogParams } from './create-asset-profile-dialog/interfaces/interfaces';
|
||||||
import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
|
|
||||||
import { MatPaginator, PageEvent } from '@angular/material/paginator';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
@ -51,13 +51,26 @@ export class AdminMarketDataComponent
|
|||||||
AssetSubClass.PRECIOUS_METAL,
|
AssetSubClass.PRECIOUS_METAL,
|
||||||
AssetSubClass.PRIVATE_EQUITY,
|
AssetSubClass.PRIVATE_EQUITY,
|
||||||
AssetSubClass.STOCK
|
AssetSubClass.STOCK
|
||||||
].map((assetSubClass) => {
|
]
|
||||||
return {
|
.map((assetSubClass) => {
|
||||||
id: assetSubClass,
|
return {
|
||||||
label: translate(assetSubClass),
|
id: assetSubClass.toString(),
|
||||||
type: 'ASSET_SUB_CLASS'
|
label: translate(assetSubClass),
|
||||||
};
|
type: <Filter['type']>'ASSET_SUB_CLASS'
|
||||||
});
|
};
|
||||||
|
})
|
||||||
|
.concat([
|
||||||
|
{
|
||||||
|
id: 'ETF_WITHOUT_COUNTRIES',
|
||||||
|
label: $localize`ETFs without Countries`,
|
||||||
|
type: <Filter['type']>'PRESET_ID'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ETF_WITHOUT_SECTORS',
|
||||||
|
label: $localize`ETFs without Sectors`,
|
||||||
|
type: <Filter['type']>'PRESET_ID'
|
||||||
|
}
|
||||||
|
]);
|
||||||
public currentDataSource: DataSource;
|
public currentDataSource: DataSource;
|
||||||
public currentSymbol: string;
|
public currentSymbol: string;
|
||||||
public dataSource: MatTableDataSource<AdminMarketDataItem> =
|
public dataSource: MatTableDataSource<AdminMarketDataItem> =
|
||||||
@ -237,6 +250,12 @@ export class AdminMarketDataComponent
|
|||||||
) {
|
) {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
|
||||||
|
this.pageSize =
|
||||||
|
this.activeFilters.length === 1 &&
|
||||||
|
this.activeFilters[0].type === 'PRESET_ID'
|
||||||
|
? undefined
|
||||||
|
: DEFAULT_PAGE_SIZE;
|
||||||
|
|
||||||
if (pageIndex === 0 && this.paginator) {
|
if (pageIndex === 0 && this.paginator) {
|
||||||
this.paginator.pageIndex = 0;
|
this.paginator.pageIndex = 0;
|
||||||
}
|
}
|
||||||
|
@ -1,57 +1,110 @@
|
|||||||
<div
|
<div
|
||||||
class="align-items-center container d-flex flex-column h-100 justify-content-center overview p-0 position-relative"
|
class="align-items-center container d-flex flex-column h-100 justify-content-center overview p-0 position-relative"
|
||||||
>
|
>
|
||||||
<div class="row w-100">
|
<div
|
||||||
<div class="col p-0">
|
*ngIf="hasPermissionToCreateOrder && historicalDataItems?.length === 0; else isUserActive"
|
||||||
<div class="chart-container mx-auto position-relative">
|
class="justify-content-center row w-100"
|
||||||
<div
|
>
|
||||||
*ngIf="hasPermissionToCreateOrder && historicalDataItems?.length === 0"
|
<div class="col introduction">
|
||||||
class="align-items-center d-flex h-100 justify-content-center w-100"
|
<h4 i18n>Welcome to Ghostfolio</h4>
|
||||||
|
<p i18n>Ready to take control of your personal finances?</p>
|
||||||
|
<ol class="font-weight-bold">
|
||||||
|
<li
|
||||||
|
class="mb-2"
|
||||||
|
[ngClass]="{ 'text-muted': user?.accounts?.length > 1 }"
|
||||||
>
|
>
|
||||||
<div class="d-flex justify-content-center">
|
<a class="d-block" [routerLink]="['/accounts']"
|
||||||
<gf-no-transactions-info-indicator></gf-no-transactions-info-indicator>
|
><span i18n>Setup your accounts</span><br />
|
||||||
</div>
|
<span class="font-weight-normal" i18n
|
||||||
|
>Get a comprehensive financial overview by adding your bank and
|
||||||
|
brokerage accounts.</span
|
||||||
|
></a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<a class="d-block" [routerLink]="['/portfolio', 'activities']">
|
||||||
|
<span i18n>Capture your activities</span><br />
|
||||||
|
<span class="font-weight-normal" i18n
|
||||||
|
>Record your investment activities to keep your portfolio up to
|
||||||
|
date.</span
|
||||||
|
></a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<a class="d-block" [routerLink]="['/portfolio']">
|
||||||
|
<span i18n>Monitor and analyze your portfolio</span><br />
|
||||||
|
<span class="font-weight-normal" i18n
|
||||||
|
>Track your progress in real-time with comprehensive analysis and
|
||||||
|
insights.</span
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
<div class="d-flex justify-content-center">
|
||||||
|
<a
|
||||||
|
*ngIf="user?.accounts?.length === 1"
|
||||||
|
color="primary"
|
||||||
|
mat-flat-button
|
||||||
|
[routerLink]="['/accounts']"
|
||||||
|
>
|
||||||
|
<ng-container i18n>Setup accounts</ng-container>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
*ngIf="user?.accounts?.length > 1"
|
||||||
|
color="primary"
|
||||||
|
mat-flat-button
|
||||||
|
[routerLink]="['/portfolio', 'activities']"
|
||||||
|
>
|
||||||
|
<ng-container i18n>Add activity</ng-container>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ng-template #isUserActive>
|
||||||
|
<div class="row w-100">
|
||||||
|
<div class="col p-0">
|
||||||
|
<div class="chart-container mx-auto position-relative">
|
||||||
|
<gf-line-chart
|
||||||
|
class="position-absolute"
|
||||||
|
symbol="Performance"
|
||||||
|
unit="%"
|
||||||
|
[colorScheme]="user?.settings?.colorScheme"
|
||||||
|
[hidden]="historicalDataItems?.length === 0"
|
||||||
|
[historicalDataItems]="historicalDataItems"
|
||||||
|
[isAnimated]="user?.settings?.dateRange === '1d' ? false : true"
|
||||||
|
[locale]="user?.settings?.locale"
|
||||||
|
[ngClass]="{ 'pr-3': deviceType === 'mobile' }"
|
||||||
|
[showGradient]="true"
|
||||||
|
[showLoader]="false"
|
||||||
|
[showXAxis]="false"
|
||||||
|
[showYAxis]="false"
|
||||||
|
></gf-line-chart>
|
||||||
</div>
|
</div>
|
||||||
<gf-line-chart
|
|
||||||
class="position-absolute"
|
|
||||||
symbol="Performance"
|
|
||||||
unit="%"
|
|
||||||
[colorScheme]="user?.settings?.colorScheme"
|
|
||||||
[hidden]="historicalDataItems?.length === 0"
|
|
||||||
[historicalDataItems]="historicalDataItems"
|
|
||||||
[isAnimated]="user?.settings?.dateRange === '1d' ? false : true"
|
|
||||||
[locale]="user?.settings?.locale"
|
|
||||||
[ngClass]="{ 'pr-3': deviceType === 'mobile' }"
|
|
||||||
[showGradient]="true"
|
|
||||||
[showLoader]="false"
|
|
||||||
[showXAxis]="false"
|
|
||||||
[showYAxis]="false"
|
|
||||||
></gf-line-chart>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="overview-container row mt-1">
|
||||||
<div class="overview-container row mt-1">
|
<div class="col">
|
||||||
<div class="col">
|
<gf-portfolio-performance
|
||||||
<gf-portfolio-performance
|
class="pb-4"
|
||||||
class="pb-4"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[deviceType]="deviceType"
|
||||||
[deviceType]="deviceType"
|
[errors]="errors"
|
||||||
[errors]="errors"
|
[isAllTimeHigh]="isAllTimeHigh"
|
||||||
[isAllTimeHigh]="isAllTimeHigh"
|
[isAllTimeLow]="isAllTimeLow"
|
||||||
[isAllTimeLow]="isAllTimeLow"
|
|
||||||
[isLoading]="isLoadingPerformance"
|
|
||||||
[locale]="user?.settings?.locale"
|
|
||||||
[performance]="performance"
|
|
||||||
[showDetails]="showDetails"
|
|
||||||
></gf-portfolio-performance>
|
|
||||||
<div *ngIf="showDetails" class="text-center">
|
|
||||||
<gf-toggle
|
|
||||||
[defaultValue]="user?.settings?.dateRange"
|
|
||||||
[isLoading]="isLoadingPerformance"
|
[isLoading]="isLoadingPerformance"
|
||||||
[options]="dateRangeOptions"
|
[locale]="user?.settings?.locale"
|
||||||
(change)="onChangeDateRange($event.value)"
|
[performance]="performance"
|
||||||
></gf-toggle>
|
[showDetails]="showDetails"
|
||||||
|
></gf-portfolio-performance>
|
||||||
|
<div *ngIf="showDetails" class="text-center">
|
||||||
|
<gf-toggle
|
||||||
|
[defaultValue]="user?.settings?.dateRange"
|
||||||
|
[isLoading]="isLoadingPerformance"
|
||||||
|
[options]="dateRangeOptions"
|
||||||
|
(change)="onChangeDateRange($event.value)"
|
||||||
|
></gf-toggle>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ng-template>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { GfPortfolioPerformanceModule } from '@ghostfolio/client/components/portfolio-performance/portfolio-performance.module';
|
import { GfPortfolioPerformanceModule } from '@ghostfolio/client/components/portfolio-performance/portfolio-performance.module';
|
||||||
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
||||||
@ -16,6 +17,7 @@ import { HomeOverviewComponent } from './home-overview.component';
|
|||||||
GfNoTransactionsInfoModule,
|
GfNoTransactionsInfoModule,
|
||||||
GfPortfolioPerformanceModule,
|
GfPortfolioPerformanceModule,
|
||||||
GfToggleModule,
|
GfToggleModule,
|
||||||
|
MatButtonModule,
|
||||||
RouterModule
|
RouterModule
|
||||||
],
|
],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
@ -31,4 +31,8 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.introduction {
|
||||||
|
max-width: 50rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,8 @@ import { TokenStorageService } from '@ghostfolio/client/services/token-storage.s
|
|||||||
templateUrl: 'login-with-access-token-dialog.html'
|
templateUrl: 'login-with-access-token-dialog.html'
|
||||||
})
|
})
|
||||||
export class LoginWithAccessTokenDialog {
|
export class LoginWithAccessTokenDialog {
|
||||||
|
public isAccessTokenHidden = true;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||||
public dialogRef: MatDialogRef<LoginWithAccessTokenDialog>,
|
public dialogRef: MatDialogRef<LoginWithAccessTokenDialog>,
|
||||||
@ -38,6 +40,12 @@ export class LoginWithAccessTokenDialog {
|
|||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onLoginWithAccessToken() {
|
||||||
|
if (this.data.accessToken) {
|
||||||
|
this.dialogRef.close(this.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async onLoginWithInternetIdentity() {
|
public async onLoginWithInternetIdentity() {
|
||||||
try {
|
try {
|
||||||
const { authToken } = await this.internetIdentityService.login();
|
const { authToken } = await this.internetIdentityService.login();
|
||||||
|
@ -6,15 +6,27 @@
|
|||||||
|
|
||||||
<div class="py-3" mat-dialog-content>
|
<div class="py-3" mat-dialog-content>
|
||||||
<div class="align-items-center d-flex flex-column">
|
<div class="align-items-center d-flex flex-column">
|
||||||
<mat-form-field appearance="outline" class="without-hint w-100">
|
<form class="w-100" (ngSubmit)="onLoginWithAccessToken()">
|
||||||
<mat-label i18n>Security Token</mat-label>
|
<mat-form-field appearance="outline" class="without-hint w-100">
|
||||||
<textarea
|
<mat-label i18n>Security Token</mat-label>
|
||||||
cdkTextareaAutosize
|
<input
|
||||||
matInput
|
matInput
|
||||||
type="text"
|
name="password"
|
||||||
[(ngModel)]="data.accessToken"
|
[type]="isAccessTokenHidden ? 'password' : 'text'"
|
||||||
></textarea>
|
[(ngModel)]="data.accessToken"
|
||||||
</mat-form-field>
|
/>
|
||||||
|
<button
|
||||||
|
mat-button
|
||||||
|
matSuffix
|
||||||
|
type="button"
|
||||||
|
(click)="isAccessTokenHidden = !isAccessTokenHidden"
|
||||||
|
>
|
||||||
|
<ion-icon
|
||||||
|
[name]="isAccessTokenHidden ? 'eye-outline' : 'eye-off-outline'"
|
||||||
|
></ion-icon>
|
||||||
|
</button>
|
||||||
|
</mat-form-field>
|
||||||
|
</form>
|
||||||
<ng-container *ngIf="data.hasPermissionToUseSocialLogin">
|
<ng-container *ngIf="data.hasPermissionToUseSocialLogin">
|
||||||
<div class="my-3 text-center text-muted" i18n>or</div>
|
<div class="my-3 text-center text-muted" i18n>or</div>
|
||||||
<div class="d-flex flex-column">
|
<div class="d-flex flex-column">
|
||||||
|
@ -163,7 +163,33 @@
|
|||||||
[isCurrency]="true"
|
[isCurrency]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[unit]="baseCurrency"
|
[unit]="baseCurrency"
|
||||||
[value]="isLoading ? undefined : summary?.emergencyFund"
|
[value]="isLoading ? undefined : summary?.emergencyFund?.total"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-nowrap px-3 py-1 row">
|
||||||
|
<div class="flex-grow-1 ml-3 text-truncate" i18n>Cash</div>
|
||||||
|
<div class="flex-column flex-wrap justify-content-end">
|
||||||
|
<gf-value
|
||||||
|
class="justify-content-end"
|
||||||
|
position="end"
|
||||||
|
[isCurrency]="true"
|
||||||
|
[locale]="locale"
|
||||||
|
[unit]="baseCurrency"
|
||||||
|
[value]="isLoading ? undefined : summary?.emergencyFund?.cash"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-nowrap px-3 py-1 row">
|
||||||
|
<div class="flex-grow-1 ml-3 text-truncate" i18n>Assets</div>
|
||||||
|
<div class="flex-column flex-wrap justify-content-end">
|
||||||
|
<gf-value
|
||||||
|
class="justify-content-end"
|
||||||
|
position="end"
|
||||||
|
[isCurrency]="true"
|
||||||
|
[locale]="locale"
|
||||||
|
[unit]="baseCurrency"
|
||||||
|
[value]="isLoading ? undefined : summary?.emergencyFund?.assets"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -215,6 +215,15 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
|||||||
this.benchmarkDataItems[0].value = this.averagePrice;
|
this.benchmarkDataItems[0].value = this.averagePrice;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.benchmarkDataItems = this.benchmarkDataItems.map(
|
||||||
|
({ date, value }) => {
|
||||||
|
return {
|
||||||
|
date,
|
||||||
|
value: value === 0 ? null : value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (Number.isInteger(this.quantity)) {
|
if (Number.isInteger(this.quantity)) {
|
||||||
this.quantityPrecision = 0;
|
this.quantityPrecision = 0;
|
||||||
} else if (this.SymbolProfile?.assetSubClass === 'CRYPTOCURRENCY') {
|
} else if (this.SymbolProfile?.assetSubClass === 'CRYPTOCURRENCY') {
|
||||||
|
@ -62,7 +62,7 @@ export class HttpResponseInterceptor implements HttpInterceptor {
|
|||||||
undefined,
|
undefined,
|
||||||
{ duration: 6000 }
|
{ duration: 6000 }
|
||||||
);
|
);
|
||||||
} else {
|
} else if (!error.url.endsWith('auth/anonymous')) {
|
||||||
this.snackBarRef = this.snackBar.open(
|
this.snackBarRef = this.snackBar.open(
|
||||||
$localize`This feature requires a subscription.`,
|
$localize`This feature requires a subscription.`,
|
||||||
this.hasPermissionForSubscription
|
this.hasPermissionForSubscription
|
||||||
|
@ -15,6 +15,10 @@ import {
|
|||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto';
|
import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
|
import {
|
||||||
|
STAY_SIGNED_IN,
|
||||||
|
SettingsStorageService
|
||||||
|
} from '@ghostfolio/client/services/settings-storage.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
|
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
|
||||||
import { getDateFormatString } from '@ghostfolio/common/helper';
|
import { getDateFormatString } from '@ghostfolio/common/helper';
|
||||||
@ -80,6 +84,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
|||||||
private snackBar: MatSnackBar,
|
private snackBar: MatSnackBar,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
|
private settingsStorageService: SettingsStorageService,
|
||||||
private stripeService: StripeService,
|
private stripeService: StripeService,
|
||||||
private userService: UserService,
|
private userService: UserService,
|
||||||
public webAuthnService: WebAuthnService
|
public webAuthnService: WebAuthnService
|
||||||
@ -397,6 +402,8 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
|
this.settingsStorageService.removeSetting(STAY_SIGNED_IN);
|
||||||
|
|
||||||
this.update();
|
this.update();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -235,7 +235,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="align-items-center d-flex mt-4 py-1">
|
<div class="align-items-center d-flex mt-4 py-1">
|
||||||
<div class="pr-1 w-50" i18n>Sign in with fingerprint</div>
|
<div class="pr-1 w-50">
|
||||||
|
<div i18n>Biometric Authentication</div>
|
||||||
|
<div class="hint-text text-muted" i18n>
|
||||||
|
Sign in with fingerprint
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="pl-1 w-50">
|
<div class="pl-1 w-50">
|
||||||
<mat-checkbox
|
<mat-checkbox
|
||||||
#toggleSignInWithFingerprintEnabledElement
|
#toggleSignInWithFingerprintEnabledElement
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
|
||||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
|
||||||
|
|
||||||
import { HalloGhostfolioPageComponent } from './hallo-ghostfolio-page.component';
|
|
||||||
|
|
||||||
const routes: Routes = [
|
|
||||||
{
|
|
||||||
canActivate: [AuthGuard],
|
|
||||||
component: HalloGhostfolioPageComponent,
|
|
||||||
path: '',
|
|
||||||
title: 'Hallo Ghostfolio'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [RouterModule.forChild(routes)],
|
|
||||||
exports: [RouterModule]
|
|
||||||
})
|
|
||||||
export class HalloGhostfolioPageRoutingModule {}
|
|
@ -1,8 +1,12 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'page' },
|
host: { class: 'page' },
|
||||||
|
imports: [MatButtonModule, RouterModule],
|
||||||
selector: 'gf-hallo-ghostfolio-page',
|
selector: 'gf-hallo-ghostfolio-page',
|
||||||
|
standalone: true,
|
||||||
templateUrl: './hallo-ghostfolio-page.html'
|
templateUrl: './hallo-ghostfolio-page.html'
|
||||||
})
|
})
|
||||||
export class HalloGhostfolioPageComponent {}
|
export class HalloGhostfolioPageComponent {}
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|
||||||
import { RouterModule } from '@angular/router';
|
|
||||||
|
|
||||||
import { HalloGhostfolioPageRoutingModule } from './hallo-ghostfolio-page-routing.module';
|
|
||||||
import { HalloGhostfolioPageComponent } from './hallo-ghostfolio-page.component';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [HalloGhostfolioPageComponent],
|
|
||||||
imports: [CommonModule, HalloGhostfolioPageRoutingModule, RouterModule],
|
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
|
||||||
})
|
|
||||||
export class HalloGhostfolioPageModule {}
|
|
@ -1,20 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
|
||||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
|
||||||
|
|
||||||
import { HelloGhostfolioPageComponent } from './hello-ghostfolio-page.component';
|
|
||||||
|
|
||||||
const routes: Routes = [
|
|
||||||
{
|
|
||||||
canActivate: [AuthGuard],
|
|
||||||
component: HelloGhostfolioPageComponent,
|
|
||||||
path: '',
|
|
||||||
title: 'Hello Ghostfolio'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [RouterModule.forChild(routes)],
|
|
||||||
exports: [RouterModule]
|
|
||||||
})
|
|
||||||
export class HelloGhostfolioPageRoutingModule {}
|
|
@ -1,8 +1,12 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'page' },
|
host: { class: 'page' },
|
||||||
|
imports: [MatButtonModule, RouterModule],
|
||||||
selector: 'gf-hello-ghostfolio-page',
|
selector: 'gf-hello-ghostfolio-page',
|
||||||
|
standalone: true,
|
||||||
templateUrl: './hello-ghostfolio-page.html'
|
templateUrl: './hello-ghostfolio-page.html'
|
||||||
})
|
})
|
||||||
export class HelloGhostfolioPageComponent {}
|
export class HelloGhostfolioPageComponent {}
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|
||||||
import { RouterModule } from '@angular/router';
|
|
||||||
|
|
||||||
import { HelloGhostfolioPageRoutingModule } from './hello-ghostfolio-page-routing.module';
|
|
||||||
import { HelloGhostfolioPageComponent } from './hello-ghostfolio-page.component';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [HelloGhostfolioPageComponent],
|
|
||||||
imports: [CommonModule, HelloGhostfolioPageRoutingModule, RouterModule],
|
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
|
||||||
})
|
|
||||||
export class HelloGhostfolioPageModule {}
|
|
@ -1,20 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
|
||||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
|
||||||
|
|
||||||
import { FirstMonthsInOpenSourcePageComponent } from './first-months-in-open-source-page.component';
|
|
||||||
|
|
||||||
const routes: Routes = [
|
|
||||||
{
|
|
||||||
canActivate: [AuthGuard],
|
|
||||||
component: FirstMonthsInOpenSourcePageComponent,
|
|
||||||
path: '',
|
|
||||||
title: 'First months in Open Source'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [RouterModule.forChild(routes)],
|
|
||||||
exports: [RouterModule]
|
|
||||||
})
|
|
||||||
export class FirstMonthsInOpenSourceRoutingModule {}
|
|
@ -1,8 +1,12 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'page' },
|
host: { class: 'page' },
|
||||||
|
imports: [MatButtonModule, RouterModule],
|
||||||
selector: 'gf-first-months-in-open-source-page',
|
selector: 'gf-first-months-in-open-source-page',
|
||||||
|
standalone: true,
|
||||||
templateUrl: './first-months-in-open-source-page.html'
|
templateUrl: './first-months-in-open-source-page.html'
|
||||||
})
|
})
|
||||||
export class FirstMonthsInOpenSourcePageComponent {}
|
export class FirstMonthsInOpenSourcePageComponent {}
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|
||||||
import { RouterModule } from '@angular/router';
|
|
||||||
|
|
||||||
import { FirstMonthsInOpenSourceRoutingModule } from './first-months-in-open-source-page-routing.module';
|
|
||||||
import { FirstMonthsInOpenSourcePageComponent } from './first-months-in-open-source-page.component';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [FirstMonthsInOpenSourcePageComponent],
|
|
||||||
imports: [CommonModule, FirstMonthsInOpenSourceRoutingModule, RouterModule],
|
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
|
||||||
})
|
|
||||||
export class FirstMonthsInOpenSourcePageModule {}
|
|
@ -1,20 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
|
||||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
|
||||||
|
|
||||||
import { GhostfolioMeetsInternetIdentityPageComponent } from './ghostfolio-meets-internet-identity-page.component';
|
|
||||||
|
|
||||||
const routes: Routes = [
|
|
||||||
{
|
|
||||||
canActivate: [AuthGuard],
|
|
||||||
component: GhostfolioMeetsInternetIdentityPageComponent,
|
|
||||||
path: '',
|
|
||||||
title: 'Ghostfolio meets Internet Identity'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [RouterModule.forChild(routes)],
|
|
||||||
exports: [RouterModule]
|
|
||||||
})
|
|
||||||
export class GhostfolioMeetsInternetIdentityRoutingModule {}
|
|
@ -1,8 +1,12 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'page' },
|
host: { class: 'page' },
|
||||||
|
imports: [MatButtonModule, RouterModule],
|
||||||
selector: 'gf-ghostfolio-meets-internet-identity-page',
|
selector: 'gf-ghostfolio-meets-internet-identity-page',
|
||||||
|
standalone: true,
|
||||||
templateUrl: './ghostfolio-meets-internet-identity-page.html'
|
templateUrl: './ghostfolio-meets-internet-identity-page.html'
|
||||||
})
|
})
|
||||||
export class GhostfolioMeetsInternetIdentityPageComponent {}
|
export class GhostfolioMeetsInternetIdentityPageComponent {}
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|
||||||
import { RouterModule } from '@angular/router';
|
|
||||||
|
|
||||||
import { GhostfolioMeetsInternetIdentityRoutingModule } from './ghostfolio-meets-internet-identity-page-routing.module';
|
|
||||||
import { GhostfolioMeetsInternetIdentityPageComponent } from './ghostfolio-meets-internet-identity-page.component';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [GhostfolioMeetsInternetIdentityPageComponent],
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
GhostfolioMeetsInternetIdentityRoutingModule,
|
|
||||||
RouterModule
|
|
||||||
],
|
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
|
||||||
})
|
|
||||||
export class GhostfolioMeetsInternetIdentityPageModule {}
|
|
@ -1,20 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
|
||||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
|
||||||
|
|
||||||
import { HowDoIGetMyFinancesInOrderPageComponent } from './how-do-i-get-my-finances-in-order-page.component';
|
|
||||||
|
|
||||||
const routes: Routes = [
|
|
||||||
{
|
|
||||||
canActivate: [AuthGuard],
|
|
||||||
component: HowDoIGetMyFinancesInOrderPageComponent,
|
|
||||||
path: '',
|
|
||||||
title: 'How do I get my finances in order?'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [RouterModule.forChild(routes)],
|
|
||||||
exports: [RouterModule]
|
|
||||||
})
|
|
||||||
export class HowDoIGetMyFinancesInOrderRoutingModule {}
|
|
@ -1,8 +1,12 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'page' },
|
host: { class: 'page' },
|
||||||
|
imports: [MatButtonModule, RouterModule],
|
||||||
selector: 'gf-how-do-i-get-my-finances-in-order-page',
|
selector: 'gf-how-do-i-get-my-finances-in-order-page',
|
||||||
|
standalone: true,
|
||||||
templateUrl: './how-do-i-get-my-finances-in-order-page.html'
|
templateUrl: './how-do-i-get-my-finances-in-order-page.html'
|
||||||
})
|
})
|
||||||
export class HowDoIGetMyFinancesInOrderPageComponent {}
|
export class HowDoIGetMyFinancesInOrderPageComponent {}
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|
||||||
import { RouterModule } from '@angular/router';
|
|
||||||
|
|
||||||
import { HowDoIGetMyFinancesInOrderRoutingModule } from './how-do-i-get-my-finances-in-order-page-routing.module';
|
|
||||||
import { HowDoIGetMyFinancesInOrderPageComponent } from './how-do-i-get-my-finances-in-order-page.component';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [HowDoIGetMyFinancesInOrderPageComponent],
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
HowDoIGetMyFinancesInOrderRoutingModule,
|
|
||||||
RouterModule
|
|
||||||
],
|
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
|
||||||
})
|
|
||||||
export class HowDoIGetMyFinancesInOrderPageModule {}
|
|
@ -1,20 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
|
||||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
|
||||||
|
|
||||||
import { FiveHundredStarsOnGitHubPageComponent } from './500-stars-on-github-page.component';
|
|
||||||
|
|
||||||
const routes: Routes = [
|
|
||||||
{
|
|
||||||
canActivate: [AuthGuard],
|
|
||||||
component: FiveHundredStarsOnGitHubPageComponent,
|
|
||||||
path: '',
|
|
||||||
title: '500 Stars on GitHub'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [RouterModule.forChild(routes)],
|
|
||||||
exports: [RouterModule]
|
|
||||||
})
|
|
||||||
export class FiveHundredStarsOnGitHubRoutingModule {}
|
|
@ -1,8 +1,12 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'page' },
|
host: { class: 'page' },
|
||||||
|
imports: [MatButtonModule, RouterModule],
|
||||||
selector: 'gf-500-stars-on-github-page',
|
selector: 'gf-500-stars-on-github-page',
|
||||||
|
standalone: true,
|
||||||
templateUrl: './500-stars-on-github-page.html'
|
templateUrl: './500-stars-on-github-page.html'
|
||||||
})
|
})
|
||||||
export class FiveHundredStarsOnGitHubPageComponent {}
|
export class FiveHundredStarsOnGitHubPageComponent {}
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|
||||||
import { RouterModule } from '@angular/router';
|
|
||||||
|
|
||||||
import { FiveHundredStarsOnGitHubRoutingModule } from './500-stars-on-github-page-routing.module';
|
|
||||||
import { FiveHundredStarsOnGitHubPageComponent } from './500-stars-on-github-page.component';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [FiveHundredStarsOnGitHubPageComponent],
|
|
||||||
imports: [CommonModule, FiveHundredStarsOnGitHubRoutingModule, RouterModule],
|
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
|
||||||
})
|
|
||||||
export class FiveHundredStarsOnGitHubPageModule {}
|
|
@ -1,20 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
|
||||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
|
||||||
|
|
||||||
import { Hacktoberfest2022PageComponent } from './hacktoberfest-2022-page.component';
|
|
||||||
|
|
||||||
const routes: Routes = [
|
|
||||||
{
|
|
||||||
canActivate: [AuthGuard],
|
|
||||||
component: Hacktoberfest2022PageComponent,
|
|
||||||
path: '',
|
|
||||||
title: 'Hacktoberfest 2022'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [RouterModule.forChild(routes)],
|
|
||||||
exports: [RouterModule]
|
|
||||||
})
|
|
||||||
export class Hacktoberfest2022RoutingModule {}
|
|
@ -1,8 +1,12 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'page' },
|
host: { class: 'page' },
|
||||||
|
imports: [MatButtonModule, RouterModule],
|
||||||
selector: 'gf-hacktoberfest-2022-page',
|
selector: 'gf-hacktoberfest-2022-page',
|
||||||
|
standalone: true,
|
||||||
templateUrl: './hacktoberfest-2022-page.html'
|
templateUrl: './hacktoberfest-2022-page.html'
|
||||||
})
|
})
|
||||||
export class Hacktoberfest2022PageComponent {}
|
export class Hacktoberfest2022PageComponent {}
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|
||||||
import { RouterModule } from '@angular/router';
|
|
||||||
|
|
||||||
import { Hacktoberfest2022RoutingModule } from './hacktoberfest-2022-page-routing.module';
|
|
||||||
import { Hacktoberfest2022PageComponent } from './hacktoberfest-2022-page.component';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [Hacktoberfest2022PageComponent],
|
|
||||||
imports: [CommonModule, Hacktoberfest2022RoutingModule, RouterModule],
|
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
|
||||||
})
|
|
||||||
export class Hacktoberfest2022PageModule {}
|
|
@ -1,20 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
|
||||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
|
||||||
|
|
||||||
import { BlackFriday2022PageComponent } from './black-friday-2022-page.component';
|
|
||||||
|
|
||||||
const routes: Routes = [
|
|
||||||
{
|
|
||||||
canActivate: [AuthGuard],
|
|
||||||
component: BlackFriday2022PageComponent,
|
|
||||||
path: '',
|
|
||||||
title: 'Black Friday 2022'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [RouterModule.forChild(routes)],
|
|
||||||
exports: [RouterModule]
|
|
||||||
})
|
|
||||||
export class BlackFriday2022RoutingModule {}
|
|
@ -1,8 +1,13 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'page' },
|
host: { class: 'page' },
|
||||||
|
imports: [GfPremiumIndicatorModule, MatButtonModule, RouterModule],
|
||||||
selector: 'gf-black-friday-2022-page',
|
selector: 'gf-black-friday-2022-page',
|
||||||
|
standalone: true,
|
||||||
templateUrl: './black-friday-2022-page.html'
|
templateUrl: './black-friday-2022-page.html'
|
||||||
})
|
})
|
||||||
export class BlackFriday2022PageComponent {
|
export class BlackFriday2022PageComponent {
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
|
||||||
import { RouterModule } from '@angular/router';
|
|
||||||
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
|
||||||
|
|
||||||
import { BlackFriday2022RoutingModule } from './black-friday-2022-page-routing.module';
|
|
||||||
import { BlackFriday2022PageComponent } from './black-friday-2022-page.component';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [BlackFriday2022PageComponent],
|
|
||||||
imports: [
|
|
||||||
BlackFriday2022RoutingModule,
|
|
||||||
CommonModule,
|
|
||||||
GfPremiumIndicatorModule,
|
|
||||||
MatButtonModule,
|
|
||||||
RouterModule
|
|
||||||
],
|
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
|
||||||
})
|
|
||||||
export class BlackFriday2022PageModule {}
|
|
@ -1,20 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
|
||||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
|
||||||
|
|
||||||
import { TheImportanceOfTrackingYourPersonalFinancesPageComponent } from './the-importance-of-tracking-your-personal-finances-page.component';
|
|
||||||
|
|
||||||
const routes: Routes = [
|
|
||||||
{
|
|
||||||
canActivate: [AuthGuard],
|
|
||||||
component: TheImportanceOfTrackingYourPersonalFinancesPageComponent,
|
|
||||||
path: '',
|
|
||||||
title: 'The importance of tracking your personal finances'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [RouterModule.forChild(routes)],
|
|
||||||
exports: [RouterModule]
|
|
||||||
})
|
|
||||||
export class TheImportanceOfTrackingYourPersonalFinancesRoutingModule {}
|
|
@ -1,8 +1,12 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'page' },
|
host: { class: 'page' },
|
||||||
|
imports: [MatButtonModule, RouterModule],
|
||||||
selector: 'gf-the-importance-of-tracking-your-personal-finances-page',
|
selector: 'gf-the-importance-of-tracking-your-personal-finances-page',
|
||||||
|
standalone: true,
|
||||||
templateUrl: './the-importance-of-tracking-your-personal-finances-page.html'
|
templateUrl: './the-importance-of-tracking-your-personal-finances-page.html'
|
||||||
})
|
})
|
||||||
export class TheImportanceOfTrackingYourPersonalFinancesPageComponent {}
|
export class TheImportanceOfTrackingYourPersonalFinancesPageComponent {}
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
|
||||||
import { RouterModule } from '@angular/router';
|
|
||||||
|
|
||||||
import { TheImportanceOfTrackingYourPersonalFinancesRoutingModule } from './the-importance-of-tracking-your-personal-finances-page-routing.module';
|
|
||||||
import { TheImportanceOfTrackingYourPersonalFinancesPageComponent } from './the-importance-of-tracking-your-personal-finances-page.component';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [TheImportanceOfTrackingYourPersonalFinancesPageComponent],
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
MatButtonModule,
|
|
||||||
RouterModule,
|
|
||||||
TheImportanceOfTrackingYourPersonalFinancesRoutingModule
|
|
||||||
],
|
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
|
||||||
})
|
|
||||||
export class TheImportanceOfTrackingYourPersonalFinancesPageModule {}
|
|
@ -1,20 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
|
||||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
|
||||||
|
|
||||||
import { GhostfolioAufSackgeldVorgestelltPageComponent } from './ghostfolio-auf-sackgeld-vorgestellt-page.component';
|
|
||||||
|
|
||||||
const routes: Routes = [
|
|
||||||
{
|
|
||||||
canActivate: [AuthGuard],
|
|
||||||
component: GhostfolioAufSackgeldVorgestelltPageComponent,
|
|
||||||
path: '',
|
|
||||||
title: 'Ghostfolio auf Sackgeld.com vorgestellt'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [RouterModule.forChild(routes)],
|
|
||||||
exports: [RouterModule]
|
|
||||||
})
|
|
||||||
export class GhostfolioAufSackgeldVorgestelltPageRoutingModule {}
|
|
@ -1,8 +1,12 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'page' },
|
host: { class: 'page' },
|
||||||
|
imports: [MatButtonModule, RouterModule],
|
||||||
selector: 'gf-ghostfolio-auf-sackgeld-vorgestellt-page',
|
selector: 'gf-ghostfolio-auf-sackgeld-vorgestellt-page',
|
||||||
|
standalone: true,
|
||||||
templateUrl: './ghostfolio-auf-sackgeld-vorgestellt-page.html'
|
templateUrl: './ghostfolio-auf-sackgeld-vorgestellt-page.html'
|
||||||
})
|
})
|
||||||
export class GhostfolioAufSackgeldVorgestelltPageComponent {}
|
export class GhostfolioAufSackgeldVorgestelltPageComponent {}
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|
||||||
import { RouterModule } from '@angular/router';
|
|
||||||
|
|
||||||
import { GhostfolioAufSackgeldVorgestelltPageRoutingModule } from './ghostfolio-auf-sackgeld-vorgestellt-page-routing.module';
|
|
||||||
import { GhostfolioAufSackgeldVorgestelltPageComponent } from './ghostfolio-auf-sackgeld-vorgestellt-page.component';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [GhostfolioAufSackgeldVorgestelltPageComponent],
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
GhostfolioAufSackgeldVorgestelltPageRoutingModule,
|
|
||||||
RouterModule
|
|
||||||
],
|
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
|
||||||
})
|
|
||||||
export class GhostfolioAufSackgeldVorgestelltPageModule {}
|
|
@ -1,20 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
|
||||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
|
||||||
|
|
||||||
import { GhostfolioMeetsUmbrelPageComponent } from './ghostfolio-meets-umbrel-page.component';
|
|
||||||
|
|
||||||
const routes: Routes = [
|
|
||||||
{
|
|
||||||
canActivate: [AuthGuard],
|
|
||||||
component: GhostfolioMeetsUmbrelPageComponent,
|
|
||||||
path: '',
|
|
||||||
title: 'Ghostfolio meets Umbrel'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [RouterModule.forChild(routes)],
|
|
||||||
exports: [RouterModule]
|
|
||||||
})
|
|
||||||
export class GhostfolioMeetsUmbrelPageRoutingModule {}
|
|
@ -1,8 +1,12 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'page' },
|
host: { class: 'page' },
|
||||||
|
imports: [MatButtonModule, RouterModule],
|
||||||
selector: 'gf-ghostfolio-meets-umbrel-page',
|
selector: 'gf-ghostfolio-meets-umbrel-page',
|
||||||
|
standalone: true,
|
||||||
templateUrl: './ghostfolio-meets-umbrel-page.html'
|
templateUrl: './ghostfolio-meets-umbrel-page.html'
|
||||||
})
|
})
|
||||||
export class GhostfolioMeetsUmbrelPageComponent {}
|
export class GhostfolioMeetsUmbrelPageComponent {}
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|
||||||
import { RouterModule } from '@angular/router';
|
|
||||||
|
|
||||||
import { GhostfolioMeetsUmbrelPageRoutingModule } from './ghostfolio-meets-umbrel-page-routing.module';
|
|
||||||
import { GhostfolioMeetsUmbrelPageComponent } from './ghostfolio-meets-umbrel-page.component';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [GhostfolioMeetsUmbrelPageComponent],
|
|
||||||
imports: [CommonModule, GhostfolioMeetsUmbrelPageRoutingModule, RouterModule],
|
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
|
||||||
})
|
|
||||||
export class GhostfolioMeetsUmbrelPageModule {}
|
|
@ -1,20 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
|
||||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
|
||||||
|
|
||||||
import { ThousandStarsOnGitHubPageComponent } from './1000-stars-on-github-page.component';
|
|
||||||
|
|
||||||
const routes: Routes = [
|
|
||||||
{
|
|
||||||
canActivate: [AuthGuard],
|
|
||||||
component: ThousandStarsOnGitHubPageComponent,
|
|
||||||
path: '',
|
|
||||||
title: 'Ghostfolio reaches 1’000 Stars on GitHub'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [RouterModule.forChild(routes)],
|
|
||||||
exports: [RouterModule]
|
|
||||||
})
|
|
||||||
export class ThousandStarsOnGitHubRoutingModule {}
|
|
@ -1,8 +1,12 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'page' },
|
host: { class: 'page' },
|
||||||
|
imports: [MatButtonModule, RouterModule],
|
||||||
selector: 'gf-1000-stars-on-github-page',
|
selector: 'gf-1000-stars-on-github-page',
|
||||||
|
standalone: true,
|
||||||
templateUrl: './1000-stars-on-github-page.html'
|
templateUrl: './1000-stars-on-github-page.html'
|
||||||
})
|
})
|
||||||
export class ThousandStarsOnGitHubPageComponent {}
|
export class ThousandStarsOnGitHubPageComponent {}
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|
||||||
import { RouterModule } from '@angular/router';
|
|
||||||
|
|
||||||
import { ThousandStarsOnGitHubRoutingModule } from './1000-stars-on-github-page-routing.module';
|
|
||||||
import { ThousandStarsOnGitHubPageComponent } from './1000-stars-on-github-page.component';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [ThousandStarsOnGitHubPageComponent],
|
|
||||||
imports: [CommonModule, ThousandStarsOnGitHubRoutingModule, RouterModule],
|
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
|
||||||
})
|
|
||||||
export class ThousandStarsOnGitHubPageModule {}
|
|
@ -1,20 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
|
||||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
|
||||||
|
|
||||||
import { UnlockYourFinancialPotentialWithGhostfolioPageComponent } from './unlock-your-financial-potential-with-ghostfolio-page.component';
|
|
||||||
|
|
||||||
const routes: Routes = [
|
|
||||||
{
|
|
||||||
canActivate: [AuthGuard],
|
|
||||||
component: UnlockYourFinancialPotentialWithGhostfolioPageComponent,
|
|
||||||
path: '',
|
|
||||||
title: 'Unlock your Financial Potential with Ghostfolio'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [RouterModule.forChild(routes)],
|
|
||||||
exports: [RouterModule]
|
|
||||||
})
|
|
||||||
export class UnlockYourFinancialPotentialWithGhostfolioRoutingModule {}
|
|
@ -1,8 +1,12 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'page' },
|
host: { class: 'page' },
|
||||||
|
imports: [MatButtonModule, RouterModule],
|
||||||
selector: 'gf-unlock-your-financial-potential-with-ghostfolio-page',
|
selector: 'gf-unlock-your-financial-potential-with-ghostfolio-page',
|
||||||
|
standalone: true,
|
||||||
templateUrl: './unlock-your-financial-potential-with-ghostfolio-page.html'
|
templateUrl: './unlock-your-financial-potential-with-ghostfolio-page.html'
|
||||||
})
|
})
|
||||||
export class UnlockYourFinancialPotentialWithGhostfolioPageComponent {}
|
export class UnlockYourFinancialPotentialWithGhostfolioPageComponent {}
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
|
||||||
import { RouterModule } from '@angular/router';
|
|
||||||
|
|
||||||
import { UnlockYourFinancialPotentialWithGhostfolioRoutingModule } from './unlock-your-financial-potential-with-ghostfolio-page-routing.module';
|
|
||||||
import { UnlockYourFinancialPotentialWithGhostfolioPageComponent } from './unlock-your-financial-potential-with-ghostfolio-page.component';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [UnlockYourFinancialPotentialWithGhostfolioPageComponent],
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
MatButtonModule,
|
|
||||||
RouterModule,
|
|
||||||
UnlockYourFinancialPotentialWithGhostfolioRoutingModule
|
|
||||||
],
|
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
|
||||||
})
|
|
||||||
export class UnlockYourFinancialPotentialWithGhostfolioPageModule {}
|
|
@ -1,20 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
|
||||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
|
||||||
|
|
||||||
import { ExploringThePathToFirePageComponent } from './exploring-the-path-to-fire-page.component';
|
|
||||||
|
|
||||||
const routes: Routes = [
|
|
||||||
{
|
|
||||||
canActivate: [AuthGuard],
|
|
||||||
component: ExploringThePathToFirePageComponent,
|
|
||||||
path: '',
|
|
||||||
title: 'Exploring the Path to FIRE'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [RouterModule.forChild(routes)],
|
|
||||||
exports: [RouterModule]
|
|
||||||
})
|
|
||||||
export class ExploringThePathToFireRoutingModule {}
|
|
@ -1,8 +1,12 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'page' },
|
host: { class: 'page' },
|
||||||
|
imports: [MatButtonModule, RouterModule],
|
||||||
selector: 'gf-exploring-the-path-to-fire-page-page',
|
selector: 'gf-exploring-the-path-to-fire-page-page',
|
||||||
|
standalone: true,
|
||||||
templateUrl: './exploring-the-path-to-fire-page.html'
|
templateUrl: './exploring-the-path-to-fire-page.html'
|
||||||
})
|
})
|
||||||
export class ExploringThePathToFirePageComponent {}
|
export class ExploringThePathToFirePageComponent {}
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
|
||||||
import { RouterModule } from '@angular/router';
|
|
||||||
|
|
||||||
import { ExploringThePathToFireRoutingModule } from './exploring-the-path-to-fire-page-routing.module';
|
|
||||||
import { ExploringThePathToFirePageComponent } from './exploring-the-path-to-fire-page.component';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [ExploringThePathToFirePageComponent],
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
ExploringThePathToFireRoutingModule,
|
|
||||||
MatButtonModule,
|
|
||||||
RouterModule
|
|
||||||
],
|
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
|
||||||
})
|
|
||||||
export class ExploringThePathToFirePageModule {}
|
|
@ -10,6 +10,132 @@ const routes: Routes = [
|
|||||||
component: BlogPageComponent,
|
component: BlogPageComponent,
|
||||||
path: '',
|
path: '',
|
||||||
title: $localize`Blog`
|
title: $localize`Blog`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
path: '2021/07/hallo-ghostfolio',
|
||||||
|
loadComponent: () =>
|
||||||
|
import('./2021/07/hallo-ghostfolio/hallo-ghostfolio-page.component').then(
|
||||||
|
(c) => c.HalloGhostfolioPageComponent
|
||||||
|
),
|
||||||
|
title: 'Hallo Ghostfolio'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
path: '2021/07/hello-ghostfolio',
|
||||||
|
loadComponent: () =>
|
||||||
|
import('./2021/07/hello-ghostfolio/hello-ghostfolio-page.component').then(
|
||||||
|
(c) => c.HelloGhostfolioPageComponent
|
||||||
|
),
|
||||||
|
title: 'Hello Ghostfolio'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
path: '2022/01/ghostfolio-first-months-in-open-source',
|
||||||
|
loadComponent: () =>
|
||||||
|
import(
|
||||||
|
'./2022/01/first-months-in-open-source/first-months-in-open-source-page.component'
|
||||||
|
).then((c) => c.FirstMonthsInOpenSourcePageComponent),
|
||||||
|
title: 'First months in Open Source'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
path: '2022/07/ghostfolio-meets-internet-identity',
|
||||||
|
loadComponent: () =>
|
||||||
|
import(
|
||||||
|
'./2022/07/ghostfolio-meets-internet-identity/ghostfolio-meets-internet-identity-page.component'
|
||||||
|
).then((c) => c.GhostfolioMeetsInternetIdentityPageComponent),
|
||||||
|
title: 'Ghostfolio meets Internet Identity'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
path: '2022/07/how-do-i-get-my-finances-in-order',
|
||||||
|
loadComponent: () =>
|
||||||
|
import(
|
||||||
|
'./2022/07/how-do-i-get-my-finances-in-order/how-do-i-get-my-finances-in-order-page.component'
|
||||||
|
).then((c) => c.HowDoIGetMyFinancesInOrderPageComponent),
|
||||||
|
title: 'How do I get my finances in order?'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
path: '2022/08/500-stars-on-github',
|
||||||
|
loadComponent: () =>
|
||||||
|
import(
|
||||||
|
'./2022/08/500-stars-on-github/500-stars-on-github-page.component'
|
||||||
|
).then((c) => c.FiveHundredStarsOnGitHubPageComponent),
|
||||||
|
title: '500 Stars on GitHub'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
path: '2022/10/hacktoberfest-2022',
|
||||||
|
loadComponent: () =>
|
||||||
|
import(
|
||||||
|
'./2022/10/hacktoberfest-2022/hacktoberfest-2022-page.component'
|
||||||
|
).then((c) => c.Hacktoberfest2022PageComponent),
|
||||||
|
title: 'Hacktoberfest 2022'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
path: '2022/11/black-friday-2022',
|
||||||
|
loadComponent: () =>
|
||||||
|
import(
|
||||||
|
'./2022/11/black-friday-2022/black-friday-2022-page.component'
|
||||||
|
).then((c) => c.BlackFriday2022PageComponent),
|
||||||
|
title: 'Black Friday 2022'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
path: '2022/12/the-importance-of-tracking-your-personal-finances',
|
||||||
|
loadComponent: () =>
|
||||||
|
import(
|
||||||
|
'./2022/12/the-importance-of-tracking-your-personal-finances/the-importance-of-tracking-your-personal-finances-page.component'
|
||||||
|
).then((c) => c.TheImportanceOfTrackingYourPersonalFinancesPageComponent),
|
||||||
|
title: 'The importance of tracking your personal finances'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
path: '2023/01/ghostfolio-auf-sackgeld-vorgestellt',
|
||||||
|
loadComponent: () =>
|
||||||
|
import(
|
||||||
|
'./2023/01/ghostfolio-auf-sackgeld-vorgestellt/ghostfolio-auf-sackgeld-vorgestellt-page.component'
|
||||||
|
).then((c) => c.GhostfolioAufSackgeldVorgestelltPageComponent),
|
||||||
|
title: 'Ghostfolio auf Sackgeld.com vorgestellt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
path: '2023/02/ghostfolio-meets-umbrel',
|
||||||
|
loadComponent: () =>
|
||||||
|
import(
|
||||||
|
'./2023/02/ghostfolio-meets-umbrel/ghostfolio-meets-umbrel-page.component'
|
||||||
|
).then((c) => c.GhostfolioMeetsUmbrelPageComponent),
|
||||||
|
title: 'Ghostfolio meets Umbrel'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
path: '2023/03/ghostfolio-reaches-1000-stars-on-github',
|
||||||
|
loadComponent: () =>
|
||||||
|
import(
|
||||||
|
'./2023/03/1000-stars-on-github/1000-stars-on-github-page.component'
|
||||||
|
).then((c) => c.ThousandStarsOnGitHubPageComponent),
|
||||||
|
title: 'Ghostfolio reaches 1’000 Stars on GitHub'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
path: '2023/05/unlock-your-financial-potential-with-ghostfolio',
|
||||||
|
loadComponent: () =>
|
||||||
|
import(
|
||||||
|
'./2023/05/unlock-your-financial-potential-with-ghostfolio/unlock-your-financial-potential-with-ghostfolio-page.component'
|
||||||
|
).then((c) => c.UnlockYourFinancialPotentialWithGhostfolioPageComponent),
|
||||||
|
title: 'Unlock your Financial Potential with Ghostfolio'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
path: '2023/07/exploring-the-path-to-fire',
|
||||||
|
loadComponent: () =>
|
||||||
|
import(
|
||||||
|
'./2023/07/exploring-the-path-to-fire/exploring-the-path-to-fire-page.component'
|
||||||
|
).then((c) => c.ExploringThePathToFirePageComponent),
|
||||||
|
title: 'Exploring the Path to FIRE'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -4,6 +4,12 @@
|
|||||||
<h3 class="d-none d-sm-block mb-3 text-center">
|
<h3 class="d-none d-sm-block mb-3 text-center">
|
||||||
Frequently Asked Questions (FAQ)
|
Frequently Asked Questions (FAQ)
|
||||||
</h3>
|
</h3>
|
||||||
|
<p>
|
||||||
|
Find quick answers to commonly asked questions about Ghostfolio in our
|
||||||
|
Frequently Asked Questions (FAQ) section. Discover what Ghostfolio is,
|
||||||
|
explore its features, and learn about our privacy practices. Get all the
|
||||||
|
information you need in one place.
|
||||||
|
</p>
|
||||||
<mat-card appearance="outlined" class="mb-3">
|
<mat-card appearance="outlined" class="mb-3">
|
||||||
<mat-card-header>
|
<mat-card-header>
|
||||||
<mat-card-title>What is Ghostfolio?</mat-card-title>
|
<mat-card-title>What is Ghostfolio?</mat-card-title>
|
||||||
@ -22,7 +28,7 @@
|
|||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
With Ghostfolio, you can keep track of various assets like stocks,
|
With Ghostfolio, you can keep track of various assets like stocks,
|
||||||
ETFs or cryptocurrencies.
|
ETFs, bonds, cryptocurrencies and commodities.
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
<mat-card appearance="outlined" class="mb-3">
|
<mat-card appearance="outlined" class="mb-3">
|
||||||
@ -51,6 +57,17 @@
|
|||||||
or <i>Google Sign</i>. We will guide you to set up your portfolio.
|
or <i>Google Sign</i>. We will guide you to set up your portfolio.
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
<mat-card appearance="outlined" class="mb-3">
|
||||||
|
<mat-card-header>
|
||||||
|
<mat-card-title
|
||||||
|
>Will you spam me with emails once I sign up?</mat-card-title
|
||||||
|
></mat-card-header
|
||||||
|
>
|
||||||
|
<mat-card-content>
|
||||||
|
No, we do not even collect your email address, so you will not receive
|
||||||
|
any spam emails from us.
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
<mat-card appearance="outlined" class="mb-3">
|
<mat-card appearance="outlined" class="mb-3">
|
||||||
<mat-card-header>
|
<mat-card-header>
|
||||||
<mat-card-title
|
<mat-card-title
|
||||||
@ -92,6 +109,30 @@
|
|||||||
get for free.</mat-card-content
|
get for free.</mat-card-content
|
||||||
>
|
>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
<mat-card appearance="outlined" class="mb-3">
|
||||||
|
<mat-card-header>
|
||||||
|
<mat-card-title
|
||||||
|
>Do you monetize or sell my financial data?</mat-card-title
|
||||||
|
></mat-card-header
|
||||||
|
>
|
||||||
|
<mat-card-content
|
||||||
|
>No, we value your privacy. We do not sell or share your financial
|
||||||
|
data with any third parties.</mat-card-content
|
||||||
|
>
|
||||||
|
</mat-card>
|
||||||
|
<mat-card appearance="outlined" class="mb-3">
|
||||||
|
<mat-card-header>
|
||||||
|
<mat-card-title
|
||||||
|
>What is your business model?</mat-card-title
|
||||||
|
></mat-card-header
|
||||||
|
>
|
||||||
|
<mat-card-content
|
||||||
|
>By offering <a [routerLink]="['/pricing']">Ghostfolio Premium</a>, a
|
||||||
|
subscription plan with a managed hosting service and enhanced
|
||||||
|
features, we fund our business while providing added value to our
|
||||||
|
users.</mat-card-content
|
||||||
|
>
|
||||||
|
</mat-card>
|
||||||
<mat-card appearance="outlined" class="mb-3">
|
<mat-card appearance="outlined" class="mb-3">
|
||||||
<mat-card-header>
|
<mat-card-header>
|
||||||
<mat-card-title
|
<mat-card-title
|
||||||
|
@ -14,23 +14,13 @@ import { DateAdapter, MAT_DATE_LOCALE } from '@angular/material/core';
|
|||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
||||||
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
||||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { getDateFormatString } from '@ghostfolio/common/helper';
|
import { getDateFormatString } from '@ghostfolio/common/helper';
|
||||||
import { translate } from '@ghostfolio/ui/i18n';
|
import { translate } from '@ghostfolio/ui/i18n';
|
||||||
import { AssetClass, AssetSubClass, Tag, Type } from '@prisma/client';
|
import { AssetClass, AssetSubClass, Tag, Type } from '@prisma/client';
|
||||||
import { isUUID } from 'class-validator';
|
import { isUUID } from 'class-validator';
|
||||||
import { isString } from 'lodash';
|
|
||||||
import { EMPTY, Observable, Subject, lastValueFrom, of } from 'rxjs';
|
import { EMPTY, Observable, Subject, lastValueFrom, of } from 'rxjs';
|
||||||
import {
|
import { catchError, map, startWith, takeUntil } from 'rxjs/operators';
|
||||||
catchError,
|
|
||||||
debounceTime,
|
|
||||||
distinctUntilChanged,
|
|
||||||
map,
|
|
||||||
startWith,
|
|
||||||
switchMap,
|
|
||||||
takeUntil
|
|
||||||
} from 'rxjs/operators';
|
|
||||||
|
|
||||||
import { CreateOrUpdateActivityDialogParams } from './interfaces/interfaces';
|
import { CreateOrUpdateActivityDialogParams } from './interfaces/interfaces';
|
||||||
|
|
||||||
@ -61,6 +51,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
|||||||
public separatorKeysCodes: number[] = [ENTER, COMMA];
|
public separatorKeysCodes: number[] = [ENTER, COMMA];
|
||||||
public tags: Tag[] = [];
|
public tags: Tag[] = [];
|
||||||
public total = 0;
|
public total = 0;
|
||||||
|
public typesTranslationMap = new Map<Type, string>();
|
||||||
public Validators = Validators;
|
public Validators = Validators;
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
@ -91,6 +82,10 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Object.keys(Type).forEach((type) => {
|
||||||
|
this.typesTranslationMap[Type[type]] = translate(Type[type]);
|
||||||
|
});
|
||||||
|
|
||||||
this.activityForm = this.formBuilder.group({
|
this.activityForm = this.formBuilder.group({
|
||||||
accountId: [this.data.activity?.accountId, Validators.required],
|
accountId: [this.data.activity?.accountId, Validators.required],
|
||||||
assetClass: [this.data.activity?.SymbolProfile?.assetClass],
|
assetClass: [this.data.activity?.SymbolProfile?.assetClass],
|
||||||
@ -374,10 +369,6 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public displayFn(aLookupItem: LookupItem) {
|
|
||||||
return aLookupItem?.symbol ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
public onAddTag(event: MatAutocompleteSelectedEvent) {
|
public onAddTag(event: MatAutocompleteSelectedEvent) {
|
||||||
this.activityForm.controls['tags'].setValue([
|
this.activityForm.controls['tags'].setValue([
|
||||||
...(this.activityForm.controls['tags'].value ?? []),
|
...(this.activityForm.controls['tags'].value ?? []),
|
||||||
|
@ -11,11 +11,41 @@
|
|||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
<mat-label i18n>Type</mat-label>
|
<mat-label i18n>Type</mat-label>
|
||||||
<mat-select formControlName="type">
|
<mat-select formControlName="type">
|
||||||
<mat-option i18n value="BUY">Buy</mat-option>
|
<mat-select-trigger
|
||||||
<mat-option i18n value="DIVIDEND">Dividend</mat-option>
|
>{{ typesTranslationMap[activityForm.controls['type'].value]
|
||||||
<mat-option i18n value="ITEM">Item</mat-option>
|
}}</mat-select-trigger
|
||||||
<mat-option i18n value="LIABILITY">Liability</mat-option>
|
>
|
||||||
<mat-option i18n value="SELL">Sell</mat-option>
|
<mat-option class="line-height-1" value="BUY">
|
||||||
|
<span><b>{{ typesTranslationMap['BUY'] }}</b></span>
|
||||||
|
<br />
|
||||||
|
<small class="text-muted text-nowrap" i18n
|
||||||
|
>Stocks, ETFs, bonds, cryptocurrencies, commodities</small
|
||||||
|
>
|
||||||
|
</mat-option>
|
||||||
|
<mat-option class="line-height-1" value="DIVIDEND">
|
||||||
|
<span><b>{{ typesTranslationMap['DIVIDEND'] }}</b></span>
|
||||||
|
</mat-option>
|
||||||
|
<mat-option class="line-height-1" value="LIABILITY">
|
||||||
|
<span><b>{{ typesTranslationMap['LIABILITY'] }}</b></span>
|
||||||
|
<br />
|
||||||
|
<small class="text-muted text-nowrap" i18n
|
||||||
|
>Mortgages, personal loans, credit cards</small
|
||||||
|
>
|
||||||
|
</mat-option>
|
||||||
|
<mat-option class="line-height-1" value="SELL">
|
||||||
|
<span><b>{{ typesTranslationMap['SELL'] }}</b></span>
|
||||||
|
<br />
|
||||||
|
<small class="text-muted text-nowrap" i18n
|
||||||
|
>Stocks, ETFs, bonds, cryptocurrencies, commodities</small
|
||||||
|
>
|
||||||
|
</mat-option>
|
||||||
|
<mat-option class="line-height-1" value="ITEM">
|
||||||
|
<span><b>{{ typesTranslationMap['ITEM'] }}</b></span>
|
||||||
|
<br />
|
||||||
|
<small class="text-muted text-nowrap" i18n
|
||||||
|
>Luxury items, real estate, private companies</small
|
||||||
|
>
|
||||||
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
@ -18,7 +18,7 @@ import {
|
|||||||
User
|
User
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { Market } from '@ghostfolio/common/types';
|
import { Market, MarketAdvanced } from '@ghostfolio/common/types';
|
||||||
import { translate } from '@ghostfolio/ui/i18n';
|
import { translate } from '@ghostfolio/ui/i18n';
|
||||||
import { Account, AssetClass, DataSource, Platform } from '@prisma/client';
|
import { Account, AssetClass, DataSource, Platform } from '@prisma/client';
|
||||||
import { isNumber } from 'lodash';
|
import { isNumber } from 'lodash';
|
||||||
@ -54,6 +54,13 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
public markets: {
|
public markets: {
|
||||||
[key in Market]: { name: string; value: number };
|
[key in Market]: { name: string; value: number };
|
||||||
};
|
};
|
||||||
|
public marketsAdvanced: {
|
||||||
|
[key in MarketAdvanced]: {
|
||||||
|
id: MarketAdvanced;
|
||||||
|
name: string;
|
||||||
|
value: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
public placeholder = '';
|
public placeholder = '';
|
||||||
public platforms: {
|
public platforms: {
|
||||||
[id: string]: Pick<Platform, 'name'> & {
|
[id: string]: Pick<Platform, 'name'> & {
|
||||||
@ -65,13 +72,8 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
public positions: {
|
public positions: {
|
||||||
[symbol: string]: Pick<
|
[symbol: string]: Pick<
|
||||||
PortfolioPosition,
|
PortfolioPosition,
|
||||||
| 'assetClass'
|
'assetClass' | 'assetSubClass' | 'currency' | 'exchange' | 'name'
|
||||||
| 'assetSubClass'
|
> & { etfProvider: string; value: number };
|
||||||
| 'currency'
|
|
||||||
| 'exchange'
|
|
||||||
| 'name'
|
|
||||||
| 'value'
|
|
||||||
> & { etfProvider: string };
|
|
||||||
};
|
};
|
||||||
public sectors: {
|
public sectors: {
|
||||||
[name: string]: { name: string; value: number };
|
[name: string]: { name: string; value: number };
|
||||||
@ -84,7 +86,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
value: number;
|
value: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
public UNKNOWN_KEY = UNKNOWN_KEY;
|
||||||
public user: User;
|
public user: User;
|
||||||
public worldMapChartFormat: string;
|
public worldMapChartFormat: string;
|
||||||
|
|
||||||
@ -139,6 +141,8 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
? $localize`Filter by account or tag...`
|
? $localize`Filter by account or tag...`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
|
this.initialize();
|
||||||
|
|
||||||
return this.dataService.fetchPortfolioDetails({
|
return this.dataService.fetchPortfolioDetails({
|
||||||
filters: this.activeFilters
|
filters: this.activeFilters
|
||||||
});
|
});
|
||||||
@ -146,6 +150,8 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
takeUntil(this.unsubscribeSubject)
|
takeUntil(this.unsubscribeSubject)
|
||||||
)
|
)
|
||||||
.subscribe((portfolioDetails) => {
|
.subscribe((portfolioDetails) => {
|
||||||
|
this.initialize();
|
||||||
|
|
||||||
this.portfolioDetails = portfolioDetails;
|
this.portfolioDetails = portfolioDetails;
|
||||||
|
|
||||||
this.initializeAnalysisData();
|
this.initializeAnalysisData();
|
||||||
@ -223,20 +229,68 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.markets = {
|
this.markets = {
|
||||||
|
[UNKNOWN_KEY]: {
|
||||||
|
name: UNKNOWN_KEY,
|
||||||
|
value: 0
|
||||||
|
},
|
||||||
developedMarkets: {
|
developedMarkets: {
|
||||||
name: 'developedMarkets',
|
name: 'developedMarkets',
|
||||||
value: undefined
|
value: 0
|
||||||
},
|
},
|
||||||
emergingMarkets: {
|
emergingMarkets: {
|
||||||
name: 'emergingMarkets',
|
name: 'emergingMarkets',
|
||||||
value: undefined
|
value: 0
|
||||||
},
|
},
|
||||||
otherMarkets: {
|
otherMarkets: {
|
||||||
name: 'otherMarkets',
|
name: 'otherMarkets',
|
||||||
value: undefined
|
value: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.marketsAdvanced = {
|
||||||
|
[UNKNOWN_KEY]: {
|
||||||
|
id: UNKNOWN_KEY,
|
||||||
|
name: UNKNOWN_KEY,
|
||||||
|
value: 0
|
||||||
|
},
|
||||||
|
asiaPacific: {
|
||||||
|
id: 'asiaPacific',
|
||||||
|
name: translate('Asia-Pacific'),
|
||||||
|
value: 0
|
||||||
|
},
|
||||||
|
emergingMarkets: {
|
||||||
|
id: 'emergingMarkets',
|
||||||
|
name: translate('Emerging Markets'),
|
||||||
|
value: 0
|
||||||
|
},
|
||||||
|
europe: {
|
||||||
|
id: 'europe',
|
||||||
|
name: translate('Europe'),
|
||||||
|
value: 0
|
||||||
|
},
|
||||||
|
japan: {
|
||||||
|
id: 'japan',
|
||||||
|
name: translate('Japan'),
|
||||||
|
value: 0
|
||||||
|
},
|
||||||
|
northAmerica: {
|
||||||
|
id: 'northAmerica',
|
||||||
|
name: translate('North America'),
|
||||||
|
value: 0
|
||||||
|
},
|
||||||
|
otherMarkets: {
|
||||||
|
id: 'otherMarkets',
|
||||||
|
name: translate('Other Markets'),
|
||||||
|
value: 0
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.platforms = {};
|
this.platforms = {};
|
||||||
|
this.portfolioDetails = {
|
||||||
|
accounts: {},
|
||||||
|
filteredValueInPercentage: 0,
|
||||||
|
holdings: {},
|
||||||
|
platforms: {},
|
||||||
|
summary: undefined
|
||||||
|
};
|
||||||
this.positions = {};
|
this.positions = {};
|
||||||
this.sectors = {
|
this.sectors = {
|
||||||
[UNKNOWN_KEY]: {
|
[UNKNOWN_KEY]: {
|
||||||
@ -254,8 +308,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public initializeAnalysisData() {
|
public initializeAnalysisData() {
|
||||||
this.initialize();
|
|
||||||
|
|
||||||
for (const [
|
for (const [
|
||||||
id,
|
id,
|
||||||
{ name, valueInBaseCurrency, valueInPercentage }
|
{ name, valueInBaseCurrency, valueInPercentage }
|
||||||
@ -283,7 +335,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
if (this.hasImpersonationId) {
|
if (this.hasImpersonationId) {
|
||||||
value = position.allocationInPercentage;
|
value = position.allocationInPercentage;
|
||||||
} else {
|
} else {
|
||||||
value = position.value;
|
value = position.valueInBaseCurrency;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.positions[symbol] = {
|
this.positions[symbol] = {
|
||||||
@ -303,50 +355,109 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
// Prepare analysis data by continents, countries and sectors except for cash
|
// Prepare analysis data by continents, countries and sectors except for cash
|
||||||
|
|
||||||
if (position.countries.length > 0) {
|
if (position.countries.length > 0) {
|
||||||
if (!this.markets.developedMarkets.value) {
|
|
||||||
this.markets.developedMarkets.value = 0;
|
|
||||||
}
|
|
||||||
if (!this.markets.emergingMarkets.value) {
|
|
||||||
this.markets.emergingMarkets.value = 0;
|
|
||||||
}
|
|
||||||
if (!this.markets.otherMarkets.value) {
|
|
||||||
this.markets.otherMarkets.value = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.markets.developedMarkets.value +=
|
this.markets.developedMarkets.value +=
|
||||||
position.markets.developedMarkets * position.value;
|
position.markets.developedMarkets *
|
||||||
|
(isNumber(position.valueInBaseCurrency)
|
||||||
|
? position.valueInBaseCurrency
|
||||||
|
: position.valueInPercentage);
|
||||||
this.markets.emergingMarkets.value +=
|
this.markets.emergingMarkets.value +=
|
||||||
position.markets.emergingMarkets * position.value;
|
position.markets.emergingMarkets *
|
||||||
|
(isNumber(position.valueInBaseCurrency)
|
||||||
|
? position.valueInBaseCurrency
|
||||||
|
: position.valueInPercentage);
|
||||||
this.markets.otherMarkets.value +=
|
this.markets.otherMarkets.value +=
|
||||||
position.markets.otherMarkets * position.value;
|
position.markets.otherMarkets *
|
||||||
|
(isNumber(position.valueInBaseCurrency)
|
||||||
|
? position.valueInBaseCurrency
|
||||||
|
: position.valueInPercentage);
|
||||||
|
|
||||||
|
this.marketsAdvanced.asiaPacific.value +=
|
||||||
|
position.marketsAdvanced.asiaPacific *
|
||||||
|
(isNumber(position.valueInBaseCurrency)
|
||||||
|
? position.valueInBaseCurrency
|
||||||
|
: position.valueInPercentage);
|
||||||
|
this.marketsAdvanced.emergingMarkets.value +=
|
||||||
|
position.marketsAdvanced.emergingMarkets *
|
||||||
|
(isNumber(position.valueInBaseCurrency)
|
||||||
|
? position.valueInBaseCurrency
|
||||||
|
: position.valueInPercentage);
|
||||||
|
this.marketsAdvanced.europe.value +=
|
||||||
|
position.marketsAdvanced.europe *
|
||||||
|
(isNumber(position.valueInBaseCurrency)
|
||||||
|
? position.valueInBaseCurrency
|
||||||
|
: position.valueInPercentage);
|
||||||
|
this.marketsAdvanced.japan.value +=
|
||||||
|
position.marketsAdvanced.japan *
|
||||||
|
(isNumber(position.valueInBaseCurrency)
|
||||||
|
? position.valueInBaseCurrency
|
||||||
|
: position.valueInPercentage);
|
||||||
|
this.marketsAdvanced.northAmerica.value +=
|
||||||
|
position.marketsAdvanced.northAmerica *
|
||||||
|
(isNumber(position.valueInBaseCurrency)
|
||||||
|
? position.valueInBaseCurrency
|
||||||
|
: position.valueInPercentage);
|
||||||
|
|
||||||
for (const country of position.countries) {
|
for (const country of position.countries) {
|
||||||
const { code, continent, name, weight } = country;
|
const { code, continent, name, weight } = country;
|
||||||
|
|
||||||
if (this.continents[continent]?.value) {
|
if (this.continents[continent]?.value) {
|
||||||
this.continents[continent].value += weight * position.value;
|
this.continents[continent].value +=
|
||||||
|
weight *
|
||||||
|
(isNumber(position.valueInBaseCurrency)
|
||||||
|
? position.valueInBaseCurrency
|
||||||
|
: position.valueInPercentage);
|
||||||
} else {
|
} else {
|
||||||
this.continents[continent] = {
|
this.continents[continent] = {
|
||||||
name: continent,
|
name: continent,
|
||||||
value: weight * this.portfolioDetails.holdings[symbol].value
|
value:
|
||||||
|
weight *
|
||||||
|
(isNumber(position.valueInBaseCurrency)
|
||||||
|
? this.portfolioDetails.holdings[symbol].valueInBaseCurrency
|
||||||
|
: this.portfolioDetails.holdings[symbol].valueInPercentage)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.countries[code]?.value) {
|
if (this.countries[code]?.value) {
|
||||||
this.countries[code].value += weight * position.value;
|
this.countries[code].value +=
|
||||||
|
weight *
|
||||||
|
(isNumber(position.valueInBaseCurrency)
|
||||||
|
? position.valueInBaseCurrency
|
||||||
|
: position.valueInPercentage);
|
||||||
} else {
|
} else {
|
||||||
this.countries[code] = {
|
this.countries[code] = {
|
||||||
name,
|
name,
|
||||||
value: weight * this.portfolioDetails.holdings[symbol].value
|
value:
|
||||||
|
weight *
|
||||||
|
(isNumber(position.valueInBaseCurrency)
|
||||||
|
? this.portfolioDetails.holdings[symbol].valueInBaseCurrency
|
||||||
|
: this.portfolioDetails.holdings[symbol].valueInPercentage)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.continents[UNKNOWN_KEY].value +=
|
this.continents[UNKNOWN_KEY].value += isNumber(
|
||||||
this.portfolioDetails.holdings[symbol].value;
|
position.valueInBaseCurrency
|
||||||
|
)
|
||||||
|
? this.portfolioDetails.holdings[symbol].valueInBaseCurrency
|
||||||
|
: this.portfolioDetails.holdings[symbol].valueInPercentage;
|
||||||
|
|
||||||
this.countries[UNKNOWN_KEY].value +=
|
this.countries[UNKNOWN_KEY].value += isNumber(
|
||||||
this.portfolioDetails.holdings[symbol].value;
|
position.valueInBaseCurrency
|
||||||
|
)
|
||||||
|
? this.portfolioDetails.holdings[symbol].valueInBaseCurrency
|
||||||
|
: this.portfolioDetails.holdings[symbol].valueInPercentage;
|
||||||
|
|
||||||
|
this.markets[UNKNOWN_KEY].value += isNumber(
|
||||||
|
position.valueInBaseCurrency
|
||||||
|
)
|
||||||
|
? this.portfolioDetails.holdings[symbol].valueInBaseCurrency
|
||||||
|
: this.portfolioDetails.holdings[symbol].valueInPercentage;
|
||||||
|
|
||||||
|
this.marketsAdvanced[UNKNOWN_KEY].value += isNumber(
|
||||||
|
position.valueInBaseCurrency
|
||||||
|
)
|
||||||
|
? this.portfolioDetails.holdings[symbol].valueInBaseCurrency
|
||||||
|
: this.portfolioDetails.holdings[symbol].valueInPercentage;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (position.sectors.length > 0) {
|
if (position.sectors.length > 0) {
|
||||||
@ -354,17 +465,28 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
const { name, weight } = sector;
|
const { name, weight } = sector;
|
||||||
|
|
||||||
if (this.sectors[name]?.value) {
|
if (this.sectors[name]?.value) {
|
||||||
this.sectors[name].value += weight * position.value;
|
this.sectors[name].value +=
|
||||||
|
weight *
|
||||||
|
(isNumber(position.valueInBaseCurrency)
|
||||||
|
? position.valueInBaseCurrency
|
||||||
|
: position.valueInPercentage);
|
||||||
} else {
|
} else {
|
||||||
this.sectors[name] = {
|
this.sectors[name] = {
|
||||||
name,
|
name,
|
||||||
value: weight * this.portfolioDetails.holdings[symbol].value
|
value:
|
||||||
|
weight *
|
||||||
|
(isNumber(position.valueInBaseCurrency)
|
||||||
|
? this.portfolioDetails.holdings[symbol].valueInBaseCurrency
|
||||||
|
: this.portfolioDetails.holdings[symbol].valueInPercentage)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.sectors[UNKNOWN_KEY].value +=
|
this.sectors[UNKNOWN_KEY].value += isNumber(
|
||||||
this.portfolioDetails.holdings[symbol].value;
|
position.valueInBaseCurrency
|
||||||
|
)
|
||||||
|
? this.portfolioDetails.holdings[symbol].valueInBaseCurrency
|
||||||
|
: this.portfolioDetails.holdings[symbol].valueInPercentage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -372,8 +494,8 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
dataSource: position.dataSource,
|
dataSource: position.dataSource,
|
||||||
name: position.name,
|
name: position.name,
|
||||||
symbol: prettifySymbol(symbol),
|
symbol: prettifySymbol(symbol),
|
||||||
value: isNumber(position.value)
|
value: isNumber(position.valueInBaseCurrency)
|
||||||
? position.value
|
? position.valueInBaseCurrency
|
||||||
: position.valueInPercentage
|
: position.valueInPercentage
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -400,7 +522,8 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
const marketsTotal =
|
const marketsTotal =
|
||||||
this.markets.developedMarkets.value +
|
this.markets.developedMarkets.value +
|
||||||
this.markets.emergingMarkets.value +
|
this.markets.emergingMarkets.value +
|
||||||
this.markets.otherMarkets.value;
|
this.markets.otherMarkets.value +
|
||||||
|
this.markets[UNKNOWN_KEY].value;
|
||||||
|
|
||||||
this.markets.developedMarkets.value =
|
this.markets.developedMarkets.value =
|
||||||
this.markets.developedMarkets.value / marketsTotal;
|
this.markets.developedMarkets.value / marketsTotal;
|
||||||
@ -408,6 +531,8 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
this.markets.emergingMarkets.value / marketsTotal;
|
this.markets.emergingMarkets.value / marketsTotal;
|
||||||
this.markets.otherMarkets.value =
|
this.markets.otherMarkets.value =
|
||||||
this.markets.otherMarkets.value / marketsTotal;
|
this.markets.otherMarkets.value / marketsTotal;
|
||||||
|
this.markets[UNKNOWN_KEY].value =
|
||||||
|
this.markets[UNKNOWN_KEY].value / marketsTotal;
|
||||||
}
|
}
|
||||||
|
|
||||||
public onAccountChartClicked({ symbol }: UniqueAsset) {
|
public onAccountChartClicked({ symbol }: UniqueAsset) {
|
||||||
|
@ -174,7 +174,7 @@
|
|||||||
<mat-card appearance="outlined" class="mb-3">
|
<mat-card appearance="outlined" class="mb-3">
|
||||||
<mat-card-header class="overflow-hidden w-100">
|
<mat-card-header class="overflow-hidden w-100">
|
||||||
<mat-card-title class="align-items-center d-flex text-truncate"
|
<mat-card-title class="align-items-center d-flex text-truncate"
|
||||||
><span i18n>By Country</span
|
><span i18n>By Market</span
|
||||||
><gf-premium-indicator
|
><gf-premium-indicator
|
||||||
*ngIf="user?.subscription?.type === 'Basic'"
|
*ngIf="user?.subscription?.type === 'Basic'"
|
||||||
class="ml-1"
|
class="ml-1"
|
||||||
@ -186,10 +186,8 @@
|
|||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
[colorScheme]="user?.settings?.colorScheme"
|
[colorScheme]="user?.settings?.colorScheme"
|
||||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||||
[keys]="['name']"
|
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[maxItems]="10"
|
[positions]="marketsAdvanced"
|
||||||
[positions]="countries"
|
|
||||||
></gf-portfolio-proportion-chart>
|
></gf-portfolio-proportion-chart>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
@ -217,7 +215,7 @@
|
|||||||
></gf-world-map-chart>
|
></gf-world-map-chart>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
<div class="col-xs-12 col-md-3 my-2">
|
||||||
<gf-value
|
<gf-value
|
||||||
i18n
|
i18n
|
||||||
size="large"
|
size="large"
|
||||||
@ -226,7 +224,7 @@
|
|||||||
>Developed Markets</gf-value
|
>Developed Markets</gf-value
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
<div class="col-xs-12 col-md-3 my-2">
|
||||||
<gf-value
|
<gf-value
|
||||||
i18n
|
i18n
|
||||||
size="large"
|
size="large"
|
||||||
@ -235,7 +233,7 @@
|
|||||||
>Emerging Markets</gf-value
|
>Emerging Markets</gf-value
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
<div class="col-xs-12 col-md-3 my-2">
|
||||||
<gf-value
|
<gf-value
|
||||||
i18n
|
i18n
|
||||||
size="large"
|
size="large"
|
||||||
@ -244,12 +242,45 @@
|
|||||||
>Other Markets</gf-value
|
>Other Markets</gf-value
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-xs-12 col-md-3 my-2">
|
||||||
|
<gf-value
|
||||||
|
i18n
|
||||||
|
size="large"
|
||||||
|
[isPercent]="true"
|
||||||
|
[value]="markets?.[UNKNOWN_KEY]?.value"
|
||||||
|
>No data available</gf-value
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<mat-card appearance="outlined" class="mb-3">
|
||||||
|
<mat-card-header class="overflow-hidden w-100">
|
||||||
|
<mat-card-title class="align-items-center d-flex text-truncate"
|
||||||
|
><span i18n>By Country</span
|
||||||
|
><gf-premium-indicator
|
||||||
|
*ngIf="user?.subscription?.type === 'Basic'"
|
||||||
|
class="ml-1"
|
||||||
|
></gf-premium-indicator
|
||||||
|
></mat-card-title>
|
||||||
|
</mat-card-header>
|
||||||
|
<mat-card-content>
|
||||||
|
<gf-portfolio-proportion-chart
|
||||||
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
|
[colorScheme]="user?.settings?.colorScheme"
|
||||||
|
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||||
|
[keys]="['name']"
|
||||||
|
[locale]="user?.settings?.locale"
|
||||||
|
[maxItems]="10"
|
||||||
|
[positions]="countries"
|
||||||
|
></gf-portfolio-proportion-chart>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<mat-card appearance="outlined" class="mb-3">
|
<mat-card appearance="outlined" class="mb-3">
|
||||||
<mat-card-header class="overflow-hidden w-100">
|
<mat-card-header class="overflow-hidden w-100">
|
||||||
|
@ -51,7 +51,7 @@ export class FirePageComponent implements OnDestroy, OnInit {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fireWealth = new Big(summary.currentValue);
|
this.fireWealth = new Big(summary.fireWealth);
|
||||||
this.withdrawalRatePerYear = this.fireWealth.mul(4).div(100);
|
this.withdrawalRatePerYear = this.fireWealth.mul(4).div(100);
|
||||||
this.withdrawalRatePerMonth = this.withdrawalRatePerYear.div(12);
|
this.withdrawalRatePerMonth = this.withdrawalRatePerYear.div(12);
|
||||||
|
|
||||||
|
@ -33,18 +33,18 @@ export class PublicPageComponent implements OnInit {
|
|||||||
};
|
};
|
||||||
public portfolioPublicDetails: PortfolioPublicDetails;
|
public portfolioPublicDetails: PortfolioPublicDetails;
|
||||||
public positions: {
|
public positions: {
|
||||||
[symbol: string]: Pick<PortfolioPosition, 'currency' | 'name' | 'value'>;
|
[symbol: string]: Pick<PortfolioPosition, 'currency' | 'name'> & {
|
||||||
|
value: number;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
public positionsArray: Pick<
|
public positionsArray: PortfolioPublicDetails['holdings'][string][];
|
||||||
PortfolioPosition,
|
|
||||||
'currency' | 'name' | 'netPerformancePercent' | 'symbol' | 'value'
|
|
||||||
>[];
|
|
||||||
public sectors: {
|
public sectors: {
|
||||||
[name: string]: { name: string; value: number };
|
[name: string]: { name: string; value: number };
|
||||||
};
|
};
|
||||||
public symbols: {
|
public symbols: {
|
||||||
[name: string]: { name: string; symbol: string; value: number };
|
[name: string]: { name: string; symbol: string; value: number };
|
||||||
};
|
};
|
||||||
|
public UNKNOWN_KEY = UNKNOWN_KEY;
|
||||||
|
|
||||||
private id: string;
|
private id: string;
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
@ -100,6 +100,10 @@ export class PublicPageComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.markets = {
|
this.markets = {
|
||||||
|
[UNKNOWN_KEY]: {
|
||||||
|
name: UNKNOWN_KEY,
|
||||||
|
value: 0
|
||||||
|
},
|
||||||
developedMarkets: {
|
developedMarkets: {
|
||||||
name: 'developedMarkets',
|
name: 'developedMarkets',
|
||||||
value: 0
|
value: 0
|
||||||
@ -143,39 +147,47 @@ export class PublicPageComponent implements OnInit {
|
|||||||
|
|
||||||
if (position.countries.length > 0) {
|
if (position.countries.length > 0) {
|
||||||
this.markets.developedMarkets.value +=
|
this.markets.developedMarkets.value +=
|
||||||
position.markets.developedMarkets * position.value;
|
position.markets.developedMarkets * position.valueInBaseCurrency;
|
||||||
this.markets.emergingMarkets.value +=
|
this.markets.emergingMarkets.value +=
|
||||||
position.markets.emergingMarkets * position.value;
|
position.markets.emergingMarkets * position.valueInBaseCurrency;
|
||||||
this.markets.otherMarkets.value +=
|
this.markets.otherMarkets.value +=
|
||||||
position.markets.otherMarkets * position.value;
|
position.markets.otherMarkets * position.valueInBaseCurrency;
|
||||||
|
|
||||||
for (const country of position.countries) {
|
for (const country of position.countries) {
|
||||||
const { code, continent, name, weight } = country;
|
const { code, continent, name, weight } = country;
|
||||||
|
|
||||||
if (this.continents[continent]?.value) {
|
if (this.continents[continent]?.value) {
|
||||||
this.continents[continent].value += weight * position.value;
|
this.continents[continent].value +=
|
||||||
|
weight * position.valueInBaseCurrency;
|
||||||
} else {
|
} else {
|
||||||
this.continents[continent] = {
|
this.continents[continent] = {
|
||||||
name: continent,
|
name: continent,
|
||||||
value: weight * this.portfolioPublicDetails.holdings[symbol].value
|
value:
|
||||||
|
weight *
|
||||||
|
this.portfolioPublicDetails.holdings[symbol].valueInBaseCurrency
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.countries[code]?.value) {
|
if (this.countries[code]?.value) {
|
||||||
this.countries[code].value += weight * position.value;
|
this.countries[code].value += weight * position.valueInBaseCurrency;
|
||||||
} else {
|
} else {
|
||||||
this.countries[code] = {
|
this.countries[code] = {
|
||||||
name,
|
name,
|
||||||
value: weight * this.portfolioPublicDetails.holdings[symbol].value
|
value:
|
||||||
|
weight *
|
||||||
|
this.portfolioPublicDetails.holdings[symbol].valueInBaseCurrency
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.continents[UNKNOWN_KEY].value +=
|
this.continents[UNKNOWN_KEY].value +=
|
||||||
this.portfolioPublicDetails.holdings[symbol].value;
|
this.portfolioPublicDetails.holdings[symbol].valueInBaseCurrency;
|
||||||
|
|
||||||
this.countries[UNKNOWN_KEY].value +=
|
this.countries[UNKNOWN_KEY].value +=
|
||||||
this.portfolioPublicDetails.holdings[symbol].value;
|
this.portfolioPublicDetails.holdings[symbol].valueInBaseCurrency;
|
||||||
|
|
||||||
|
this.markets[UNKNOWN_KEY].value +=
|
||||||
|
this.portfolioPublicDetails.holdings[symbol].valueInBaseCurrency;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (position.sectors.length > 0) {
|
if (position.sectors.length > 0) {
|
||||||
@ -183,24 +195,26 @@ export class PublicPageComponent implements OnInit {
|
|||||||
const { name, weight } = sector;
|
const { name, weight } = sector;
|
||||||
|
|
||||||
if (this.sectors[name]?.value) {
|
if (this.sectors[name]?.value) {
|
||||||
this.sectors[name].value += weight * position.value;
|
this.sectors[name].value += weight * position.valueInBaseCurrency;
|
||||||
} else {
|
} else {
|
||||||
this.sectors[name] = {
|
this.sectors[name] = {
|
||||||
name,
|
name,
|
||||||
value: weight * this.portfolioPublicDetails.holdings[symbol].value
|
value:
|
||||||
|
weight *
|
||||||
|
this.portfolioPublicDetails.holdings[symbol].valueInBaseCurrency
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.sectors[UNKNOWN_KEY].value +=
|
this.sectors[UNKNOWN_KEY].value +=
|
||||||
this.portfolioPublicDetails.holdings[symbol].value;
|
this.portfolioPublicDetails.holdings[symbol].valueInBaseCurrency;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.symbols[prettifySymbol(symbol)] = {
|
this.symbols[prettifySymbol(symbol)] = {
|
||||||
name: position.name,
|
name: position.name,
|
||||||
symbol: prettifySymbol(symbol),
|
symbol: prettifySymbol(symbol),
|
||||||
value: isNumber(position.value)
|
value: isNumber(position.valueInBaseCurrency)
|
||||||
? position.value
|
? position.valueInBaseCurrency
|
||||||
: position.valueInPercentage
|
: position.valueInPercentage
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -208,7 +222,8 @@ export class PublicPageComponent implements OnInit {
|
|||||||
const marketsTotal =
|
const marketsTotal =
|
||||||
this.markets.developedMarkets.value +
|
this.markets.developedMarkets.value +
|
||||||
this.markets.emergingMarkets.value +
|
this.markets.emergingMarkets.value +
|
||||||
this.markets.otherMarkets.value;
|
this.markets.otherMarkets.value +
|
||||||
|
this.markets[UNKNOWN_KEY].value;
|
||||||
|
|
||||||
this.markets.developedMarkets.value =
|
this.markets.developedMarkets.value =
|
||||||
this.markets.developedMarkets.value / marketsTotal;
|
this.markets.developedMarkets.value / marketsTotal;
|
||||||
@ -216,6 +231,8 @@ export class PublicPageComponent implements OnInit {
|
|||||||
this.markets.emergingMarkets.value / marketsTotal;
|
this.markets.emergingMarkets.value / marketsTotal;
|
||||||
this.markets.otherMarkets.value =
|
this.markets.otherMarkets.value =
|
||||||
this.markets.otherMarkets.value / marketsTotal;
|
this.markets.otherMarkets.value / marketsTotal;
|
||||||
|
this.markets[UNKNOWN_KEY].value =
|
||||||
|
this.markets[UNKNOWN_KEY].value / marketsTotal;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
|
@ -84,7 +84,7 @@
|
|||||||
></gf-world-map-chart>
|
></gf-world-map-chart>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
<div class="col-xs-12 col-md-3 my-2">
|
||||||
<gf-value
|
<gf-value
|
||||||
i18n
|
i18n
|
||||||
size="large"
|
size="large"
|
||||||
@ -93,7 +93,7 @@
|
|||||||
>Developed Markets</gf-value
|
>Developed Markets</gf-value
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
<div class="col-xs-12 col-md-3 my-2">
|
||||||
<gf-value
|
<gf-value
|
||||||
i18n
|
i18n
|
||||||
size="large"
|
size="large"
|
||||||
@ -102,7 +102,7 @@
|
|||||||
>Emerging Markets</gf-value
|
>Emerging Markets</gf-value
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
<div class="col-xs-12 col-md-3 my-2">
|
||||||
<gf-value
|
<gf-value
|
||||||
i18n
|
i18n
|
||||||
size="large"
|
size="large"
|
||||||
@ -111,6 +111,15 @@
|
|||||||
>Other Markets</gf-value
|
>Other Markets</gf-value
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-xs-12 col-md-3 my-2">
|
||||||
|
<gf-value
|
||||||
|
i18n
|
||||||
|
size="large"
|
||||||
|
[isPercent]="true"
|
||||||
|
[value]="markets?.[UNKNOWN_KEY]?.value"
|
||||||
|
>No data available</gf-value
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user