Compare commits
47 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 |
95
CHANGELOG.md
95
CHANGELOG.md
@ -5,6 +5,101 @@ 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
|
## 1.287.0 - 2023-07-09
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
@ -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,9 +184,7 @@ export class AdminService {
|
|||||||
this.prismaService.symbolProfile.count({ where })
|
this.prismaService.symbolProfile.count({ where })
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
let marketData = assetProfiles.map(
|
||||||
count,
|
|
||||||
marketData: assetProfiles.map(
|
|
||||||
({
|
({
|
||||||
_count,
|
_count,
|
||||||
assetClass,
|
assetClass,
|
||||||
@ -211,7 +219,25 @@ export class AdminService {
|
|||||||
date: Order?.[0]?.date
|
date: Order?.[0]?.date
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
|
if (presetId) {
|
||||||
|
if (presetId === 'ETF_WITHOUT_COUNTRIES') {
|
||||||
|
marketData = marketData.filter(({ countriesCount }) => {
|
||||||
|
return countriesCount === 0;
|
||||||
|
});
|
||||||
|
} else if (presetId === 'ETF_WITHOUT_SECTORS') {
|
||||||
|
marketData = marketData.filter(({ sectorsCount }) => {
|
||||||
|
return sectorsCount === 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = (
|
||||||
|
await this.accountService.accounts({
|
||||||
orderBy: {
|
orderBy: {
|
||||||
name: 'asc'
|
name: 'asc'
|
||||||
},
|
},
|
||||||
select: {
|
|
||||||
accountType: true,
|
|
||||||
balance: true,
|
|
||||||
comment: true,
|
|
||||||
currency: true,
|
|
||||||
id: true,
|
|
||||||
isExcluded: true,
|
|
||||||
name: true,
|
|
||||||
platformId: true
|
|
||||||
},
|
|
||||||
where: { userId }
|
where: { userId }
|
||||||
});
|
})
|
||||||
|
).map(
|
||||||
|
({
|
||||||
|
accountType,
|
||||||
|
balance,
|
||||||
|
comment,
|
||||||
|
currency,
|
||||||
|
id,
|
||||||
|
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(
|
||||||
|
({ 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(dataGatheringItems),
|
this.dataProviderService.getQuotes({ items: dataGatheringItems }),
|
||||||
this.symbolProfileService.getSymbolProfiles(dataGatheringItems)
|
this.symbolProfileService.getSymbolProfiles(dataGatheringItems)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -536,12 +539,23 @@ 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
|
||||||
|
};
|
||||||
|
|
||||||
|
if (symbolProfile.countries.length > 0) {
|
||||||
for (const country of symbolProfile.countries) {
|
for (const country of symbolProfile.countries) {
|
||||||
if (developedMarkets.includes(country.code)) {
|
if (developedMarkets.includes(country.code)) {
|
||||||
markets.developedMarkets = new Big(markets.developedMarkets)
|
markets.developedMarkets = new Big(markets.developedMarkets)
|
||||||
@ -556,10 +570,48 @@ export class PortfolioService {
|
|||||||
.plus(country.weight)
|
.plus(country.weight)
|
||||||
.toNumber();
|
.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(
|
valueInBaseCurrencyOfEmergencyFundPositions.plus(valueInBaseCurrency);
|
||||||
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,12 +166,27 @@ 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
|
||||||
|
);
|
||||||
|
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);
|
currentPermissions.push(permissions.enableSubscriptionInterstitial);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (user.subscription?.type === 'Premium') {
|
if (user.subscription?.type === 'Premium') {
|
||||||
currentPermissions.push(permissions.reportDataGlitch);
|
currentPermissions.push(permissions.reportDataGlitch);
|
||||||
|
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"
|
||||||
|
]
|
@ -6,494 +6,514 @@
|
|||||||
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/de</loc>
|
<loc>https://ghostfol.io/de</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/de/blog</loc>
|
<loc>https://ghostfol.io/de/blog</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/de/blog/2021/07/hallo-ghostfolio</loc>
|
<loc>https://ghostfol.io/de/blog/2021/07/hallo-ghostfolio</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/de/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt</loc>
|
<loc>https://ghostfol.io/de/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/de/features</loc>
|
<loc>https://ghostfol.io/de/features</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/de/haeufig-gestellte-fragen</loc>
|
<loc>https://ghostfol.io/de/haeufig-gestellte-fragen</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/de/maerkte</loc>
|
<loc>https://ghostfol.io/de/maerkte</loc>
|
||||||
<changefreq>daily</changefreq>
|
<changefreq>daily</changefreq>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/de/open</loc>
|
<loc>https://ghostfol.io/de/open</loc>
|
||||||
<changefreq>daily</changefreq>
|
<changefreq>daily</changefreq>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/de/preise</loc>
|
<loc>https://ghostfol.io/de/preise</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/de/registrierung</loc>
|
<loc>https://ghostfol.io/de/registrierung</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/de/ressourcen</loc>
|
<loc>https://ghostfol.io/de/ressourcen</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/de/ueber-uns</loc>
|
<loc>https://ghostfol.io/de/ueber-uns</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/de/ueber-uns/changelog</loc>
|
<loc>https://ghostfol.io/de/ueber-uns/changelog</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/de/ueber-uns/datenschutzbestimmungen</loc>
|
<loc>https://ghostfol.io/de/ueber-uns/datenschutzbestimmungen</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/de/ueber-uns/lizenz</loc>
|
<loc>https://ghostfol.io/de/ueber-uns/lizenz</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en</loc>
|
<loc>https://ghostfol.io/en</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/about</loc>
|
<loc>https://ghostfol.io/en/about</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/about/changelog</loc>
|
<loc>https://ghostfol.io/en/about/changelog</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/about/license</loc>
|
<loc>https://ghostfol.io/en/about/license</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/blog</loc>
|
<loc>https://ghostfol.io/en/blog</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/blog/2021/07/hello-ghostfolio</loc>
|
<loc>https://ghostfol.io/en/blog/2021/07/hello-ghostfolio</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/blog/2022/01/ghostfolio-first-months-in-open-source</loc>
|
<loc>https://ghostfol.io/en/blog/2022/01/ghostfolio-first-months-in-open-source</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/blog/2022/07/ghostfolio-meets-internet-identity</loc>
|
<loc>https://ghostfol.io/en/blog/2022/07/ghostfolio-meets-internet-identity</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/blog/2022/07/how-do-i-get-my-finances-in-order</loc>
|
<loc>https://ghostfol.io/en/blog/2022/07/how-do-i-get-my-finances-in-order</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/blog/2022/08/500-stars-on-github</loc>
|
<loc>https://ghostfol.io/en/blog/2022/08/500-stars-on-github</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/blog/2022/10/hacktoberfest-2022</loc>
|
<loc>https://ghostfol.io/en/blog/2022/10/hacktoberfest-2022</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/blog/2022/11/black-friday-2022</loc>
|
<loc>https://ghostfol.io/en/blog/2022/11/black-friday-2022</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/blog/2022/12/the-importance-of-tracking-your-personal-finances</loc>
|
<loc>https://ghostfol.io/en/blog/2022/12/the-importance-of-tracking-your-personal-finances</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/blog/2023/02/ghostfolio-meets-umbrel</loc>
|
<loc>https://ghostfol.io/en/blog/2023/02/ghostfolio-meets-umbrel</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/blog/2023/03/ghostfolio-reaches-1000-stars-on-github</loc>
|
<loc>https://ghostfol.io/en/blog/2023/03/ghostfolio-reaches-1000-stars-on-github</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/blog/2023/05/unlock-your-financial-potential-with-ghostfolio</loc>
|
<loc>https://ghostfol.io/en/blog/2023/05/unlock-your-financial-potential-with-ghostfolio</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/blog/2023/07/exploring-the-path-to-fire</loc>
|
<loc>https://ghostfol.io/en/blog/2023/07/exploring-the-path-to-fire</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/faq</loc>
|
<loc>https://ghostfol.io/en/faq</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/features</loc>
|
<loc>https://ghostfol.io/en/features</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/markets</loc>
|
<loc>https://ghostfol.io/en/markets</loc>
|
||||||
<changefreq>daily</changefreq>
|
<changefreq>daily</changefreq>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/open</loc>
|
<loc>https://ghostfol.io/en/open</loc>
|
||||||
<changefreq>daily</changefreq>
|
<changefreq>daily</changefreq>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/pricing</loc>
|
<loc>https://ghostfol.io/en/pricing</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/register</loc>
|
<loc>https://ghostfol.io/en/register</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/resources</loc>
|
<loc>https://ghostfol.io/en/resources</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-altoo</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-altoo</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<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>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-divvydiary</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-divvydiary</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-exirio</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-exirio</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-folishare</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-folishare</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-getquin</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-getquin</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<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>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-justetf</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-justetf</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-kubera</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-kubera</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<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>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-maybe-finance</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-maybe-finance</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-monse</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-monse</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-parqet</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-parqet</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<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>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-portfolio-dividend-tracker</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-portfolio-dividend-tracker</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-portseido</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-portseido</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-projectionlab</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-projectionlab</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-seeking-alpha</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-seeking-alpha</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-sharesight</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-sharesight</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-simple-portfolio</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-simple-portfolio</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-snowball-analytics</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-snowball-analytics</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-sumio</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-sumio</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-utluna</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-utluna</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-yeekatee</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-yeekatee</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/es</loc>
|
<loc>https://ghostfol.io/es</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/es/funcionalidades</loc>
|
<loc>https://ghostfol.io/es/funcionalidades</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/es/mercados</loc>
|
<loc>https://ghostfol.io/es/mercados</loc>
|
||||||
<changefreq>daily</changefreq>
|
<changefreq>daily</changefreq>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/es/open</loc>
|
<loc>https://ghostfol.io/es/open</loc>
|
||||||
<changefreq>daily</changefreq>
|
<changefreq>daily</changefreq>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/es/precios</loc>
|
<loc>https://ghostfol.io/es/precios</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/es/preguntas-mas-frecuentes</loc>
|
<loc>https://ghostfol.io/es/preguntas-mas-frecuentes</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/es/recursos</loc>
|
<loc>https://ghostfol.io/es/recursos</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/es/registro</loc>
|
<loc>https://ghostfol.io/es/registro</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/es/sobre</loc>
|
<loc>https://ghostfol.io/es/sobre</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/es/sobre/changelog</loc>
|
<loc>https://ghostfol.io/es/sobre/changelog</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/es/sobre/licencia</loc>
|
<loc>https://ghostfol.io/es/sobre/licencia</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/es/sobre/politica-de-privacidad</loc>
|
<loc>https://ghostfol.io/es/sobre/politica-de-privacidad</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/fr</loc>
|
<loc>https://ghostfol.io/fr</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/fr/a-propos</loc>
|
<loc>https://ghostfol.io/fr/a-propos</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/fr/a-propos/changelog</loc>
|
<loc>https://ghostfol.io/fr/a-propos/changelog</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/fr/a-propos/licence</loc>
|
<loc>https://ghostfol.io/fr/a-propos/licence</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/fr/a-propos/politique-de-confidentialite</loc>
|
<loc>https://ghostfol.io/fr/a-propos/politique-de-confidentialite</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/fr/enregistrement</loc>
|
<loc>https://ghostfol.io/fr/enregistrement</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/fr/fonctionnalites</loc>
|
<loc>https://ghostfol.io/fr/fonctionnalites</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/fr/foire-aux-questions</loc>
|
<loc>https://ghostfol.io/fr/foire-aux-questions</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/fr/marches</loc>
|
<loc>https://ghostfol.io/fr/marches</loc>
|
||||||
<changefreq>daily</changefreq>
|
<changefreq>daily</changefreq>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/fr/open</loc>
|
<loc>https://ghostfol.io/fr/open</loc>
|
||||||
<changefreq>daily</changefreq>
|
<changefreq>daily</changefreq>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/fr/prix</loc>
|
<loc>https://ghostfol.io/fr/prix</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/fr/ressources</loc>
|
<loc>https://ghostfol.io/fr/ressources</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/it</loc>
|
<loc>https://ghostfol.io/it</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/it/domande-piu-frequenti</loc>
|
<loc>https://ghostfol.io/it/domande-piu-frequenti</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/it/funzionalita</loc>
|
<loc>https://ghostfol.io/it/funzionalita</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/it/informazioni-su</loc>
|
<loc>https://ghostfol.io/it/informazioni-su</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/it/informazioni-su/changelog</loc>
|
<loc>https://ghostfol.io/it/informazioni-su/changelog</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/it/informazioni-su/licenza</loc>
|
<loc>https://ghostfol.io/it/informazioni-su/licenza</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/it/informazioni-su/informativa-sulla-privacy</loc>
|
<loc>https://ghostfol.io/it/informazioni-su/informativa-sulla-privacy</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/it/iscrizione</loc>
|
<loc>https://ghostfol.io/it/iscrizione</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/it/mercati</loc>
|
<loc>https://ghostfol.io/it/mercati</loc>
|
||||||
<changefreq>daily</changefreq>
|
<changefreq>daily</changefreq>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/it/open</loc>
|
<loc>https://ghostfol.io/it/open</loc>
|
||||||
<changefreq>daily</changefreq>
|
<changefreq>daily</changefreq>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/it/prezzi</loc>
|
<loc>https://ghostfol.io/it/prezzi</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/it/risorse</loc>
|
<loc>https://ghostfol.io/it/risorse</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/nl</loc>
|
<loc>https://ghostfol.io/nl</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/nl/bronnen</loc>
|
<loc>https://ghostfol.io/nl/bronnen</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/nl/kenmerken</loc>
|
<loc>https://ghostfol.io/nl/kenmerken</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/nl/markten</loc>
|
<loc>https://ghostfol.io/nl/markten</loc>
|
||||||
<changefreq>daily</changefreq>
|
<changefreq>daily</changefreq>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/nl/open</loc>
|
<loc>https://ghostfol.io/nl/open</loc>
|
||||||
<changefreq>daily</changefreq>
|
<changefreq>daily</changefreq>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/nl/over</loc>
|
<loc>https://ghostfol.io/nl/over</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/nl/over/changelog</loc>
|
<loc>https://ghostfol.io/nl/over/changelog</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/nl/over/licentie</loc>
|
<loc>https://ghostfol.io/nl/over/licentie</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/nl/over/privacybeleid</loc>
|
<loc>https://ghostfol.io/nl/over/privacybeleid</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/nl/prijzen</loc>
|
<loc>https://ghostfol.io/nl/prijzen</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/nl/registratie</loc>
|
<loc>https://ghostfol.io/nl/registratie</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/nl/vaak-gestelde-vragen</loc>
|
<loc>https://ghostfol.io/nl/vaak-gestelde-vragen</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/pt/blog</loc>
|
<loc>https://ghostfol.io/pt/blog</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/pt/funcionalidades</loc>
|
<loc>https://ghostfol.io/pt/funcionalidades</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/pt/mercados</loc>
|
<loc>https://ghostfol.io/pt/mercados</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/pt/open</loc>
|
<loc>https://ghostfol.io/pt/open</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/pt/perguntas-mais-frequentes</loc>
|
<loc>https://ghostfol.io/pt/perguntas-mais-frequentes</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/pt/precos</loc>
|
<loc>https://ghostfol.io/pt/precos</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/pt/recursos</loc>
|
<loc>https://ghostfol.io/pt/recursos</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/pt/registo</loc>
|
<loc>https://ghostfol.io/pt/registo</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/pt/sobre</loc>
|
<loc>https://ghostfol.io/pt/sobre</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/pt/sobre/changelog</loc>
|
<loc>https://ghostfol.io/pt/sobre/changelog</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/pt/sobre/licenca</loc>
|
<loc>https://ghostfol.io/pt/sobre/licenca</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/pt/sobre/politica-de-privacidade</loc>
|
<loc>https://ghostfol.io/pt/sobre/politica-de-privacidade</loc>
|
||||||
<lastmod>2023-07-07T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
</urlset>
|
</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,
|
dataSource,
|
||||||
symbol
|
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,9 +249,10 @@ 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) {
|
||||||
|
if (useCache) {
|
||||||
const quoteString = await this.redisCacheService.get(
|
const quoteString = await this.redisCacheService.get(
|
||||||
this.redisCacheService.getQuoteKey({ dataSource, symbol })
|
this.redisCacheService.getQuoteKey({ dataSource, symbol })
|
||||||
);
|
);
|
||||||
@ -249,12 +261,12 @@ export class DataProviderService {
|
|||||||
try {
|
try {
|
||||||
const cachedDataProviderResponse = JSON.parse(quoteString);
|
const cachedDataProviderResponse = JSON.parse(quoteString);
|
||||||
response[symbol] = cachedDataProviderResponse;
|
response[symbol] = cachedDataProviderResponse;
|
||||||
|
continue;
|
||||||
} catch {}
|
} 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": "",
|
||||||
|
@ -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) => {
|
]
|
||||||
|
.map((assetSubClass) => {
|
||||||
return {
|
return {
|
||||||
id: assetSubClass,
|
id: assetSubClass.toString(),
|
||||||
label: translate(assetSubClass),
|
label: translate(assetSubClass),
|
||||||
type: 'ASSET_SUB_CLASS'
|
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,17 +1,69 @@
|
|||||||
<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
|
||||||
|
*ngIf="hasPermissionToCreateOrder && historicalDataItems?.length === 0; else isUserActive"
|
||||||
|
class="justify-content-center row w-100"
|
||||||
|
>
|
||||||
|
<div class="col introduction">
|
||||||
|
<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 }"
|
||||||
|
>
|
||||||
|
<a class="d-block" [routerLink]="['/accounts']"
|
||||||
|
><span i18n>Setup your accounts</span><br />
|
||||||
|
<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="row w-100">
|
||||||
<div class="col p-0">
|
<div class="col p-0">
|
||||||
<div class="chart-container mx-auto position-relative">
|
<div class="chart-container mx-auto position-relative">
|
||||||
<div
|
|
||||||
*ngIf="hasPermissionToCreateOrder && historicalDataItems?.length === 0"
|
|
||||||
class="align-items-center d-flex h-100 justify-content-center w-100"
|
|
||||||
>
|
|
||||||
<div class="d-flex justify-content-center">
|
|
||||||
<gf-no-transactions-info-indicator></gf-no-transactions-info-indicator>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<gf-line-chart
|
<gf-line-chart
|
||||||
class="position-absolute"
|
class="position-absolute"
|
||||||
symbol="Performance"
|
symbol="Performance"
|
||||||
@ -54,4 +106,5 @@
|
|||||||
</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">
|
||||||
|
<form class="w-100" (ngSubmit)="onLoginWithAccessToken()">
|
||||||
<mat-form-field appearance="outline" class="without-hint w-100">
|
<mat-form-field appearance="outline" class="without-hint w-100">
|
||||||
<mat-label i18n>Security Token</mat-label>
|
<mat-label i18n>Security Token</mat-label>
|
||||||
<textarea
|
<input
|
||||||
cdkTextareaAutosize
|
|
||||||
matInput
|
matInput
|
||||||
type="text"
|
name="password"
|
||||||
|
[type]="isAccessTokenHidden ? 'password' : 'text'"
|
||||||
[(ngModel)]="data.accessToken"
|
[(ngModel)]="data.accessToken"
|
||||||
></textarea>
|
/>
|
||||||
|
<button
|
||||||
|
mat-button
|
||||||
|
matSuffix
|
||||||
|
type="button"
|
||||||
|
(click)="isAccessTokenHidden = !isAccessTokenHidden"
|
||||||
|
>
|
||||||
|
<ion-icon
|
||||||
|
[name]="isAccessTokenHidden ? 'eye-outline' : 'eye-off-outline'"
|
||||||
|
></ion-icon>
|
||||||
|
</button>
|
||||||
</mat-form-field>
|
</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>
|
||||||
|
@ -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
|
||||||
|
@ -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<
|
};
|
||||||
PortfolioPosition,
|
public positionsArray: PortfolioPublicDetails['holdings'][string][];
|
||||||
'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>
|
||||||
|
@ -21,9 +21,15 @@
|
|||||||
financial future.
|
financial future.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Ghostfolio is open source software (OSS) where a community of
|
Ghostfolio is an open source software (OSS), providing a
|
||||||
developers, contributors, and enthusiasts collaborate to enhance its
|
cost-effective alternative to {{ product2.name }} making it
|
||||||
capabilities, security, and user experience.
|
particularly suitable for individuals on a tight budget, such as
|
||||||
|
those
|
||||||
|
<a href="../en/blog/2023/07/exploring-the-path-to-fire"
|
||||||
|
>pursuing Financial Independence, Retire Early (FIRE)</a
|
||||||
|
>. By leveraging the collective efforts of a community of developers
|
||||||
|
and personal finance enthusiasts, Ghostfolio continuously enhances
|
||||||
|
its capabilities, security, and user experience.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Let’s dive deeper into the detailed comparison table below to gain a
|
Let’s dive deeper into the detailed comparison table below to gain a
|
||||||
@ -69,11 +75,21 @@
|
|||||||
<td class="mat-mdc-cell px-3 py-2 text-right" i18n>
|
<td class="mat-mdc-cell px-3 py-2 text-right" i18n>
|
||||||
Available in
|
Available in
|
||||||
</td>
|
</td>
|
||||||
<td class="mat-mdc-cell px-1 py-2">{{ product1.languages }}</td>
|
<td class="mat-mdc-cell px-1 py-2">
|
||||||
<td class="mat-mdc-cell px-1 py-2">{{ product2.languages }}</td>
|
<ng-container
|
||||||
|
*ngFor="let language of product1.languages; last as isLast"
|
||||||
|
>{{ language }}{{ isLast ? '' : ', ' }}</ng-container
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
<td class="mat-mdc-cell px-1 py-2">
|
||||||
|
<ng-container
|
||||||
|
*ngFor="let language of product2.languages; last as isLast"
|
||||||
|
>{{ language }}{{ isLast ? '' : ', ' }}</ng-container
|
||||||
|
>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="mat-mdc-row">
|
<tr class="mat-mdc-row">
|
||||||
<td class="mat-mdc-cell px-3 py-2 text-right" i18n>
|
<td class="mat-mdc-cell px-3 py-2 text-right">
|
||||||
Open Source Software
|
Open Source Software
|
||||||
</td>
|
</td>
|
||||||
<td class="mat-mdc-cell px-1 py-2">
|
<td class="mat-mdc-cell px-1 py-2">
|
||||||
@ -118,6 +134,25 @@
|
|||||||
>
|
>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr class="mat-mdc-row">
|
||||||
|
<td class="mat-mdc-cell px-3 py-2 text-right" i18n>
|
||||||
|
Use anonymously
|
||||||
|
</td>
|
||||||
|
<td class="mat-mdc-cell px-1 py-2">
|
||||||
|
<ng-container *ngIf="product1.useAnonymously === true" i18n
|
||||||
|
>✅ Yes</ng-container
|
||||||
|
><ng-container *ngIf="product1.useAnonymously === false" i18n
|
||||||
|
>❌ No</ng-container
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
<td class="mat-mdc-cell px-1 py-2">
|
||||||
|
<ng-container *ngIf="product2.useAnonymously === true" i18n
|
||||||
|
>✅ Yes</ng-container
|
||||||
|
><ng-container *ngIf="product2.useAnonymously === false" i18n
|
||||||
|
>❌ No</ng-container
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr class="mat-mdc-row">
|
<tr class="mat-mdc-row">
|
||||||
<td class="mat-mdc-cell px-3 py-2 text-right" i18n>
|
<td class="mat-mdc-cell px-3 py-2 text-right" i18n>
|
||||||
Free Plan
|
Free Plan
|
||||||
@ -157,7 +192,19 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</section>
|
</section>
|
||||||
<section class="mb-4 py-3">
|
<section class="mb-4">
|
||||||
|
<p>
|
||||||
|
Please note that the information provided is based on our
|
||||||
|
independent research and analysis. This website is not affiliated
|
||||||
|
with {{ product2.name }} or any other product mentioned in the
|
||||||
|
comparison. As the landscape of personal finance tools evolves, it
|
||||||
|
is essential to verify any specific details or changes directly from
|
||||||
|
the respective product page. Data needs a refresh? Help us maintain
|
||||||
|
accurate data on
|
||||||
|
<a href="https://github.com/ghostfolio/ghostfolio">GitHub</a>.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<section class="call-to-action mb-4 py-3 rounded">
|
||||||
<h2 class="h4 mb-0 text-center">
|
<h2 class="h4 mb-0 text-center">
|
||||||
Ready to take your <strong>investments</strong> to the
|
Ready to take your <strong>investments</strong> to the
|
||||||
<strong>next level</strong>?
|
<strong>next level</strong>?
|
||||||
@ -172,18 +219,6 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section class="mb-4">
|
|
||||||
<small>
|
|
||||||
Please note that the information provided is based on our
|
|
||||||
independent research and analysis. This website is not affiliated
|
|
||||||
with {{ product2.name }} or any other product mentioned in the
|
|
||||||
comparison. As the landscape of personal finance tools evolves, it
|
|
||||||
is essential to verify any specific details or changes directly from
|
|
||||||
the respective product page. Data needs a refresh? Help us maintain
|
|
||||||
accurate data on
|
|
||||||
<a href="https://github.com/ghostfolio/ghostfolio">GitHub</a>.
|
|
||||||
</small>
|
|
||||||
</section>
|
|
||||||
<section class="mb-4">
|
<section class="mb-4">
|
||||||
<ul class="list-inline">
|
<ul class="list-inline">
|
||||||
<li class="list-inline-item">
|
<li class="list-inline-item">
|
||||||
|
@ -10,8 +10,16 @@
|
|||||||
color: rgba(var(--palette-primary-300), 1);
|
color: rgba(var(--palette-primary-300), 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.call-to-action {
|
||||||
|
background-color: rgba(var(--palette-foreground-text), 0.02);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:host-context(.is-dark-theme) {
|
:host-context(.is-dark-theme) {
|
||||||
color: rgb(var(--light-primary-text));
|
color: rgb(var(--light-primary-text));
|
||||||
|
|
||||||
|
.call-to-action {
|
||||||
|
background-color: rgba(var(--palette-foreground-text-dark), 0.02);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,23 @@
|
|||||||
import { Product } from '@ghostfolio/common/interfaces';
|
import { Product } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import { AltooPageComponent } from './products/altoo-page.component';
|
import { AltooPageComponent } from './products/altoo-page.component';
|
||||||
|
import { CopilotMoneyPageComponent } from './products/copilot-money-page.component';
|
||||||
|
import { DeltaPageComponent } from './products/delta-page.component';
|
||||||
import { DivvyDiaryPageComponent } from './products/divvydiary-page.component';
|
import { DivvyDiaryPageComponent } from './products/divvydiary-page.component';
|
||||||
import { ExirioPageComponent } from './products/exirio-page.component';
|
import { ExirioPageComponent } from './products/exirio-page.component';
|
||||||
import { FolisharePageComponent } from './products/folishare-page.component';
|
import { FolisharePageComponent } from './products/folishare-page.component';
|
||||||
import { GetquinPageComponent } from './products/getquin-page.component';
|
import { GetquinPageComponent } from './products/getquin-page.component';
|
||||||
|
import { GoSpatzPageComponent } from './products/gospatz-page.component';
|
||||||
import { JustEtfPageComponent } from './products/justetf-page.component';
|
import { JustEtfPageComponent } from './products/justetf-page.component';
|
||||||
import { KuberaPageComponent } from './products/kubera-page.component';
|
import { KuberaPageComponent } from './products/kubera-page.component';
|
||||||
|
import { MarketsShPageComponent } from './products/markets.sh-page.component';
|
||||||
import { MaybeFinancePageComponent } from './products/maybe-finance-page.component';
|
import { MaybeFinancePageComponent } from './products/maybe-finance-page.component';
|
||||||
import { MonsePageComponent } from './products/monse-page.component';
|
import { MonsePageComponent } from './products/monse-page.component';
|
||||||
import { ParqetPageComponent } from './products/parqet-page.component';
|
import { ParqetPageComponent } from './products/parqet-page.component';
|
||||||
|
import { PlannixPageComponent } from './products/plannix-page.component';
|
||||||
import { PortfolioDividendTrackerPageComponent } from './products/portfolio-dividend-tracker-page.component';
|
import { PortfolioDividendTrackerPageComponent } from './products/portfolio-dividend-tracker-page.component';
|
||||||
import { PortseidoPageComponent } from './products/portseido-page.component';
|
import { PortseidoPageComponent } from './products/portseido-page.component';
|
||||||
|
import { ProjectionLabPageComponent } from './products/projectionlab-page.component';
|
||||||
import { SeekingAlphaPageComponent } from './products/seeking-alpha-page.component';
|
import { SeekingAlphaPageComponent } from './products/seeking-alpha-page.component';
|
||||||
import { SharesightPageComponent } from './products/sharesight-page.component';
|
import { SharesightPageComponent } from './products/sharesight-page.component';
|
||||||
import { SimplePortfolioPageComponent } from './products/simple-portfolio-page.component';
|
import { SimplePortfolioPageComponent } from './products/simple-portfolio-page.component';
|
||||||
@ -19,7 +25,6 @@ import { SnowballAnalyticsPageComponent } from './products/snowball-analytics-pa
|
|||||||
import { SumioPageComponent } from './products/sumio-page.component';
|
import { SumioPageComponent } from './products/sumio-page.component';
|
||||||
import { UtlunaPageComponent } from './products/utluna-page.component';
|
import { UtlunaPageComponent } from './products/utluna-page.component';
|
||||||
import { YeekateePageComponent } from './products/yeekatee-page.component';
|
import { YeekateePageComponent } from './products/yeekatee-page.component';
|
||||||
import { ProjectionLabPageComponent } from './products/projectionlab-page.component';
|
|
||||||
|
|
||||||
export const products: Product[] = [
|
export const products: Product[] = [
|
||||||
{
|
{
|
||||||
@ -29,12 +34,21 @@ export const products: Product[] = [
|
|||||||
hasSelfHostingAbility: true,
|
hasSelfHostingAbility: true,
|
||||||
isOpenSource: true,
|
isOpenSource: true,
|
||||||
key: 'ghostfolio',
|
key: 'ghostfolio',
|
||||||
languages: 'Dutch, English, French, German, Italian, Portuguese, Spanish',
|
languages: [
|
||||||
|
'Dutch',
|
||||||
|
'English',
|
||||||
|
'French',
|
||||||
|
'German',
|
||||||
|
'Italian',
|
||||||
|
'Portuguese',
|
||||||
|
'Spanish'
|
||||||
|
],
|
||||||
name: 'Ghostfolio',
|
name: 'Ghostfolio',
|
||||||
origin: 'Switzerland',
|
origin: 'Switzerland',
|
||||||
pricingPerYear: '$19',
|
pricingPerYear: '$19',
|
||||||
region: 'Global',
|
region: 'Global',
|
||||||
slogan: 'Open Source Wealth Management'
|
slogan: 'Open Source Wealth Management',
|
||||||
|
useAnonymously: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: AltooPageComponent,
|
component: AltooPageComponent,
|
||||||
@ -46,6 +60,30 @@ export const products: Product[] = [
|
|||||||
origin: 'Switzerland',
|
origin: 'Switzerland',
|
||||||
slogan: 'Simplicity for Complex Wealth'
|
slogan: 'Simplicity for Complex Wealth'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
component: CopilotMoneyPageComponent,
|
||||||
|
founded: 2019,
|
||||||
|
hasFreePlan: false,
|
||||||
|
hasSelfHostingAbility: false,
|
||||||
|
isOpenSource: false,
|
||||||
|
key: 'copilot-money',
|
||||||
|
name: 'Copilot Money',
|
||||||
|
origin: 'United States',
|
||||||
|
pricingPerYear: '$70',
|
||||||
|
slogan: 'Do money better with Copilot'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: DeltaPageComponent,
|
||||||
|
founded: 2017,
|
||||||
|
hasFreePlan: true,
|
||||||
|
hasSelfHostingAbility: false,
|
||||||
|
isOpenSource: false,
|
||||||
|
key: 'delta',
|
||||||
|
name: 'Delta Investment Tracker',
|
||||||
|
note: 'Acquired by eToro',
|
||||||
|
origin: 'Belgium',
|
||||||
|
slogan: 'The app to track all your investments. Make smart moves only.'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: DivvyDiaryPageComponent,
|
component: DivvyDiaryPageComponent,
|
||||||
founded: 2019,
|
founded: 2019,
|
||||||
@ -53,7 +91,7 @@ export const products: Product[] = [
|
|||||||
hasSelfHostingAbility: false,
|
hasSelfHostingAbility: false,
|
||||||
isOpenSource: false,
|
isOpenSource: false,
|
||||||
key: 'divvydiary',
|
key: 'divvydiary',
|
||||||
languages: 'English, German',
|
languages: ['English', 'German'],
|
||||||
name: 'DivvyDiary',
|
name: 'DivvyDiary',
|
||||||
origin: 'Germany',
|
origin: 'Germany',
|
||||||
pricingPerYear: '€65',
|
pricingPerYear: '€65',
|
||||||
@ -77,7 +115,7 @@ export const products: Product[] = [
|
|||||||
hasSelfHostingAbility: false,
|
hasSelfHostingAbility: false,
|
||||||
isOpenSource: false,
|
isOpenSource: false,
|
||||||
key: 'folishare',
|
key: 'folishare',
|
||||||
languages: 'English, German',
|
languages: ['English', 'German'],
|
||||||
name: 'folishare',
|
name: 'folishare',
|
||||||
origin: 'Austria',
|
origin: 'Austria',
|
||||||
pricingPerYear: '$65',
|
pricingPerYear: '$65',
|
||||||
@ -90,12 +128,22 @@ export const products: Product[] = [
|
|||||||
hasSelfHostingAbility: false,
|
hasSelfHostingAbility: false,
|
||||||
isOpenSource: false,
|
isOpenSource: false,
|
||||||
key: 'getquin',
|
key: 'getquin',
|
||||||
languages: 'English, German',
|
languages: ['English', 'German'],
|
||||||
name: 'getquin',
|
name: 'getquin',
|
||||||
origin: 'Germany',
|
origin: 'Germany',
|
||||||
pricingPerYear: '€48',
|
pricingPerYear: '€48',
|
||||||
slogan: 'Portfolio Tracker, Analysis & Community'
|
slogan: 'Portfolio Tracker, Analysis & Community'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
component: GoSpatzPageComponent,
|
||||||
|
hasFreePlan: true,
|
||||||
|
hasSelfHostingAbility: false,
|
||||||
|
isOpenSource: false,
|
||||||
|
key: 'gospatz',
|
||||||
|
name: 'goSPATZ',
|
||||||
|
origin: 'Germany',
|
||||||
|
slogan: 'Volle Kontrolle über deine Investitionen'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: JustEtfPageComponent,
|
component: JustEtfPageComponent,
|
||||||
founded: 2011,
|
founded: 2011,
|
||||||
@ -120,13 +168,27 @@ export const products: Product[] = [
|
|||||||
pricingPerYear: '$150',
|
pricingPerYear: '$150',
|
||||||
slogan: 'The Time Machine for your Net Worth'
|
slogan: 'The Time Machine for your Net Worth'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
component: MarketsShPageComponent,
|
||||||
|
founded: 2022,
|
||||||
|
hasFreePlan: true,
|
||||||
|
hasSelfHostingAbility: false,
|
||||||
|
isOpenSource: false,
|
||||||
|
key: 'markets.sh',
|
||||||
|
languages: ['English'],
|
||||||
|
name: 'markets.sh',
|
||||||
|
origin: 'Germany',
|
||||||
|
pricingPerYear: '€168',
|
||||||
|
region: 'Global',
|
||||||
|
slogan: 'Track your investments'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: MaybeFinancePageComponent,
|
component: MaybeFinancePageComponent,
|
||||||
founded: 2021,
|
founded: 2021,
|
||||||
hasSelfHostingAbility: false,
|
hasSelfHostingAbility: false,
|
||||||
isOpenSource: false,
|
isOpenSource: false,
|
||||||
key: 'maybe-finance',
|
key: 'maybe-finance',
|
||||||
languages: 'English',
|
languages: ['English'],
|
||||||
name: 'Maybe Finance',
|
name: 'Maybe Finance',
|
||||||
note: 'Sunset in 2023',
|
note: 'Sunset in 2023',
|
||||||
origin: 'United States',
|
origin: 'United States',
|
||||||
@ -158,13 +220,23 @@ export const products: Product[] = [
|
|||||||
region: 'Austria, Germany, Switzerland',
|
region: 'Austria, Germany, Switzerland',
|
||||||
slogan: 'Dein Vermögen immer im Blick'
|
slogan: 'Dein Vermögen immer im Blick'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
component: PlannixPageComponent,
|
||||||
|
founded: 2023,
|
||||||
|
hasSelfHostingAbility: false,
|
||||||
|
isOpenSource: false,
|
||||||
|
key: 'plannix',
|
||||||
|
name: 'Plannix',
|
||||||
|
origin: 'Italy',
|
||||||
|
slogan: 'Your Personal Finance Hub'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: PortfolioDividendTrackerPageComponent,
|
component: PortfolioDividendTrackerPageComponent,
|
||||||
hasFreePlan: false,
|
hasFreePlan: false,
|
||||||
hasSelfHostingAbility: false,
|
hasSelfHostingAbility: false,
|
||||||
isOpenSource: false,
|
isOpenSource: false,
|
||||||
key: 'portfolio-dividend-tracker',
|
key: 'portfolio-dividend-tracker',
|
||||||
languages: 'English, Dutch',
|
languages: ['English', 'Dutch'],
|
||||||
name: 'Portfolio Dividend Tracker',
|
name: 'Portfolio Dividend Tracker',
|
||||||
origin: 'Netherlands',
|
origin: 'Netherlands',
|
||||||
pricingPerYear: '€60',
|
pricingPerYear: '€60',
|
||||||
@ -177,7 +249,7 @@ export const products: Product[] = [
|
|||||||
hasSelfHostingAbility: false,
|
hasSelfHostingAbility: false,
|
||||||
isOpenSource: false,
|
isOpenSource: false,
|
||||||
key: 'portseido',
|
key: 'portseido',
|
||||||
languages: 'Dutch, English, French, German',
|
languages: ['Dutch', 'English', 'French', 'German'],
|
||||||
name: 'Portseido',
|
name: 'Portseido',
|
||||||
origin: 'Thailand',
|
origin: 'Thailand',
|
||||||
pricingPerYear: '$96',
|
pricingPerYear: '$96',
|
||||||
@ -260,11 +332,12 @@ export const products: Product[] = [
|
|||||||
hasSelfHostingAbility: false,
|
hasSelfHostingAbility: false,
|
||||||
isOpenSource: false,
|
isOpenSource: false,
|
||||||
key: 'utluna',
|
key: 'utluna',
|
||||||
languages: 'English, French, German',
|
languages: ['English', 'French', 'German'],
|
||||||
name: 'Utluna',
|
name: 'Utluna',
|
||||||
origin: 'Switzerland',
|
origin: 'Switzerland',
|
||||||
pricingPerYear: '$300',
|
pricingPerYear: '$300',
|
||||||
slogan: 'Your Portfolio. Revealed.'
|
slogan: 'Your Portfolio. Revealed.',
|
||||||
|
useAnonymously: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: YeekateePageComponent,
|
component: YeekateePageComponent,
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { products } from '../products';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
host: { class: 'page' },
|
||||||
|
imports: [CommonModule, MatButtonModule, RouterModule],
|
||||||
|
selector: 'gf-copilot-money-page',
|
||||||
|
standalone: true,
|
||||||
|
styleUrls: ['../product-page-template.scss'],
|
||||||
|
templateUrl: '../product-page-template.html'
|
||||||
|
})
|
||||||
|
export class CopilotMoneyPageComponent {
|
||||||
|
public product1 = products.find(({ key }) => {
|
||||||
|
return key === 'ghostfolio';
|
||||||
|
});
|
||||||
|
|
||||||
|
public product2 = products.find(({ key }) => {
|
||||||
|
return key === 'copilot-money';
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { products } from '../products';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
host: { class: 'page' },
|
||||||
|
imports: [CommonModule, MatButtonModule, RouterModule],
|
||||||
|
selector: 'gf-delta-page',
|
||||||
|
standalone: true,
|
||||||
|
styleUrls: ['../product-page-template.scss'],
|
||||||
|
templateUrl: '../product-page-template.html'
|
||||||
|
})
|
||||||
|
export class DeltaPageComponent {
|
||||||
|
public product1 = products.find(({ key }) => {
|
||||||
|
return key === 'ghostfolio';
|
||||||
|
});
|
||||||
|
|
||||||
|
public product2 = products.find(({ key }) => {
|
||||||
|
return key === 'delta';
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { products } from '../products';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
host: { class: 'page' },
|
||||||
|
imports: [CommonModule, MatButtonModule, RouterModule],
|
||||||
|
selector: 'gf-gospatz-page',
|
||||||
|
standalone: true,
|
||||||
|
styleUrls: ['../product-page-template.scss'],
|
||||||
|
templateUrl: '../product-page-template.html'
|
||||||
|
})
|
||||||
|
export class GoSpatzPageComponent {
|
||||||
|
public product1 = products.find(({ key }) => {
|
||||||
|
return key === 'ghostfolio';
|
||||||
|
});
|
||||||
|
|
||||||
|
public product2 = products.find(({ key }) => {
|
||||||
|
return key === 'gospatz';
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { products } from '../products';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
host: { class: 'page' },
|
||||||
|
imports: [CommonModule, MatButtonModule, RouterModule],
|
||||||
|
selector: 'gf-markets-sh-page',
|
||||||
|
standalone: true,
|
||||||
|
styleUrls: ['../product-page-template.scss'],
|
||||||
|
templateUrl: '../product-page-template.html'
|
||||||
|
})
|
||||||
|
export class MarketsShPageComponent {
|
||||||
|
public product1 = products.find(({ key }) => {
|
||||||
|
return key === 'ghostfolio';
|
||||||
|
});
|
||||||
|
|
||||||
|
public product2 = products.find(({ key }) => {
|
||||||
|
return key === 'markets.sh';
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { products } from '../products';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
host: { class: 'page' },
|
||||||
|
imports: [CommonModule, MatButtonModule, RouterModule],
|
||||||
|
selector: 'gf-plannix-page',
|
||||||
|
standalone: true,
|
||||||
|
styleUrls: ['../product-page-template.scss'],
|
||||||
|
templateUrl: '../product-page-template.html'
|
||||||
|
})
|
||||||
|
export class PlannixPageComponent {
|
||||||
|
public product1 = products.find(({ key }) => {
|
||||||
|
return key === 'ghostfolio';
|
||||||
|
});
|
||||||
|
|
||||||
|
public product2 = products.find(({ key }) => {
|
||||||
|
return key === 'plannix';
|
||||||
|
});
|
||||||
|
}
|
@ -1,4 +1,7 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
|
import { InfoItem } from '@ghostfolio/common/interfaces';
|
||||||
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -8,11 +11,21 @@ import { Subject } from 'rxjs';
|
|||||||
templateUrl: './resources-page.html'
|
templateUrl: './resources-page.html'
|
||||||
})
|
})
|
||||||
export class ResourcesPageComponent implements OnInit {
|
export class ResourcesPageComponent implements OnInit {
|
||||||
|
public hasPermissionForSubscription: boolean;
|
||||||
|
public info: InfoItem;
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
public constructor() {}
|
public constructor(private dataService: DataService) {
|
||||||
|
this.info = this.dataService.fetchInfo();
|
||||||
|
}
|
||||||
|
|
||||||
public ngOnInit() {}
|
public ngOnInit() {
|
||||||
|
this.hasPermissionForSubscription = hasPermission(
|
||||||
|
this.info?.globalPermissions,
|
||||||
|
permissions.enableSubscription
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
this.unsubscribeSubject.next();
|
this.unsubscribeSubject.next();
|
||||||
|
@ -170,7 +170,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4 media">
|
<div *ngIf="hasPermissionForSubscription" class="mb-4 media">
|
||||||
<div class="media-body">
|
<div class="media-body">
|
||||||
<h3 class="h5 mt-0">Personal Finance Tools</h3>
|
<h3 class="h5 mt-0">Personal Finance Tools</h3>
|
||||||
<div class="mb-1">
|
<div class="mb-1">
|
||||||
@ -179,7 +179,7 @@
|
|||||||
monitor investments, and make informed financial decisions.
|
monitor investments, and make informed financial decisions.
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a i18n [routerLink]="['/resources', 'personal-finance-tools']"
|
<a [routerLink]="['/resources', 'personal-finance-tools']"
|
||||||
>Personal Finance Tools →</a
|
>Personal Finance Tools →</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
@ -19,6 +19,7 @@ import { DataSource, MarketData, Platform, Prisma } from '@prisma/client';
|
|||||||
import { JobStatus } from 'bull';
|
import { JobStatus } from 'bull';
|
||||||
import { format, parseISO } from 'date-fns';
|
import { format, parseISO } from 'date-fns';
|
||||||
import { Observable, map } from 'rxjs';
|
import { Observable, map } from 'rxjs';
|
||||||
|
|
||||||
import { DataService } from './data.service';
|
import { DataService } from './data.service';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@ -94,7 +95,9 @@ export class AdminService {
|
|||||||
params = params.append('sortDirection', sortDirection);
|
params = params.append('sortDirection', sortDirection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (take) {
|
||||||
params = params.append('take', take);
|
params = params.append('take', take);
|
||||||
|
}
|
||||||
|
|
||||||
return this.http.get<AdminMarketData>('/api/v1/admin/market-data', {
|
return this.http.get<AdminMarketData>('/api/v1/admin/market-data', {
|
||||||
params
|
params
|
||||||
|
@ -57,6 +57,7 @@ export class DataService {
|
|||||||
ACCOUNT: filtersByAccount,
|
ACCOUNT: filtersByAccount,
|
||||||
ASSET_CLASS: filtersByAssetClass,
|
ASSET_CLASS: filtersByAssetClass,
|
||||||
ASSET_SUB_CLASS: filtersByAssetSubClass,
|
ASSET_SUB_CLASS: filtersByAssetSubClass,
|
||||||
|
PRESET_ID: filtersByPresetId,
|
||||||
TAG: filtersByTag
|
TAG: filtersByTag
|
||||||
} = groupBy(filters, (filter) => {
|
} = groupBy(filters, (filter) => {
|
||||||
return filter.type;
|
return filter.type;
|
||||||
@ -95,6 +96,10 @@ export class DataService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (filtersByPresetId) {
|
||||||
|
params = params.append('presetId', filtersByPresetId[0].id);
|
||||||
|
}
|
||||||
|
|
||||||
if (filtersByTag) {
|
if (filtersByTag) {
|
||||||
params = params.append(
|
params = params.append(
|
||||||
'tags',
|
'tags',
|
||||||
@ -410,10 +415,10 @@ export class DataService {
|
|||||||
map((response) => {
|
map((response) => {
|
||||||
if (response.holdings) {
|
if (response.holdings) {
|
||||||
for (const symbol of Object.keys(response.holdings)) {
|
for (const symbol of Object.keys(response.holdings)) {
|
||||||
response.holdings[symbol].value = isNumber(
|
response.holdings[symbol].valueInBaseCurrency = isNumber(
|
||||||
response.holdings[symbol].value
|
response.holdings[symbol].valueInBaseCurrency
|
||||||
)
|
)
|
||||||
? response.holdings[symbol].value
|
? response.holdings[symbol].valueInBaseCurrency
|
||||||
: response.holdings[symbol].valueInPercentage;
|
: response.holdings[symbol].valueInPercentage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,7 @@ import { getDate, getMonth, getYear, parse, subDays } from 'date-fns';
|
|||||||
import { de, es, fr, it, nl, pt } from 'date-fns/locale';
|
import { de, es, fr, it, nl, pt } from 'date-fns/locale';
|
||||||
|
|
||||||
import { ghostfolioScraperApiSymbolPrefix, locale } from './config';
|
import { ghostfolioScraperApiSymbolPrefix, locale } from './config';
|
||||||
import { Benchmark } from './interfaces';
|
import { Benchmark, UniqueAsset } from './interfaces';
|
||||||
import { ColorScheme } from './types';
|
import { ColorScheme } from './types';
|
||||||
|
|
||||||
const NUMERIC_REGEXP = /[-]{0,1}[\d]*[.,]{0,1}[\d]+/g;
|
const NUMERIC_REGEXP = /[-]{0,1}[\d]*[.,]{0,1}[\d]+/g;
|
||||||
@ -64,6 +64,10 @@ export function extractNumberFromString(aString: string): number {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getAssetProfileIdentifier({ dataSource, symbol }: UniqueAsset) {
|
||||||
|
return `${dataSource}-${symbol}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function getBackgroundColor(aColorScheme: ColorScheme) {
|
export function getBackgroundColor(aColorScheme: ColorScheme) {
|
||||||
return getCssVariable(
|
return getCssVariable(
|
||||||
aColorScheme === 'DARK' ||
|
aColorScheme === 'DARK' ||
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
export interface Filter {
|
export interface Filter {
|
||||||
id: string;
|
id: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
type: 'ACCOUNT' | 'ASSET_CLASS' | 'ASSET_SUB_CLASS' | 'SYMBOL' | 'TAG';
|
type:
|
||||||
|
| 'ACCOUNT'
|
||||||
|
| 'ASSET_CLASS'
|
||||||
|
| 'ASSET_SUB_CLASS'
|
||||||
|
| 'PRESET_ID'
|
||||||
|
| 'SYMBOL'
|
||||||
|
| 'TAG';
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { AssetClass, AssetSubClass, DataSource } from '@prisma/client';
|
import { AssetClass, AssetSubClass, DataSource, Tag } from '@prisma/client';
|
||||||
|
|
||||||
import { Market, MarketState } from '../types';
|
import { Market, MarketAdvanced, MarketState } from '../types';
|
||||||
import { Country } from './country.interface';
|
import { Country } from './country.interface';
|
||||||
import { Sector } from './sector.interface';
|
import { Sector } from './sector.interface';
|
||||||
|
|
||||||
@ -20,16 +20,18 @@ export interface PortfolioPosition {
|
|||||||
marketChangePercent?: number;
|
marketChangePercent?: number;
|
||||||
marketPrice: number;
|
marketPrice: number;
|
||||||
markets?: { [key in Market]: number };
|
markets?: { [key in Market]: number };
|
||||||
|
marketsAdvanced?: { [key in MarketAdvanced]: number };
|
||||||
marketState: MarketState;
|
marketState: MarketState;
|
||||||
name: string;
|
name: string;
|
||||||
netPerformance: number;
|
netPerformance: number;
|
||||||
netPerformancePercent: number;
|
netPerformancePercent: number;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
sectors: Sector[];
|
sectors: Sector[];
|
||||||
transactionCount: number;
|
|
||||||
symbol: string;
|
symbol: string;
|
||||||
|
tags?: Tag[];
|
||||||
|
transactionCount: number;
|
||||||
type?: string;
|
type?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
value?: number;
|
valueInBaseCurrency?: number;
|
||||||
valueInPercentage?: number;
|
valueInPercentage?: number;
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ export interface PortfolioPublicDetails {
|
|||||||
| 'sectors'
|
| 'sectors'
|
||||||
| 'symbol'
|
| 'symbol'
|
||||||
| 'url'
|
| 'url'
|
||||||
| 'value'
|
| 'valueInBaseCurrency'
|
||||||
| 'valueInPercentage'
|
| 'valueInPercentage'
|
||||||
>;
|
>;
|
||||||
};
|
};
|
||||||
|
@ -5,9 +5,14 @@ export interface PortfolioSummary extends PortfolioPerformance {
|
|||||||
cash: number;
|
cash: number;
|
||||||
committedFunds: number;
|
committedFunds: number;
|
||||||
dividend: number;
|
dividend: number;
|
||||||
emergencyFund: number;
|
emergencyFund: {
|
||||||
|
assets: number;
|
||||||
|
cash: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
excludedAccountsAndActivities: number;
|
excludedAccountsAndActivities: number;
|
||||||
fees: number;
|
fees: number;
|
||||||
|
fireWealth: number;
|
||||||
firstOrderDate: Date;
|
firstOrderDate: Date;
|
||||||
items: number;
|
items: number;
|
||||||
liabilities: number;
|
liabilities: number;
|
||||||
|
@ -5,11 +5,12 @@ export interface Product {
|
|||||||
hasSelfHostingAbility?: boolean;
|
hasSelfHostingAbility?: boolean;
|
||||||
isOpenSource: boolean;
|
isOpenSource: boolean;
|
||||||
key: string;
|
key: string;
|
||||||
languages?: string;
|
languages?: string[];
|
||||||
name: string;
|
name: string;
|
||||||
note?: string;
|
note?: string;
|
||||||
origin?: string;
|
origin?: string;
|
||||||
pricingPerYear?: string;
|
pricingPerYear?: string;
|
||||||
region?: string;
|
region?: string;
|
||||||
slogan?: string;
|
slogan?: string;
|
||||||
|
useAnonymously?: boolean;
|
||||||
}
|
}
|
||||||
|
@ -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 TimelinePosition {
|
export interface TimelinePosition {
|
||||||
@ -15,5 +15,6 @@ export interface TimelinePosition {
|
|||||||
netPerformancePercentage: Big;
|
netPerformancePercentage: Big;
|
||||||
quantity: Big;
|
quantity: Big;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
|
tags?: Tag[];
|
||||||
transactionCount: number;
|
transactionCount: number;
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,8 @@ import type { ColorScheme } from './color-scheme.type';
|
|||||||
import type { DateRange } from './date-range.type';
|
import type { DateRange } from './date-range.type';
|
||||||
import type { Granularity } from './granularity.type';
|
import type { Granularity } from './granularity.type';
|
||||||
import type { GroupBy } from './group-by.type';
|
import type { GroupBy } from './group-by.type';
|
||||||
|
import type { MarketAdvanced } from './market-advanced.type';
|
||||||
|
import type { MarketDataPreset } from './market-data-preset.type';
|
||||||
import type { MarketState } from './market-state.type';
|
import type { MarketState } from './market-state.type';
|
||||||
import type { Market } from './market.type';
|
import type { Market } from './market.type';
|
||||||
import type { OrderWithAccount } from './order-with-account.type';
|
import type { OrderWithAccount } from './order-with-account.type';
|
||||||
@ -23,6 +25,8 @@ export type {
|
|||||||
Granularity,
|
Granularity,
|
||||||
GroupBy,
|
GroupBy,
|
||||||
Market,
|
Market,
|
||||||
|
MarketAdvanced,
|
||||||
|
MarketDataPreset,
|
||||||
MarketState,
|
MarketState,
|
||||||
OrderWithAccount,
|
OrderWithAccount,
|
||||||
RequestWithUser,
|
RequestWithUser,
|
||||||
|
8
libs/common/src/lib/types/market-advanced.type.ts
Normal file
8
libs/common/src/lib/types/market-advanced.type.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export type MarketAdvanced =
|
||||||
|
| 'asiaPacific'
|
||||||
|
| 'emergingMarkets'
|
||||||
|
| 'europe'
|
||||||
|
| 'japan'
|
||||||
|
| 'northAmerica'
|
||||||
|
| 'otherMarkets'
|
||||||
|
| 'UNKNOWN';
|
1
libs/common/src/lib/types/market-data-preset.type.ts
Normal file
1
libs/common/src/lib/types/market-data-preset.type.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type MarketDataPreset = 'ETF_WITHOUT_COUNTRIES' | 'ETF_WITHOUT_SECTORS';
|
@ -1 +1,5 @@
|
|||||||
export type Market = 'developedMarkets' | 'emergingMarkets' | 'otherMarkets';
|
export type Market =
|
||||||
|
| 'developedMarkets'
|
||||||
|
| 'emergingMarkets'
|
||||||
|
| 'otherMarkets'
|
||||||
|
| 'UNKNOWN';
|
||||||
|
@ -383,13 +383,14 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getTotalValue() {
|
private getTotalValue() {
|
||||||
let totalValue = new Big(0);
|
|
||||||
const paginatedData = this.getPaginatedData();
|
const paginatedData = this.getPaginatedData();
|
||||||
for (const activity of paginatedData) {
|
let totalValue = new Big(0);
|
||||||
if (isNumber(activity.valueInBaseCurrency)) {
|
|
||||||
if (activity.type === 'BUY' || activity.type === 'ITEM') {
|
for (const { type, valueInBaseCurrency } of paginatedData) {
|
||||||
totalValue = totalValue.plus(activity.valueInBaseCurrency);
|
if (isNumber(valueInBaseCurrency)) {
|
||||||
} else if (activity.type === 'SELL') {
|
if (type === 'BUY' || type === 'ITEM') {
|
||||||
|
totalValue = totalValue.plus(valueInBaseCurrency);
|
||||||
|
} else if (type === 'LIABILITY' || type === 'SELL') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -61,7 +61,7 @@
|
|||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="value">
|
<ng-container matColumnDef="valueInBaseCurrency">
|
||||||
<th
|
<th
|
||||||
*matHeaderCellDef
|
*matHeaderCellDef
|
||||||
class="d-none d-lg-table-cell justify-content-end px-1"
|
class="d-none d-lg-table-cell justify-content-end px-1"
|
||||||
@ -79,7 +79,7 @@
|
|||||||
<gf-value
|
<gf-value
|
||||||
[isCurrency]="true"
|
[isCurrency]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[value]="isLoading ? undefined : element.value"
|
[value]="isLoading ? undefined : element.valueInBaseCurrency"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -55,7 +55,7 @@ export class HoldingsTableComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
this.displayedColumns = ['icon', 'nameWithSymbol', 'dateOfFirstActivity'];
|
this.displayedColumns = ['icon', 'nameWithSymbol', 'dateOfFirstActivity'];
|
||||||
|
|
||||||
if (this.hasPermissionToShowValues) {
|
if (this.hasPermissionToShowValues) {
|
||||||
this.displayedColumns.push('value');
|
this.displayedColumns.push('valueInBaseCurrency');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.displayedColumns.push('allocationInPercentage');
|
this.displayedColumns.push('allocationInPercentage');
|
||||||
|
@ -2,6 +2,7 @@ import '@angular/localize/init';
|
|||||||
|
|
||||||
const locales = {
|
const locales = {
|
||||||
ACCOUNT: $localize`Account`,
|
ACCOUNT: $localize`Account`,
|
||||||
|
'Asia-Pacific': $localize`Asia-Pacific`,
|
||||||
ASSET_CLASS: $localize`Asset Class`,
|
ASSET_CLASS: $localize`Asset Class`,
|
||||||
ASSET_SUB_CLASS: $localize`Asset Sub Class`,
|
ASSET_SUB_CLASS: $localize`Asset Sub Class`,
|
||||||
CORE: $localize`Core`,
|
CORE: $localize`Core`,
|
||||||
@ -12,10 +13,12 @@ const locales = {
|
|||||||
GRANT: $localize`Grant`,
|
GRANT: $localize`Grant`,
|
||||||
HIGHER_RISK: $localize`Higher Risk`,
|
HIGHER_RISK: $localize`Higher Risk`,
|
||||||
IMPORT_ACTIVITY_ERROR_IS_DUPLICATE: $localize`This activity already exists.`,
|
IMPORT_ACTIVITY_ERROR_IS_DUPLICATE: $localize`This activity already exists.`,
|
||||||
|
Japan: $localize`Japan`,
|
||||||
LOWER_RISK: $localize`Lower Risk`,
|
LOWER_RISK: $localize`Lower Risk`,
|
||||||
MONTH: $localize`Month`,
|
MONTH: $localize`Month`,
|
||||||
MONTHS: $localize`Months`,
|
MONTHS: $localize`Months`,
|
||||||
OTHER: $localize`Other`,
|
OTHER: $localize`Other`,
|
||||||
|
PRESET_ID: $localize`Preset`,
|
||||||
RETIREMENT_PROVISION: $localize`Retirement Provision`,
|
RETIREMENT_PROVISION: $localize`Retirement Provision`,
|
||||||
SATELLITE: $localize`Satellite`,
|
SATELLITE: $localize`Satellite`,
|
||||||
SECURITIES: $localize`Securities`,
|
SECURITIES: $localize`Securities`,
|
||||||
@ -24,6 +27,13 @@ const locales = {
|
|||||||
YEAR: $localize`Year`,
|
YEAR: $localize`Year`,
|
||||||
YEARS: $localize`Years`,
|
YEARS: $localize`Years`,
|
||||||
|
|
||||||
|
// Activity types
|
||||||
|
BUY: $localize`Buy`,
|
||||||
|
DIVIDEND: $localize`Dividend`,
|
||||||
|
ITEM: $localize`Valuable`,
|
||||||
|
LIABILITY: $localize`Liability`,
|
||||||
|
SELL: $localize`Sell`,
|
||||||
|
|
||||||
// enum AssetClass
|
// enum AssetClass
|
||||||
CASH: $localize`Cash`,
|
CASH: $localize`Cash`,
|
||||||
COMMODITY: $localize`Commodity`,
|
COMMODITY: $localize`Commodity`,
|
||||||
|
@ -90,7 +90,7 @@ export class PortfolioProportionChartComponent
|
|||||||
[symbol: string]: {
|
[symbol: string]: {
|
||||||
color?: string;
|
color?: string;
|
||||||
name: string;
|
name: string;
|
||||||
subCategory: { [symbol: string]: { value: Big } };
|
subCategory?: { [symbol: string]: { value: Big } };
|
||||||
value: Big;
|
value: Big;
|
||||||
};
|
};
|
||||||
} = {};
|
} = {};
|
||||||
@ -99,12 +99,14 @@ export class PortfolioProportionChartComponent
|
|||||||
[UNKNOWN_KEY]: `rgba(${getTextColor(this.colorScheme)}, 0.12)`
|
[UNKNOWN_KEY]: `rgba(${getTextColor(this.colorScheme)}, 0.12)`
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (this.keys.length > 0) {
|
||||||
Object.keys(this.positions).forEach((symbol) => {
|
Object.keys(this.positions).forEach((symbol) => {
|
||||||
if (this.positions[symbol][this.keys[0]]?.toUpperCase()) {
|
if (this.positions[symbol][this.keys[0]]?.toUpperCase()) {
|
||||||
if (chartData[this.positions[symbol][this.keys[0]].toUpperCase()]) {
|
if (chartData[this.positions[symbol][this.keys[0]].toUpperCase()]) {
|
||||||
chartData[this.positions[symbol][this.keys[0]].toUpperCase()].value =
|
|
||||||
chartData[
|
chartData[
|
||||||
this.positions[symbol][this.keys[0]].toUpperCase()
|
this.positions[symbol][this.keys[0]].toUpperCase()
|
||||||
|
].value = chartData[
|
||||||
|
this.positions[symbol][this.keys[0]].toUpperCase()
|
||||||
].value.plus(this.positions[symbol].value);
|
].value.plus(this.positions[symbol].value);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -122,8 +124,9 @@ export class PortfolioProportionChartComponent
|
|||||||
} else {
|
} else {
|
||||||
chartData[
|
chartData[
|
||||||
this.positions[symbol][this.keys[0]].toUpperCase()
|
this.positions[symbol][this.keys[0]].toUpperCase()
|
||||||
].subCategory[this.positions[symbol][this.keys[1]] ?? UNKNOWN_KEY] =
|
].subCategory[
|
||||||
{ value: new Big(this.positions[symbol].value) };
|
this.positions[symbol][this.keys[1]] ?? UNKNOWN_KEY
|
||||||
|
] = { value: new Big(this.positions[symbol].value) };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
chartData[this.positions[symbol][this.keys[0]].toUpperCase()] = {
|
chartData[this.positions[symbol][this.keys[0]].toUpperCase()] = {
|
||||||
@ -158,6 +161,14 @@ export class PortfolioProportionChartComponent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
Object.keys(this.positions).forEach((symbol) => {
|
||||||
|
chartData[symbol] = {
|
||||||
|
name: this.positions[symbol].name,
|
||||||
|
value: new Big(this.positions[symbol].value)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let chartDataSorted = Object.entries(chartData)
|
let chartDataSorted = Object.entries(chartData)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
|
10
package.json
10
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ghostfolio",
|
"name": "ghostfolio",
|
||||||
"version": "1.287.0",
|
"version": "1.296.0",
|
||||||
"homepage": "https://ghostfol.io",
|
"homepage": "https://ghostfol.io",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -79,7 +79,7 @@
|
|||||||
"@nestjs/platform-express": "9.1.4",
|
"@nestjs/platform-express": "9.1.4",
|
||||||
"@nestjs/schedule": "2.1.0",
|
"@nestjs/schedule": "2.1.0",
|
||||||
"@nestjs/serve-static": "3.0.0",
|
"@nestjs/serve-static": "3.0.0",
|
||||||
"@prisma/client": "4.15.0",
|
"@prisma/client": "4.16.2",
|
||||||
"@simplewebauthn/browser": "5.2.1",
|
"@simplewebauthn/browser": "5.2.1",
|
||||||
"@simplewebauthn/server": "5.2.1",
|
"@simplewebauthn/server": "5.2.1",
|
||||||
"@stripe/stripe-js": "1.47.0",
|
"@stripe/stripe-js": "1.47.0",
|
||||||
@ -120,14 +120,14 @@
|
|||||||
"passport": "0.6.0",
|
"passport": "0.6.0",
|
||||||
"passport-google-oauth20": "2.0.0",
|
"passport-google-oauth20": "2.0.0",
|
||||||
"passport-jwt": "4.0.0",
|
"passport-jwt": "4.0.0",
|
||||||
"prisma": "4.15.0",
|
"prisma": "4.16.2",
|
||||||
"reflect-metadata": "0.1.13",
|
"reflect-metadata": "0.1.13",
|
||||||
"rxjs": "7.5.6",
|
"rxjs": "7.5.6",
|
||||||
"stripe": "11.12.0",
|
"stripe": "11.12.0",
|
||||||
"svgmap": "2.6.0",
|
"svgmap": "2.6.0",
|
||||||
"twitter-api-v2": "1.14.2",
|
"twitter-api-v2": "1.14.2",
|
||||||
"uuid": "9.0.0",
|
"uuid": "9.0.0",
|
||||||
"yahoo-finance2": "2.4.1",
|
"yahoo-finance2": "2.4.3",
|
||||||
"zone.js": "0.12.0"
|
"zone.js": "0.12.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -165,7 +165,7 @@
|
|||||||
"@types/color": "3.0.3",
|
"@types/color": "3.0.3",
|
||||||
"@types/google-spreadsheet": "3.1.5",
|
"@types/google-spreadsheet": "3.1.5",
|
||||||
"@types/jest": "29.4.4",
|
"@types/jest": "29.4.4",
|
||||||
"@types/lodash": "4.14.191",
|
"@types/lodash": "4.14.195",
|
||||||
"@types/marked": "4.0.8",
|
"@types/marked": "4.0.8",
|
||||||
"@types/node": "18.11.18",
|
"@types/node": "18.11.18",
|
||||||
"@types/papaparse": "5.3.7",
|
"@types/papaparse": "5.3.7",
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "AccountBalance" (
|
||||||
|
"accountId" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"value" DOUBLE PRECISION NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "AccountBalance_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "AccountBalance" ADD CONSTRAINT "AccountBalance_accountId_userId_fkey" FOREIGN KEY ("accountId", "userId") REFERENCES "Account"("id", "userId") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- Migrate current account balance to time series (AccountBalance[])
|
||||||
|
INSERT INTO "AccountBalance" ("accountId", "createdAt", "date", "id", "updatedAt", "userId", "value")
|
||||||
|
SELECT
|
||||||
|
"id",
|
||||||
|
"updatedAt",
|
||||||
|
"updatedAt",
|
||||||
|
"id",
|
||||||
|
"updatedAt",
|
||||||
|
"userId",
|
||||||
|
"balance"
|
||||||
|
FROM "Account";
|
@ -23,6 +23,7 @@ model Access {
|
|||||||
model Account {
|
model Account {
|
||||||
accountType AccountType @default(SECURITIES)
|
accountType AccountType @default(SECURITIES)
|
||||||
balance Float @default(0)
|
balance Float @default(0)
|
||||||
|
balances AccountBalance[]
|
||||||
comment String?
|
comment String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
currency String?
|
currency String?
|
||||||
@ -40,6 +41,17 @@ model Account {
|
|||||||
@@id([id, userId])
|
@@id([id, userId])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model AccountBalance {
|
||||||
|
accountId String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
date DateTime @default(now())
|
||||||
|
id String @id @default(uuid())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
userId String
|
||||||
|
value Float
|
||||||
|
Account Account @relation(fields: [accountId, userId], onDelete: Cascade, references: [id, userId])
|
||||||
|
}
|
||||||
|
|
||||||
model Analytics {
|
model Analytics {
|
||||||
activityCount Int @default(0)
|
activityCount Int @default(0)
|
||||||
country String?
|
country String?
|
||||||
|
52
yarn.lock
52
yarn.lock
@ -3738,22 +3738,22 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
|
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
|
||||||
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
|
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
|
||||||
|
|
||||||
"@prisma/client@4.15.0":
|
"@prisma/client@4.16.2":
|
||||||
version "4.15.0"
|
version "4.16.2"
|
||||||
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-4.15.0.tgz#f52ec6ca6fbde37395a54b0a9e5da603a9de15f3"
|
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-4.16.2.tgz#3bb9ebd49b35c8236b3d468d0215192267016e2b"
|
||||||
integrity sha512-xnROvyABcGiwqRNdrObHVZkD9EjkJYHOmVdlKy1yGgI+XOzvMzJ4tRg3dz1pUlsyhKxXGCnjIQjWW+2ur+YXuw==
|
integrity sha512-qCoEyxv1ZrQ4bKy39GnylE8Zq31IRmm8bNhNbZx7bF2cU5aiCCnSa93J2imF88MBjn7J9eUQneNxUQVJdl/rPQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@prisma/engines-version" "4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944"
|
"@prisma/engines-version" "4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81"
|
||||||
|
|
||||||
"@prisma/engines-version@4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944":
|
"@prisma/engines-version@4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81":
|
||||||
version "4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944"
|
version "4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81"
|
||||||
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944.tgz#8d880becf996cffe08c78ad5afab6bc06090c990"
|
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81.tgz#d3b5dcf95b6d220e258cbf6ae19b06d30a7e9f14"
|
||||||
integrity sha512-sVOig4tjGxxlYaFcXgE71f/rtFhzyYrfyfNFUsxCIEJyVKU9rdOWIlIwQ2NQ7PntvGnn+x0XuFo4OC1jvPJKzg==
|
integrity sha512-q617EUWfRIDTriWADZ4YiWRZXCa/WuhNgLTVd+HqWLffjMSPzyM5uOWoauX91wvQClSKZU4pzI4JJLQ9Kl62Qg==
|
||||||
|
|
||||||
"@prisma/engines@4.15.0":
|
"@prisma/engines@4.16.2":
|
||||||
version "4.15.0"
|
version "4.16.2"
|
||||||
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.15.0.tgz#d8687a9fda615fab88b75b466931280289de9e26"
|
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.16.2.tgz#5ec8dd672c2173d597e469194916ad4826ce2e5f"
|
||||||
integrity sha512-FTaOCGs0LL0OW68juZlGxFtYviZa4xdQj/rQEdat2txw0s3Vu/saAPKjNVXfIgUsGXmQ72HPgNr6935/P8FNAA==
|
integrity sha512-vx1nxVvN4QeT/cepQce68deh/Turxy5Mr+4L4zClFuK1GlxN3+ivxfuv+ej/gvidWn1cE1uAhW7ALLNlYbRUAw==
|
||||||
|
|
||||||
"@samverschueren/stream-to-observable@^0.3.0":
|
"@samverschueren/stream-to-observable@^0.3.0":
|
||||||
version "0.3.1"
|
version "0.3.1"
|
||||||
@ -4860,10 +4860,10 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/lodash@4.14.191":
|
"@types/lodash@4.14.195":
|
||||||
version "4.14.191"
|
version "4.14.195"
|
||||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa"
|
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.195.tgz#bafc975b252eb6cea78882ce8a7b6bf22a6de632"
|
||||||
integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==
|
integrity sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==
|
||||||
|
|
||||||
"@types/lodash@^4.14.167":
|
"@types/lodash@^4.14.167":
|
||||||
version "4.14.194"
|
version "4.14.194"
|
||||||
@ -14544,12 +14544,12 @@ pretty-hrtime@^1.0.3:
|
|||||||
resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
|
resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
|
||||||
integrity sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==
|
integrity sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==
|
||||||
|
|
||||||
prisma@4.15.0:
|
prisma@4.16.2:
|
||||||
version "4.15.0"
|
version "4.16.2"
|
||||||
resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.15.0.tgz#4faa94f0d584828b68468953ff0bc88f37912c8c"
|
resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.16.2.tgz#469e0a0991c6ae5bcde289401726bb012253339e"
|
||||||
integrity sha512-iKZZpobPl48gTcSZVawLMQ3lEy6BnXwtoMj7hluoGFYu2kQ6F9LBuBrUyF95zRVnNo8/3KzLXJXJ5TEnLSJFiA==
|
integrity sha512-SYCsBvDf0/7XSJyf2cHTLjLeTLVXYfqp7pG5eEVafFLeT0u/hLFz/9W196nDRGUOo1JfPatAEb+uEnTQImQC1g==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@prisma/engines" "4.15.0"
|
"@prisma/engines" "4.16.2"
|
||||||
|
|
||||||
prismjs@^1.28.0:
|
prismjs@^1.28.0:
|
||||||
version "1.29.0"
|
version "1.29.0"
|
||||||
@ -17558,10 +17558,10 @@ y18n@^5.0.5:
|
|||||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
||||||
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
|
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
|
||||||
|
|
||||||
yahoo-finance2@2.4.1:
|
yahoo-finance2@2.4.3:
|
||||||
version "2.4.1"
|
version "2.4.3"
|
||||||
resolved "https://registry.yarnpkg.com/yahoo-finance2/-/yahoo-finance2-2.4.1.tgz#2ccd422e33228fc34d42e919b0d2fdd8d3f76bbf"
|
resolved "https://registry.yarnpkg.com/yahoo-finance2/-/yahoo-finance2-2.4.3.tgz#be4099182dc0a2e2908779e04d7b802688c15f0e"
|
||||||
integrity sha512-jl5oHr25RC24nOmoIiDqjnc/Iiy3MZAB+dIPCyUR+o5uz72xHfTZDM9tPeheggmlAGV+KftPP6smZ6L6lNgkSQ==
|
integrity sha512-LVcl+h4XBMe3N/l8BOZdDFoK7AGMiblSBE00dU9t2zB0Zfxa6QQMESnUkJ1m35RWBr8QXFJyJnToPt+qKiEQXQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/tough-cookie" "^4.0.2"
|
"@types/tough-cookie" "^4.0.2"
|
||||||
ajv "8.10.0"
|
ajv "8.10.0"
|
||||||
|
Reference in New Issue
Block a user