Compare commits
31 Commits
Author | SHA1 | Date | |
---|---|---|---|
25b3de5828 | |||
40b454d2f3 | |||
5596e5f03b | |||
66992ef915 | |||
7f67430685 | |||
8a49a04324 | |||
5d7c19b0ed | |||
cde74b6c62 | |||
633c65e33c | |||
d1617f2d87 | |||
68e558f198 | |||
12ca01c862 | |||
2115745471 | |||
2cabd21315 | |||
3615e2f057 | |||
d3679d41b3 | |||
f2d431a6b8 | |||
2bc8bebfb8 | |||
5b20ba3382 | |||
15cc294581 | |||
b060b81204 | |||
a8d557eb1b | |||
6ae3a47b54 | |||
88c19eb45e | |||
7728706bc8 | |||
2e9d40c201 | |||
c002e37285 | |||
6be38a1c19 | |||
a3178fb213 | |||
e7158f6e16 | |||
dbea0456bc |
71
CHANGELOG.md
71
CHANGELOG.md
@ -5,15 +5,82 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## 2.60.0 - 2024-03-02
|
||||
|
||||
- Added support for the cryptocurrency _Uniswap_ (`UNI7083-USD`)
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the usability of the benchmarks in the markets overview
|
||||
- Integrated (wealth) items into the transaction point concept in the portfolio service
|
||||
- Refreshed the cryptocurrencies list
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed a missing value in the activities table on mobile
|
||||
- Fixed a missing value on the public page
|
||||
- Displayed the button to fetch the current market price only if the activity is from today
|
||||
|
||||
## 2.59.0 - 2024-02-29
|
||||
|
||||
### Added
|
||||
|
||||
- Added an index for `isExcluded` to the account database table
|
||||
- Extended the content of the _Self-Hosting_ section on the Frequently Asked Questions (FAQ) page
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the activities import by `isin` in the _Yahoo Finance_ service
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue with the exchange rate calculation of (wealth) items in accounts
|
||||
|
||||
## 2.58.0 - 2024-02-27
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the handling of activities without account
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the query to filter activities of excluded accounts
|
||||
- Improved the asset profile validation in the activities import
|
||||
|
||||
## 2.57.0 - 2024-02-25
|
||||
|
||||
### Changed
|
||||
|
||||
- Moved the break down of the performance into asset and currency on the analysis page from experimental to general availability
|
||||
- Restructured the `copy-assets` `Nx` target
|
||||
|
||||
### Fixed
|
||||
|
||||
- Changed the performances of the _Top 3_ and _Bottom 3_ performers on the analysis page to take the currency effects into account
|
||||
|
||||
## 2.56.0 - 2024-02-24
|
||||
|
||||
### Changed
|
||||
|
||||
- Switched the performance calculations to take the currency effects into account
|
||||
- Removed the `isDefault` flag from the `Account` database schema
|
||||
- Exposed the database index of _Redis_ as an environment variable (`REDIS_DB`)
|
||||
- Improved the language localization for German (`de`)
|
||||
- Upgraded `prisma` from version `5.9.1` to `5.10.2`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Added the missing default currency to the prepare currencies function in the exchange rate data service
|
||||
|
||||
## 2.55.0 - 2024-02-22
|
||||
|
||||
### Added
|
||||
|
||||
- Added indexes for `alias`, `granteeUserId` and `userId` to the access database table
|
||||
- Added indexes for `currency`, `name` and `userId` to the account database table
|
||||
- Added an index for `accountId`, `date` and `updatedAt` to the account balance database table
|
||||
- Added indexes for `accountId`, `date` and `updatedAt` to the account balance database table
|
||||
- Added an index for `userId` to the auth device database table
|
||||
- Added an index for `marketPrice` and `state` to the market data database table
|
||||
- Added indexes for `marketPrice` and `state` to the market data database table
|
||||
- Added indexes for `date`, `isDraft` and `userId` to the order database table
|
||||
- Added an index for `name` to the platform database table
|
||||
- Added indexes for `assetClass`, `currency`, `dataSource`, `isin`, `name` and `symbol` to the symbol profile database table
|
||||
|
@ -99,6 +99,7 @@ We provide official container images hosted on [Docker Hub](https://hub.docker.c
|
||||
| `POSTGRES_DB` | | The name of the _PostgreSQL_ database |
|
||||
| `POSTGRES_PASSWORD` | | The password of the _PostgreSQL_ database |
|
||||
| `POSTGRES_USER` | | The user of the _PostgreSQL_ database |
|
||||
| `REDIS_DB` | `0` | The database index of _Redis_ |
|
||||
| `REDIS_HOST` | | The host where _Redis_ is running |
|
||||
| `REDIS_PASSWORD` | | The password of _Redis_ |
|
||||
| `REDIS_PORT` | | The port where _Redis_ is running |
|
||||
|
@ -9,12 +9,13 @@
|
||||
"build": {
|
||||
"executor": "@nx/webpack:webpack",
|
||||
"options": {
|
||||
"outputPath": "dist/apps/api",
|
||||
"main": "apps/api/src/main.ts",
|
||||
"tsConfig": "apps/api/tsconfig.app.json",
|
||||
"assets": ["apps/api/src/assets"],
|
||||
"target": "node",
|
||||
"compiler": "tsc",
|
||||
"deleteOutputPath": false,
|
||||
"main": "apps/api/src/main.ts",
|
||||
"outputPath": "dist/apps/api",
|
||||
"sourceMap": true,
|
||||
"target": "node",
|
||||
"tsConfig": "apps/api/tsconfig.app.json",
|
||||
"webpackConfig": "apps/api/webpack.config.js"
|
||||
},
|
||||
"configurations": {
|
||||
@ -33,6 +34,26 @@
|
||||
},
|
||||
"outputs": ["{options.outputPath}"]
|
||||
},
|
||||
"copy-assets": {
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "shx rm -rf dist/apps/api"
|
||||
},
|
||||
{
|
||||
"command": "shx mkdir -p dist/apps/api/assets/locales"
|
||||
},
|
||||
{
|
||||
"command": "shx cp -r apps/api/src/assets/* dist/apps/api/assets"
|
||||
},
|
||||
{
|
||||
"command": "shx cp -r apps/client/src/locales/* dist/apps/api/assets/locales"
|
||||
}
|
||||
],
|
||||
"parallel": false
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"executor": "@nx/js:node",
|
||||
"options": {
|
||||
|
@ -63,7 +63,7 @@ export class AccountController {
|
||||
{ Order: true }
|
||||
);
|
||||
|
||||
if (account?.isDefault || account?.Order.length > 0) {
|
||||
if (!account || account?.Order.length > 0) {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||
StatusCodes.FORBIDDEN
|
||||
|
@ -53,6 +53,7 @@ import { UserModule } from './user/user.module';
|
||||
BenchmarkModule,
|
||||
BullModule.forRoot({
|
||||
redis: {
|
||||
db: parseInt(process.env.REDIS_DB ?? '0', 10),
|
||||
host: process.env.REDIS_HOST,
|
||||
port: parseInt(process.env.REDIS_PORT ?? '6379', 10),
|
||||
password: process.env.REDIS_PASSWORD
|
||||
|
@ -43,7 +43,7 @@ export class ImportController {
|
||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||
public async import(
|
||||
@Body() importData: ImportDataDto,
|
||||
@Query('dryRun') isDryRun?: boolean
|
||||
@Query('dryRun') isDryRun = false
|
||||
): Promise<ImportResponse> {
|
||||
if (
|
||||
!hasPermission(this.request.user.permissions, permissions.createAccount)
|
||||
|
@ -570,17 +570,10 @@ export class ImportService {
|
||||
[assetProfileIdentifier: string]: Partial<SymbolProfile>;
|
||||
} = {};
|
||||
|
||||
const uniqueActivitiesDto = uniqBy(
|
||||
activitiesDto,
|
||||
({ dataSource, symbol }) => {
|
||||
return getAssetProfileIdentifier({ dataSource, symbol });
|
||||
}
|
||||
);
|
||||
|
||||
for (const [
|
||||
index,
|
||||
{ currency, dataSource, symbol, type }
|
||||
] of uniqueActivitiesDto.entries()) {
|
||||
] of activitiesDto.entries()) {
|
||||
if (!this.configurationService.get('DATA_SOURCES').includes(dataSource)) {
|
||||
throw new Error(
|
||||
`activities.${index}.dataSource ("${dataSource}") is not valid`
|
||||
@ -602,37 +595,33 @@ export class ImportService {
|
||||
}
|
||||
}
|
||||
|
||||
const assetProfile = {
|
||||
currency,
|
||||
...(
|
||||
await this.dataProviderService.getAssetProfiles([
|
||||
{ dataSource, symbol }
|
||||
])
|
||||
)?.[symbol]
|
||||
};
|
||||
if (!assetProfiles[getAssetProfileIdentifier({ dataSource, symbol })]) {
|
||||
const assetProfile = {
|
||||
currency,
|
||||
...(
|
||||
await this.dataProviderService.getAssetProfiles([
|
||||
{ dataSource, symbol }
|
||||
])
|
||||
)?.[symbol]
|
||||
};
|
||||
|
||||
if (type === 'BUY' || type === 'DIVIDEND' || type === 'SELL') {
|
||||
if (!assetProfile?.name) {
|
||||
throw new Error(
|
||||
`activities.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")`
|
||||
);
|
||||
if (type === 'BUY' || type === 'DIVIDEND' || type === 'SELL') {
|
||||
if (!assetProfile?.name) {
|
||||
throw new Error(
|
||||
`activities.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")`
|
||||
);
|
||||
}
|
||||
|
||||
if (assetProfile.currency !== currency) {
|
||||
throw new Error(
|
||||
`activities.${index}.currency ("${currency}") does not match with currency of ${assetProfile.symbol} ("${assetProfile.currency}")`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
assetProfile.currency !== currency &&
|
||||
!this.exchangeRateDataService.hasCurrencyPair(
|
||||
currency,
|
||||
assetProfile.currency
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
`activities.${index}.currency ("${currency}") does not match with "${assetProfile.currency}" and no exchange rate is available from "${currency}" to "${assetProfile.currency}"`
|
||||
);
|
||||
}
|
||||
assetProfiles[getAssetProfileIdentifier({ dataSource, symbol })] =
|
||||
assetProfile;
|
||||
}
|
||||
|
||||
assetProfiles[getAssetProfileIdentifier({ dataSource, symbol })] =
|
||||
assetProfile;
|
||||
}
|
||||
|
||||
return assetProfiles;
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
GATHER_ASSET_PROFILE_PROCESS_OPTIONS
|
||||
} from '@ghostfolio/common/config';
|
||||
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
|
||||
import { Filter } from '@ghostfolio/common/interfaces';
|
||||
import { Filter, UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
@ -200,6 +200,17 @@ export class OrderService {
|
||||
return count;
|
||||
}
|
||||
|
||||
public async getLatestOrder({ dataSource, symbol }: UniqueAsset) {
|
||||
return this.prismaService.order.findFirst({
|
||||
orderBy: {
|
||||
date: 'desc'
|
||||
},
|
||||
where: {
|
||||
SymbolProfile: { dataSource, symbol }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async getOrders({
|
||||
filters,
|
||||
includeDrafts = false,
|
||||
@ -292,19 +303,14 @@ export class OrderService {
|
||||
}
|
||||
|
||||
if (types) {
|
||||
where.OR = types.map((type) => {
|
||||
return {
|
||||
type: {
|
||||
equals: type
|
||||
}
|
||||
};
|
||||
});
|
||||
where.type = { in: types };
|
||||
}
|
||||
|
||||
if (withExcludedAccounts === false) {
|
||||
where.Account = {
|
||||
NOT: { isExcluded: true }
|
||||
};
|
||||
where.OR = [
|
||||
{ Account: null },
|
||||
{ Account: { NOT: { isExcluded: true } } }
|
||||
];
|
||||
}
|
||||
|
||||
const [orders, count] = await Promise.all([
|
||||
|
@ -108,6 +108,7 @@ describe('CurrentRateService', () => {
|
||||
currentRateService = new CurrentRateService(
|
||||
dataProviderService,
|
||||
marketDataService,
|
||||
null,
|
||||
null
|
||||
);
|
||||
});
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
|
||||
import { resetHours } from '@ghostfolio/common/helper';
|
||||
@ -22,6 +23,7 @@ export class CurrentRateService {
|
||||
public constructor(
|
||||
private readonly dataProviderService: DataProviderService,
|
||||
private readonly marketDataService: MarketDataService,
|
||||
private readonly orderService: OrderService,
|
||||
@Inject(REQUEST) private readonly request: RequestWithUser
|
||||
) {}
|
||||
|
||||
@ -121,11 +123,17 @@ export class CurrentRateService {
|
||||
});
|
||||
|
||||
if (!value) {
|
||||
// Fallback to unit price of latest activity
|
||||
const latestActivity = await this.orderService.getLatestOrder({
|
||||
dataSource,
|
||||
symbol
|
||||
});
|
||||
|
||||
value = {
|
||||
dataSource,
|
||||
symbol,
|
||||
date: today,
|
||||
marketPrice: 0
|
||||
marketPrice: latestActivity?.unitPrice ?? 0
|
||||
};
|
||||
|
||||
response.values.push(value);
|
||||
|
@ -21,7 +21,7 @@ describe('PortfolioCalculator', () => {
|
||||
let exchangeRateDataService: ExchangeRateDataService;
|
||||
|
||||
beforeEach(() => {
|
||||
currentRateService = new CurrentRateService(null, null, null);
|
||||
currentRateService = new CurrentRateService(null, null, null, null);
|
||||
|
||||
exchangeRateDataService = new ExchangeRateDataService(
|
||||
null,
|
||||
|
@ -21,7 +21,7 @@ describe('PortfolioCalculator', () => {
|
||||
let exchangeRateDataService: ExchangeRateDataService;
|
||||
|
||||
beforeEach(() => {
|
||||
currentRateService = new CurrentRateService(null, null, null);
|
||||
currentRateService = new CurrentRateService(null, null, null, null);
|
||||
|
||||
exchangeRateDataService = new ExchangeRateDataService(
|
||||
null,
|
||||
|
@ -34,7 +34,7 @@ describe('PortfolioCalculator', () => {
|
||||
let exchangeRateDataService: ExchangeRateDataService;
|
||||
|
||||
beforeEach(() => {
|
||||
currentRateService = new CurrentRateService(null, null, null);
|
||||
currentRateService = new CurrentRateService(null, null, null, null);
|
||||
|
||||
exchangeRateDataService = new ExchangeRateDataService(
|
||||
null,
|
||||
|
@ -34,7 +34,7 @@ describe('PortfolioCalculator', () => {
|
||||
let exchangeRateDataService: ExchangeRateDataService;
|
||||
|
||||
beforeEach(() => {
|
||||
currentRateService = new CurrentRateService(null, null, null);
|
||||
currentRateService = new CurrentRateService(null, null, null, null);
|
||||
|
||||
exchangeRateDataService = new ExchangeRateDataService(
|
||||
null,
|
||||
|
@ -21,7 +21,7 @@ describe('PortfolioCalculator', () => {
|
||||
let exchangeRateDataService: ExchangeRateDataService;
|
||||
|
||||
beforeEach(() => {
|
||||
currentRateService = new CurrentRateService(null, null, null);
|
||||
currentRateService = new CurrentRateService(null, null, null, null);
|
||||
|
||||
exchangeRateDataService = new ExchangeRateDataService(
|
||||
null,
|
||||
|
@ -21,7 +21,7 @@ describe('PortfolioCalculator', () => {
|
||||
let exchangeRateDataService: ExchangeRateDataService;
|
||||
|
||||
beforeEach(() => {
|
||||
currentRateService = new CurrentRateService(null, null, null);
|
||||
currentRateService = new CurrentRateService(null, null, null, null);
|
||||
|
||||
exchangeRateDataService = new ExchangeRateDataService(
|
||||
null,
|
||||
|
@ -21,7 +21,7 @@ describe('PortfolioCalculator', () => {
|
||||
let exchangeRateDataService: ExchangeRateDataService;
|
||||
|
||||
beforeEach(() => {
|
||||
currentRateService = new CurrentRateService(null, null, null);
|
||||
currentRateService = new CurrentRateService(null, null, null, null);
|
||||
|
||||
exchangeRateDataService = new ExchangeRateDataService(
|
||||
null,
|
||||
|
@ -10,7 +10,7 @@ describe('PortfolioCalculator', () => {
|
||||
let exchangeRateDataService: ExchangeRateDataService;
|
||||
|
||||
beforeEach(() => {
|
||||
currentRateService = new CurrentRateService(null, null, null);
|
||||
currentRateService = new CurrentRateService(null, null, null, null);
|
||||
|
||||
exchangeRateDataService = new ExchangeRateDataService(
|
||||
null,
|
||||
|
@ -825,6 +825,7 @@ export class PortfolioCalculator {
|
||||
|
||||
switch (type) {
|
||||
case 'BUY':
|
||||
case 'ITEM':
|
||||
factor = 1;
|
||||
break;
|
||||
case 'SELL':
|
||||
|
@ -118,27 +118,23 @@ export class PortfolioController {
|
||||
this.userService.isRestrictedView(this.request.user)
|
||||
) {
|
||||
const totalInvestment = Object.values(holdings)
|
||||
.map((portfolioPosition) => {
|
||||
return portfolioPosition.investment;
|
||||
.map(({ investment }) => {
|
||||
return investment;
|
||||
})
|
||||
.reduce((a, b) => a + b, 0);
|
||||
|
||||
const totalValue = Object.values(holdings)
|
||||
.map((portfolioPosition) => {
|
||||
return this.exchangeRateDataService.toCurrency(
|
||||
portfolioPosition.quantity * portfolioPosition.marketPrice,
|
||||
portfolioPosition.currency,
|
||||
this.request.user.Settings.settings.baseCurrency
|
||||
);
|
||||
.filter(({ assetClass, assetSubClass }) => {
|
||||
return assetClass !== 'CASH' && assetSubClass !== 'CASH';
|
||||
})
|
||||
.map(({ valueInBaseCurrency }) => {
|
||||
return valueInBaseCurrency;
|
||||
})
|
||||
.reduce((a, b) => a + b, 0);
|
||||
|
||||
for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
|
||||
portfolioPosition.grossPerformance = null;
|
||||
portfolioPosition.investment =
|
||||
portfolioPosition.investment / totalInvestment;
|
||||
portfolioPosition.netPerformance = null;
|
||||
portfolioPosition.quantity = null;
|
||||
portfolioPosition.valueInPercentage =
|
||||
portfolioPosition.valueInBaseCurrency / totalValue;
|
||||
}
|
||||
@ -346,7 +342,8 @@ export class PortfolioController {
|
||||
@Query('assetClasses') filterByAssetClasses?: string,
|
||||
@Query('range') dateRange: DateRange = 'max',
|
||||
@Query('tags') filterByTags?: string,
|
||||
@Query('withExcludedAccounts') withExcludedAccounts = false
|
||||
@Query('withExcludedAccounts') withExcludedAccounts = false,
|
||||
@Query('withItems') withItems = false
|
||||
): Promise<PortfolioPerformanceResponse> {
|
||||
const hasReadRestrictedAccessPermission =
|
||||
this.userService.hasReadRestrictedAccessPermission({
|
||||
@ -365,6 +362,7 @@ export class PortfolioController {
|
||||
filters,
|
||||
impersonationId,
|
||||
withExcludedAccounts,
|
||||
withItems,
|
||||
userId: this.request.user.id
|
||||
});
|
||||
|
||||
@ -429,6 +427,10 @@ export class PortfolioController {
|
||||
return nullifyValuesInObject(item, ['totalInvestment', 'value']);
|
||||
}
|
||||
);
|
||||
performanceInformation.performance = nullifyValuesInObject(
|
||||
performanceInformation.performance,
|
||||
['currentNetPerformance', 'currentNetPerformancePercent']
|
||||
);
|
||||
}
|
||||
|
||||
return performanceInformation;
|
||||
@ -515,7 +517,8 @@ export class PortfolioController {
|
||||
dateOfFirstActivity: portfolioPosition.dateOfFirstActivity,
|
||||
markets: hasDetails ? portfolioPosition.markets : undefined,
|
||||
name: portfolioPosition.name,
|
||||
netPerformancePercent: portfolioPosition.netPerformancePercent,
|
||||
netPerformancePercentWithCurrencyEffect:
|
||||
portfolioPosition.netPerformancePercentWithCurrencyEffect,
|
||||
sectors: hasDetails ? portfolioPosition.sectors : [],
|
||||
symbol: portfolioPosition.symbol,
|
||||
url: portfolioPosition.url,
|
||||
|
@ -119,7 +119,7 @@ export class PortfolioService {
|
||||
userId: string;
|
||||
withExcludedAccounts?: boolean;
|
||||
}): Promise<AccountWithValue[]> {
|
||||
const where: Prisma.AccountWhereInput = { userId: userId };
|
||||
const where: Prisma.AccountWhereInput = { userId };
|
||||
|
||||
const accountFilter = filters?.find(({ type }) => {
|
||||
return type === 'ACCOUNT';
|
||||
@ -227,18 +227,20 @@ export class PortfolioService {
|
||||
impersonationId: string;
|
||||
}): Promise<InvestmentItem[]> {
|
||||
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
||||
const user = await this.userService.user({ id: userId });
|
||||
const userCurrency = this.getUserCurrency(user);
|
||||
|
||||
const { activities } = await this.orderService.getOrders({
|
||||
filters,
|
||||
userCurrency,
|
||||
userId,
|
||||
types: ['DIVIDEND'],
|
||||
userCurrency: this.request.user.Settings.settings.baseCurrency
|
||||
types: ['DIVIDEND']
|
||||
});
|
||||
|
||||
let dividends = activities.map((dividend) => {
|
||||
let dividends = activities.map(({ date, valueInBaseCurrency }) => {
|
||||
return {
|
||||
date: format(dividend.date, DATE_FORMAT),
|
||||
investment: dividend.valueInBaseCurrency
|
||||
date: format(date, DATE_FORMAT),
|
||||
investment: valueInBaseCurrency
|
||||
};
|
||||
});
|
||||
|
||||
@ -275,7 +277,8 @@ export class PortfolioService {
|
||||
await this.getTransactionPoints({
|
||||
filters,
|
||||
userId,
|
||||
includeDrafts: true
|
||||
includeDrafts: true,
|
||||
types: ['BUY', 'SELL']
|
||||
});
|
||||
|
||||
if (transactionPoints.length === 0) {
|
||||
@ -529,12 +532,20 @@ export class PortfolioService {
|
||||
grossPerformance: item.grossPerformance?.toNumber() ?? 0,
|
||||
grossPerformancePercent:
|
||||
item.grossPerformancePercentage?.toNumber() ?? 0,
|
||||
grossPerformancePercentWithCurrencyEffect:
|
||||
item.grossPerformancePercentageWithCurrencyEffect?.toNumber() ?? 0,
|
||||
grossPerformanceWithCurrencyEffect:
|
||||
item.grossPerformanceWithCurrencyEffect?.toNumber() ?? 0,
|
||||
investment: item.investment.toNumber(),
|
||||
marketPrice: item.marketPrice,
|
||||
marketState: dataProviderResponse?.marketState ?? 'delayed',
|
||||
name: symbolProfile.name,
|
||||
netPerformance: item.netPerformance?.toNumber() ?? 0,
|
||||
netPerformancePercent: item.netPerformancePercentage?.toNumber() ?? 0,
|
||||
netPerformancePercentWithCurrencyEffect:
|
||||
item.netPerformancePercentageWithCurrencyEffect?.toNumber() ?? 0,
|
||||
netPerformanceWithCurrencyEffect:
|
||||
item.netPerformanceWithCurrencyEffect?.toNumber() ?? 0,
|
||||
quantity: item.quantity.toNumber(),
|
||||
sectors: symbolProfile.sectors,
|
||||
symbol: item.symbol,
|
||||
@ -692,7 +703,7 @@ export class PortfolioService {
|
||||
.filter((order) => {
|
||||
tags = tags.concat(order.tags);
|
||||
|
||||
return order.type === 'BUY' || order.type === 'SELL';
|
||||
return ['BUY', 'ITEM', 'SELL'].includes(order.type);
|
||||
})
|
||||
.map((order) => ({
|
||||
currency: order.SymbolProfile.currency,
|
||||
@ -741,7 +752,9 @@ export class PortfolioService {
|
||||
} = position;
|
||||
|
||||
const accounts: PortfolioPositionDetail['accounts'] = uniqBy(
|
||||
orders,
|
||||
orders.filter(({ Account }) => {
|
||||
return Account;
|
||||
}),
|
||||
'Account.id'
|
||||
).map(({ Account }) => {
|
||||
return Account;
|
||||
@ -945,7 +958,8 @@ export class PortfolioService {
|
||||
const { portfolioOrders, transactionPoints } =
|
||||
await this.getTransactionPoints({
|
||||
filters,
|
||||
userId
|
||||
userId,
|
||||
types: ['BUY', 'SELL']
|
||||
});
|
||||
|
||||
if (transactionPoints?.length <= 0) {
|
||||
@ -1075,13 +1089,15 @@ export class PortfolioService {
|
||||
filters,
|
||||
impersonationId,
|
||||
userId,
|
||||
withExcludedAccounts = false
|
||||
withExcludedAccounts = false,
|
||||
withItems = false
|
||||
}: {
|
||||
dateRange?: DateRange;
|
||||
filters?: Filter[];
|
||||
impersonationId: string;
|
||||
userId: string;
|
||||
withExcludedAccounts?: boolean;
|
||||
withItems?: boolean;
|
||||
}): Promise<PortfolioPerformanceResponse> {
|
||||
userId = await this.getUserId(impersonationId, userId);
|
||||
const user = await this.userService.user({ id: userId });
|
||||
@ -1116,7 +1132,8 @@ export class PortfolioService {
|
||||
await this.getTransactionPoints({
|
||||
filters,
|
||||
userId,
|
||||
withExcludedAccounts
|
||||
withExcludedAccounts,
|
||||
types: withItems ? ['BUY', 'ITEM', 'SELL'] : ['BUY', 'SELL']
|
||||
});
|
||||
|
||||
const portfolioCalculator = new PortfolioCalculator({
|
||||
@ -1268,7 +1285,8 @@ export class PortfolioService {
|
||||
|
||||
const { orders, portfolioOrders, transactionPoints } =
|
||||
await this.getTransactionPoints({
|
||||
userId
|
||||
userId,
|
||||
types: ['BUY', 'SELL']
|
||||
});
|
||||
|
||||
const portfolioCalculator = new PortfolioCalculator({
|
||||
@ -1600,12 +1618,16 @@ export class PortfolioService {
|
||||
dateOfFirstActivity: undefined,
|
||||
grossPerformance: 0,
|
||||
grossPerformancePercent: 0,
|
||||
grossPerformancePercentWithCurrencyEffect: 0,
|
||||
grossPerformanceWithCurrencyEffect: 0,
|
||||
investment: balance,
|
||||
marketPrice: 0,
|
||||
marketState: 'open',
|
||||
name: currency,
|
||||
netPerformance: 0,
|
||||
netPerformancePercent: 0,
|
||||
netPerformancePercentWithCurrencyEffect: 0,
|
||||
netPerformanceWithCurrencyEffect: 0,
|
||||
quantity: 0,
|
||||
sectors: [],
|
||||
symbol: currency,
|
||||
@ -1814,9 +1836,25 @@ export class PortfolioService {
|
||||
})
|
||||
?.toNumber();
|
||||
|
||||
const annualizedPerformancePercentWithCurrencyEffect =
|
||||
new PortfolioCalculator({
|
||||
currency: userCurrency,
|
||||
currentRateService: this.currentRateService,
|
||||
exchangeRateDataService: this.exchangeRateDataService,
|
||||
orders: []
|
||||
})
|
||||
.getAnnualizedPerformancePercent({
|
||||
daysInMarket,
|
||||
netPerformancePercent: new Big(
|
||||
performanceInformation.performance.currentNetPerformancePercentWithCurrencyEffect
|
||||
)
|
||||
})
|
||||
?.toNumber();
|
||||
|
||||
return {
|
||||
...performanceInformation.performance,
|
||||
annualizedPerformancePercent,
|
||||
annualizedPerformancePercentWithCurrencyEffect,
|
||||
cash,
|
||||
dividend,
|
||||
excludedAccountsAndActivities,
|
||||
@ -1881,11 +1919,13 @@ export class PortfolioService {
|
||||
private async getTransactionPoints({
|
||||
filters,
|
||||
includeDrafts = false,
|
||||
types = ['BUY', 'ITEM', 'SELL'],
|
||||
userId,
|
||||
withExcludedAccounts = false
|
||||
}: {
|
||||
filters?: Filter[];
|
||||
includeDrafts?: boolean;
|
||||
types?: ActivityType[];
|
||||
userId: string;
|
||||
withExcludedAccounts?: boolean;
|
||||
}): Promise<{
|
||||
@ -1899,10 +1939,10 @@ export class PortfolioService {
|
||||
const { activities, count } = await this.orderService.getOrders({
|
||||
filters,
|
||||
includeDrafts,
|
||||
types,
|
||||
userCurrency,
|
||||
userId,
|
||||
withExcludedAccounts,
|
||||
types: ['BUY', 'SELL']
|
||||
withExcludedAccounts
|
||||
});
|
||||
|
||||
if (count <= 0) {
|
||||
@ -1962,7 +2002,7 @@ export class PortfolioService {
|
||||
withExcludedAccounts = false
|
||||
}: {
|
||||
filters?: Filter[];
|
||||
orders: OrderWithAccount[];
|
||||
orders: Activity[];
|
||||
portfolioItemsNow: { [p: string]: TimelinePosition };
|
||||
userCurrency: string;
|
||||
userId: string;
|
||||
@ -1974,7 +2014,7 @@ export class PortfolioService {
|
||||
userCurrency,
|
||||
userId,
|
||||
withExcludedAccounts,
|
||||
types: ['ITEM', 'LIABILITY']
|
||||
types: ['LIABILITY']
|
||||
});
|
||||
|
||||
const accounts: PortfolioDetails['accounts'] = {};
|
||||
@ -2058,41 +2098,42 @@ export class PortfolioService {
|
||||
};
|
||||
}
|
||||
|
||||
for (const order of ordersByAccount) {
|
||||
for (const {
|
||||
Account,
|
||||
quantity,
|
||||
SymbolProfile,
|
||||
type
|
||||
} of ordersByAccount) {
|
||||
let currentValueOfSymbolInBaseCurrency =
|
||||
order.quantity *
|
||||
(portfolioItemsNow[order.SymbolProfile.symbol]
|
||||
?.marketPriceInBaseCurrency ??
|
||||
order.unitPrice ??
|
||||
0);
|
||||
quantity *
|
||||
portfolioItemsNow[SymbolProfile.symbol]
|
||||
?.marketPriceInBaseCurrency ?? 0;
|
||||
|
||||
if (order.type === 'LIABILITY' || order.type === 'SELL') {
|
||||
if (['LIABILITY', 'SELL'].includes(type)) {
|
||||
currentValueOfSymbolInBaseCurrency *= -1;
|
||||
}
|
||||
|
||||
if (accounts[order.Account?.id || UNKNOWN_KEY]?.valueInBaseCurrency) {
|
||||
accounts[order.Account?.id || UNKNOWN_KEY].valueInBaseCurrency +=
|
||||
if (accounts[Account?.id || UNKNOWN_KEY]?.valueInBaseCurrency) {
|
||||
accounts[Account?.id || UNKNOWN_KEY].valueInBaseCurrency +=
|
||||
currentValueOfSymbolInBaseCurrency;
|
||||
} else {
|
||||
accounts[order.Account?.id || UNKNOWN_KEY] = {
|
||||
accounts[Account?.id || UNKNOWN_KEY] = {
|
||||
balance: 0,
|
||||
currency: order.Account?.currency,
|
||||
currency: Account?.currency,
|
||||
name: account.name,
|
||||
valueInBaseCurrency: currentValueOfSymbolInBaseCurrency
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
platforms[order.Account?.Platform?.id || UNKNOWN_KEY]
|
||||
?.valueInBaseCurrency
|
||||
platforms[Account?.Platform?.id || UNKNOWN_KEY]?.valueInBaseCurrency
|
||||
) {
|
||||
platforms[
|
||||
order.Account?.Platform?.id || UNKNOWN_KEY
|
||||
].valueInBaseCurrency += currentValueOfSymbolInBaseCurrency;
|
||||
platforms[Account?.Platform?.id || UNKNOWN_KEY].valueInBaseCurrency +=
|
||||
currentValueOfSymbolInBaseCurrency;
|
||||
} else {
|
||||
platforms[order.Account?.Platform?.id || UNKNOWN_KEY] = {
|
||||
platforms[Account?.Platform?.id || UNKNOWN_KEY] = {
|
||||
balance: 0,
|
||||
currency: order.Account?.currency,
|
||||
currency: Account?.currency,
|
||||
name: account.Platform?.name,
|
||||
valueInBaseCurrency: currentValueOfSymbolInBaseCurrency
|
||||
};
|
||||
|
@ -15,6 +15,7 @@ import { RedisCacheService } from './redis-cache.service';
|
||||
inject: [ConfigurationService],
|
||||
useFactory: async (configurationService: ConfigurationService) => {
|
||||
return <RedisClientOptions>{
|
||||
db: configurationService.get('REDIS_DB'),
|
||||
host: configurationService.get('REDIS_HOST'),
|
||||
max: configurationService.get('MAX_ITEM_IN_CACHE'),
|
||||
password: configurationService.get('REDIS_PASSWORD'),
|
||||
|
@ -39,7 +39,7 @@ export class SymbolController {
|
||||
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||
public async lookupSymbol(
|
||||
@Query('includeIndices') includeIndices: boolean = false,
|
||||
@Query('includeIndices') includeIndices = false,
|
||||
@Query('query') query = ''
|
||||
): Promise<{ items: LookupItem[] }> {
|
||||
try {
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
|
||||
import { environment } from '@ghostfolio/api/environments/environment';
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||
import { I18nService } from '@ghostfolio/api/services/i18n/i18n.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||
import { TagService } from '@ghostfolio/api/services/tag/tag.service';
|
||||
import {
|
||||
DEFAULT_CURRENCY,
|
||||
DEFAULT_LANGUAGE_CODE,
|
||||
PROPERTY_IS_READ_ONLY_MODE,
|
||||
PROPERTY_SYSTEM_MESSAGE,
|
||||
locale
|
||||
@ -31,6 +33,8 @@ const crypto = require('crypto');
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
private i18nService = new I18nService();
|
||||
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService,
|
||||
private readonly prismaService: PrismaService,
|
||||
@ -325,8 +329,10 @@ export class UserService {
|
||||
Account: {
|
||||
create: {
|
||||
currency: DEFAULT_CURRENCY,
|
||||
isDefault: true,
|
||||
name: 'Default Account'
|
||||
name: this.i18nService.getTranslation({
|
||||
id: 'myAccount',
|
||||
languageCode: DEFAULT_LANGUAGE_CODE // TODO
|
||||
})
|
||||
}
|
||||
},
|
||||
Settings: {
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -5,5 +5,6 @@
|
||||
"LUNA2": "Terra",
|
||||
"SGB1": "Songbird",
|
||||
"UNI1": "Uniswap",
|
||||
"UNI7083": "Uniswap",
|
||||
"UST": "TerraUSD"
|
||||
}
|
||||
|
@ -51,8 +51,10 @@ export class RedactValuesInResponseInterceptor<T>
|
||||
'feeInBaseCurrency',
|
||||
'filteredValueInBaseCurrency',
|
||||
'grossPerformance',
|
||||
'grossPerformanceWithCurrencyEffect',
|
||||
'investment',
|
||||
'netPerformance',
|
||||
'netPerformanceWithCurrencyEffect',
|
||||
'quantity',
|
||||
'symbolMapping',
|
||||
'totalBalanceInBaseCurrency',
|
||||
|
@ -43,6 +43,7 @@ export class ConfigurationService {
|
||||
MAX_ACTIVITIES_TO_IMPORT: num({ default: Number.MAX_SAFE_INTEGER }),
|
||||
MAX_ITEM_IN_CACHE: num({ default: 9999 }),
|
||||
PORT: port({ default: 3333 }),
|
||||
REDIS_DB: num({ default: 0 }),
|
||||
REDIS_HOST: str({ default: 'localhost' }),
|
||||
REDIS_PASSWORD: str({ default: '' }),
|
||||
REDIS_PORT: port({ default: 6379 }),
|
||||
|
@ -196,7 +196,9 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
|
||||
shortName: assetProfile.price.shortName,
|
||||
symbol: assetProfile.price.symbol
|
||||
});
|
||||
response.symbol = assetProfile.price.symbol;
|
||||
response.symbol = this.convertFromYahooFinanceSymbol(
|
||||
assetProfile.price.symbol
|
||||
);
|
||||
|
||||
if (assetSubClass === AssetSubClass.MUTUALFUND) {
|
||||
response.sectors = [];
|
||||
|
@ -166,13 +166,15 @@ export class ManualService implements DataProviderInterface {
|
||||
}
|
||||
});
|
||||
|
||||
for (const symbolProfile of symbolProfiles) {
|
||||
response[symbolProfile.symbol] = {
|
||||
currency: symbolProfile.currency,
|
||||
for (const { currency, symbol } of symbolProfiles) {
|
||||
let marketPrice = marketData.find((marketDataItem) => {
|
||||
return marketDataItem.symbol === symbol;
|
||||
})?.marketPrice;
|
||||
|
||||
response[symbol] = {
|
||||
currency,
|
||||
marketPrice,
|
||||
dataSource: this.getName(),
|
||||
marketPrice: marketData.find((marketDataItem) => {
|
||||
return marketDataItem.symbol === symbolProfile.symbol;
|
||||
})?.marketPrice,
|
||||
marketState: 'delayed'
|
||||
};
|
||||
}
|
||||
|
@ -38,17 +38,7 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
}: {
|
||||
symbol: string;
|
||||
}): Promise<Partial<SymbolProfile>> {
|
||||
const { assetClass, assetSubClass, currency, name } =
|
||||
await this.yahooFinanceDataEnhancerService.getAssetProfile(symbol);
|
||||
|
||||
return {
|
||||
assetClass,
|
||||
assetSubClass,
|
||||
currency,
|
||||
name,
|
||||
symbol,
|
||||
dataSource: this.getName()
|
||||
};
|
||||
return this.yahooFinanceDataEnhancerService.getAssetProfile(symbol);
|
||||
}
|
||||
|
||||
public getDataProviderInfo(): DataProviderInfo {
|
||||
|
@ -451,7 +451,7 @@ export class ExchangeRateDataService {
|
||||
}
|
||||
|
||||
private async prepareCurrencies(): Promise<string[]> {
|
||||
let currencies: string[] = [];
|
||||
let currencies: string[] = [DEFAULT_CURRENCY];
|
||||
|
||||
(
|
||||
await this.prismaService.account.findMany({
|
||||
|
@ -30,6 +30,7 @@ export interface Environment extends CleanedEnvAccessors {
|
||||
MAX_ACTIVITIES_TO_IMPORT: number;
|
||||
MAX_ITEM_IN_CACHE: number;
|
||||
PORT: number;
|
||||
REDIS_DB: number;
|
||||
REDIS_HOST: string;
|
||||
REDIS_PASSWORD: string;
|
||||
REDIS_PORT: number;
|
||||
|
@ -13,13 +13,13 @@
|
||||
"build": {
|
||||
"executor": "@nx/angular:webpack-browser",
|
||||
"options": {
|
||||
"deleteOutputPath": false,
|
||||
"localize": true,
|
||||
"outputPath": "dist/apps/client",
|
||||
"index": "apps/client/src/index.html",
|
||||
"main": "apps/client/src/main.ts",
|
||||
"polyfills": "apps/client/src/polyfills.ts",
|
||||
"tsConfig": "apps/client/tsconfig.app.json",
|
||||
"assets": [],
|
||||
"styles": [
|
||||
"apps/client/src/assets/fonts/inter.css",
|
||||
"apps/client/src/styles/theme.scss",
|
||||
@ -108,13 +108,22 @@
|
||||
"options": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "shx mkdir -p dist/apps/client"
|
||||
"command": "shx rm -rf dist/apps/client"
|
||||
},
|
||||
{
|
||||
"command": "shx cp -r apps/client/src/assets dist/apps/client"
|
||||
"command": "shx mkdir -p dist/apps/client/.well-known"
|
||||
},
|
||||
{
|
||||
"command": "shx cp -r apps/client/src/assets/.well-known dist/apps/client"
|
||||
"command": "shx mkdir -p dist/apps/client/assets"
|
||||
},
|
||||
{
|
||||
"command": "shx mkdir -p dist/apps/client/ionicons"
|
||||
},
|
||||
{
|
||||
"command": "shx cp -r apps/client/src/assets/* dist/apps/client/assets"
|
||||
},
|
||||
{
|
||||
"command": "shx cp -r apps/client/src/assets/.well-known/* dist/apps/client/.well-known"
|
||||
},
|
||||
{
|
||||
"command": "shx cp apps/client/src/assets/favicon.ico dist/apps/client"
|
||||
@ -128,9 +137,6 @@
|
||||
{
|
||||
"command": "shx cp apps/client/src/assets/site.webmanifest dist/apps/client"
|
||||
},
|
||||
{
|
||||
"command": "shx cp -r apps/client/src/locales dist/apps/api/assets"
|
||||
},
|
||||
{
|
||||
"command": "shx cp node_modules/ionicons/dist/index.js dist/apps/client"
|
||||
},
|
||||
@ -138,7 +144,7 @@
|
||||
"command": "shx cp node_modules/ionicons/dist/ionicons.js dist/apps/client"
|
||||
},
|
||||
{
|
||||
"command": "shx cp -r node_modules/ionicons/dist/ionicons dist/apps/client/ionicons"
|
||||
"command": "shx cp -r node_modules/ionicons/dist/ionicons/* dist/apps/client/ionicons"
|
||||
},
|
||||
{
|
||||
"command": "shx cp CHANGELOG.md dist/apps/client/assets"
|
||||
@ -146,7 +152,8 @@
|
||||
{
|
||||
"command": "shx cp LICENSE dist/apps/client/assets"
|
||||
}
|
||||
]
|
||||
],
|
||||
"parallel": false
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
|
@ -227,7 +227,8 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
||||
}
|
||||
],
|
||||
range: 'max',
|
||||
withExcludedAccounts: true
|
||||
withExcludedAccounts: true,
|
||||
withItems: true
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ chart }) => {
|
||||
|
@ -40,12 +40,7 @@
|
||||
[tooltip]="element.Platform?.name"
|
||||
[url]="element.Platform?.url"
|
||||
/>
|
||||
<span>{{ element.name }} </span>
|
||||
<span
|
||||
*ngIf="element.isDefault"
|
||||
class="d-lg-inline-block d-none text-muted"
|
||||
>(Default)</span
|
||||
>
|
||||
<span>{{ element.name }}</span>
|
||||
</td>
|
||||
<td *matFooterCellDef class="px-1" i18n mat-footer-cell>Total</td>
|
||||
</ng-container>
|
||||
@ -261,7 +256,7 @@
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
[disabled]="element.isDefault || element.transactionCount > 0"
|
||||
[disabled]="element.transactionCount > 0"
|
||||
(click)="onDeleteAccount(element.id)"
|
||||
>
|
||||
<span class="align-items-center d-flex">
|
||||
|
@ -154,8 +154,8 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
||||
this.dataService
|
||||
.fetchPositions({ range: this.user?.settings?.dateRange })
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((response) => {
|
||||
this.positions = response.positions;
|
||||
.subscribe(({ positions }) => {
|
||||
this.positions = positions;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
@ -127,10 +127,10 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
||||
this.isLoadingPerformance = false;
|
||||
|
||||
this.historicalDataItems = chart.map(
|
||||
({ date, netPerformanceInPercentage }) => {
|
||||
({ date, netPerformanceInPercentageWithCurrencyEffect }) => {
|
||||
return {
|
||||
date,
|
||||
value: netPerformanceInPercentage
|
||||
value: netPerformanceInPercentageWithCurrencyEffect
|
||||
};
|
||||
}
|
||||
);
|
||||
|
@ -40,7 +40,11 @@
|
||||
[colorizeSign]="true"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : performance?.currentNetPerformance"
|
||||
[value]="
|
||||
isLoading
|
||||
? undefined
|
||||
: performance?.currentNetPerformanceWithCurrencyEffect
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="col">
|
||||
@ -49,7 +53,9 @@
|
||||
[isPercent]="true"
|
||||
[locale]="locale"
|
||||
[value]="
|
||||
isLoading ? undefined : performance?.currentNetPerformancePercent
|
||||
isLoading
|
||||
? undefined
|
||||
: performance?.currentNetPerformancePercentWithCurrencyEffect
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
@ -63,7 +63,8 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
|
||||
} else if (this.showDetails === false) {
|
||||
new CountUp(
|
||||
'value',
|
||||
this.performance?.currentNetPerformancePercent * 100,
|
||||
this.performance?.currentNetPerformancePercentWithCurrencyEffect *
|
||||
100,
|
||||
{
|
||||
decimal: getNumberFormatDecimal(this.locale),
|
||||
decimalPlaces: 2,
|
||||
|
@ -64,7 +64,11 @@
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.currentGrossPerformance"
|
||||
[value]="
|
||||
isLoading
|
||||
? undefined
|
||||
: summary?.currentGrossPerformanceWithCurrencyEffect
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -85,7 +89,9 @@
|
||||
[isPercent]="true"
|
||||
[locale]="locale"
|
||||
[value]="
|
||||
isLoading ? undefined : summary?.currentGrossPerformancePercent
|
||||
isLoading
|
||||
? undefined
|
||||
: summary?.currentGrossPerformancePercentWithCurrencyEffect
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
@ -114,7 +120,11 @@
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.currentNetPerformance"
|
||||
[value]="
|
||||
isLoading
|
||||
? undefined
|
||||
: summary?.currentNetPerformanceWithCurrencyEffect
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -134,7 +144,11 @@
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : summary?.currentNetPerformancePercent"
|
||||
[value]="
|
||||
isLoading
|
||||
? undefined
|
||||
: summary?.currentNetPerformancePercentWithCurrencyEffect
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -283,7 +297,11 @@
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : summary?.annualizedPerformancePercent"
|
||||
[value]="
|
||||
isLoading
|
||||
? undefined
|
||||
: summary?.annualizedPerformancePercentWithCurrencyEffect
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -50,15 +50,13 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
||||
public dividendInBaseCurrency: number;
|
||||
public feeInBaseCurrency: number;
|
||||
public firstBuyDate: string;
|
||||
public grossPerformance: number;
|
||||
public grossPerformancePercent: number;
|
||||
public historicalDataItems: LineChartItem[];
|
||||
public investment: number;
|
||||
public marketPrice: number;
|
||||
public maxPrice: number;
|
||||
public minPrice: number;
|
||||
public netPerformance: number;
|
||||
public netPerformancePercent: number;
|
||||
public netPerformancePercentWithCurrencyEffect: number;
|
||||
public netPerformanceWithCurrencyEffect: number;
|
||||
public quantity: number;
|
||||
public quantityPrecision = 2;
|
||||
public reportDataGlitchMail: string;
|
||||
@ -99,15 +97,13 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
||||
dividendInBaseCurrency,
|
||||
feeInBaseCurrency,
|
||||
firstBuyDate,
|
||||
grossPerformance,
|
||||
grossPerformancePercent,
|
||||
historicalData,
|
||||
investment,
|
||||
marketPrice,
|
||||
maxPrice,
|
||||
minPrice,
|
||||
netPerformance,
|
||||
netPerformancePercent,
|
||||
netPerformancePercentWithCurrencyEffect,
|
||||
netPerformanceWithCurrencyEffect,
|
||||
orders,
|
||||
quantity,
|
||||
SymbolProfile,
|
||||
@ -125,8 +121,6 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
||||
this.dividendInBaseCurrency = dividendInBaseCurrency;
|
||||
this.feeInBaseCurrency = feeInBaseCurrency;
|
||||
this.firstBuyDate = firstBuyDate;
|
||||
this.grossPerformance = grossPerformance;
|
||||
this.grossPerformancePercent = grossPerformancePercent;
|
||||
this.historicalDataItems = historicalData.map(
|
||||
(historicalDataItem) => {
|
||||
this.benchmarkDataItems.push({
|
||||
@ -144,8 +138,10 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
||||
this.marketPrice = marketPrice;
|
||||
this.maxPrice = maxPrice;
|
||||
this.minPrice = minPrice;
|
||||
this.netPerformance = netPerformance;
|
||||
this.netPerformancePercent = netPerformancePercent;
|
||||
this.netPerformancePercentWithCurrencyEffect =
|
||||
netPerformancePercentWithCurrencyEffect;
|
||||
this.netPerformanceWithCurrencyEffect =
|
||||
netPerformanceWithCurrencyEffect;
|
||||
this.quantity = quantity;
|
||||
this.reportDataGlitchMail = `mailto:hi@ghostfol.io?Subject=Ghostfolio Data Glitch Report&body=Hello%0D%0DI would like to report a data glitch for%0D%0DSymbol: ${SymbolProfile?.symbol}%0DData Source: ${SymbolProfile?.dataSource}%0D%0DAdditional notes:%0D%0DCan you please take a look?%0D%0DKind regards`;
|
||||
this.sectors = {};
|
||||
|
@ -44,7 +44,7 @@
|
||||
[isCurrency]="true"
|
||||
[locale]="data.locale"
|
||||
[unit]="data.baseCurrency"
|
||||
[value]="netPerformance"
|
||||
[value]="netPerformanceWithCurrencyEffect"
|
||||
>Change</gf-value
|
||||
>
|
||||
</div>
|
||||
@ -55,7 +55,7 @@
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="data.locale"
|
||||
[value]="netPerformancePercent"
|
||||
[value]="netPerformancePercentWithCurrencyEffect"
|
||||
>Performance</gf-value
|
||||
>
|
||||
</div>
|
||||
|
@ -17,7 +17,7 @@
|
||||
[isLoading]="isLoading"
|
||||
[marketState]="position?.marketState"
|
||||
[range]="range"
|
||||
[value]="position?.netPerformancePercentage"
|
||||
[value]="position?.netPerformancePercentageWithCurrencyEffect"
|
||||
/>
|
||||
</div>
|
||||
<div *ngIf="isLoading" class="flex-grow-1">
|
||||
@ -49,13 +49,13 @@
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="position?.netPerformance"
|
||||
[value]="position?.netPerformanceWithCurrencyEffect"
|
||||
/>
|
||||
<gf-value
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="locale"
|
||||
[value]="position?.netPerformancePercentage"
|
||||
[value]="position?.netPerformancePercentageWithCurrencyEffect"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -18,6 +18,76 @@
|
||||
<a href="https://github.com/ghostfolio/ghostfolio">GitHub</a>.
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title>How do I add a new currency?</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<p>
|
||||
Ghostfolio manages currencies automatically based on all the
|
||||
recorded activities. If you need an additional currency, you can
|
||||
manually enter it.
|
||||
</p>
|
||||
<ol>
|
||||
<li>Go to the <i>Admin Control</i> panel</li>
|
||||
<li>Click on the <i>Add Currency</i> button</li>
|
||||
<li>Insert e.g. <code>EUR</code> in the prompt</li>
|
||||
</ol>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title
|
||||
>How do I resolve
|
||||
<i>No exchange rate has been found</i> errors?</mat-card-title
|
||||
>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<p>
|
||||
In Ghostfolio, you are responsible for providing the relevant
|
||||
historical exchange rates. This can be done with a one-time import
|
||||
of the data. If you see errors like
|
||||
<i
|
||||
>Historical exchange rate at 2024-01-01 is not available from
|
||||
"EUR" to "USD"</i
|
||||
>
|
||||
do the following:
|
||||
</p>
|
||||
<ol>
|
||||
<li>Go to the <i>Admin Control</i> panel</li>
|
||||
<li>Go to the <i>Market Data</i> section</li>
|
||||
<li>Select <i>Filter by Currencies</i></li>
|
||||
<li>Find the entry <i>USDEUR</i></li>
|
||||
<li>
|
||||
Click the menu item <i>Gather Historical Data</i> in the dialog
|
||||
</li>
|
||||
</ol>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title>How do I add a new platform?</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<ol>
|
||||
<li>Go to the <i>Admin Control</i> panel</li>
|
||||
<li>Go to the <i>Settings</i> section</li>
|
||||
<li>Click on the <i>Add Platform</i> button</li>
|
||||
</ol>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title>How do I add a new tag?</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<ol>
|
||||
<li>Go to the <i>Admin Control</i> panel</li>
|
||||
<li>Go to the <i>Settings</i> section</li>
|
||||
<li>Click on the <i>Add Tag</i> button</li>
|
||||
</ol>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Which devices are supported?</mat-card-title>
|
||||
|
@ -10,6 +10,7 @@
|
||||
app, asset, cryptocurrency, dashboard, etf, finance, management,
|
||||
performance, portfolio, software, stock, trading, wealth, web3
|
||||
</li>
|
||||
<li i18n="@@myAccount">My Account</li>
|
||||
<li i18n="@@slogan">Open Source Wealth Management Software</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -36,7 +36,6 @@ import { ImportActivitiesDialogParams } from './import-activities-dialog/interfa
|
||||
export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
||||
public activities: Activity[];
|
||||
public dataSource: MatTableDataSource<Activity>;
|
||||
public defaultAccountId: string;
|
||||
public deviceType: string;
|
||||
public hasImpersonationId: boolean;
|
||||
public hasPermissionToCreateActivity: boolean;
|
||||
@ -323,7 +322,7 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
||||
accounts: this.user?.accounts,
|
||||
activity: {
|
||||
...aActivity,
|
||||
accountId: aActivity?.accountId ?? this.defaultAccountId,
|
||||
accountId: aActivity?.accountId,
|
||||
date: new Date(),
|
||||
id: null,
|
||||
fee: 0,
|
||||
@ -399,10 +398,6 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
||||
private updateUser(aUser: User) {
|
||||
this.user = aUser;
|
||||
|
||||
this.defaultAccountId = this.user?.accounts.find((account) => {
|
||||
return account.isDefault;
|
||||
})?.id;
|
||||
|
||||
this.hasPermissionToCreateActivity =
|
||||
!this.hasImpersonationId &&
|
||||
hasPermission(this.user.permissions, permissions.createOrder);
|
||||
|
@ -20,6 +20,7 @@ import { DateAdapter, MAT_DATE_LOCALE } from '@angular/material/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { AssetClass, AssetSubClass, Tag, Type } from '@prisma/client';
|
||||
import { isUUID } from 'class-validator';
|
||||
import { isToday } from 'date-fns';
|
||||
import { EMPTY, Observable, Subject, lastValueFrom, of } from 'rxjs';
|
||||
import { catchError, delay, map, startWith, takeUntil } from 'rxjs/operators';
|
||||
|
||||
@ -48,6 +49,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
||||
public defaultDateFormat: string;
|
||||
public filteredTagsObservable: Observable<Tag[]> = of([]);
|
||||
public isLoading = false;
|
||||
public isToday = isToday;
|
||||
public platforms: { id: string; name: string }[];
|
||||
public separatorKeysCodes: number[] = [ENTER, COMMA];
|
||||
public tags: Tag[] = [];
|
||||
|
@ -240,7 +240,8 @@
|
||||
<button
|
||||
*ngIf="
|
||||
currentMarketPrice &&
|
||||
(data.activity.type === 'BUY' || data.activity.type === 'SELL')
|
||||
(data.activity.type === 'BUY' || data.activity.type === 'SELL') &&
|
||||
isToday(activityForm.controls['date']?.value)
|
||||
"
|
||||
class="ml-2 mt-1 no-min-width"
|
||||
mat-button
|
||||
|
@ -270,23 +270,28 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
index,
|
||||
{
|
||||
date,
|
||||
netPerformanceInPercentage,
|
||||
totalInvestment,
|
||||
value,
|
||||
valueInPercentage
|
||||
netPerformanceInPercentageWithCurrencyEffect,
|
||||
totalInvestmentValueWithCurrencyEffect,
|
||||
valueInPercentage,
|
||||
valueWithCurrencyEffect
|
||||
}
|
||||
] of chart.entries()) {
|
||||
if (index > 0 || this.user?.settings?.dateRange === 'max') {
|
||||
// Ignore first item where value is 0
|
||||
this.investments.push({ date, investment: totalInvestment });
|
||||
this.investments.push({
|
||||
date,
|
||||
investment: totalInvestmentValueWithCurrencyEffect
|
||||
});
|
||||
this.performanceDataItems.push({
|
||||
date,
|
||||
value: isNumber(value) ? value : valueInPercentage
|
||||
value: isNumber(valueWithCurrencyEffect)
|
||||
? valueWithCurrencyEffect
|
||||
: valueInPercentage
|
||||
});
|
||||
}
|
||||
this.performanceDataItemsInPercentage.push({
|
||||
date,
|
||||
value: netPerformanceInPercentage
|
||||
value: netPerformanceInPercentageWithCurrencyEffect
|
||||
});
|
||||
}
|
||||
|
||||
@ -305,10 +310,10 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ positions }) => {
|
||||
const positionsSorted = sortBy(
|
||||
positions.filter(({ netPerformancePercentage }) => {
|
||||
return isNumber(netPerformancePercentage);
|
||||
positions.filter(({ netPerformancePercentageWithCurrencyEffect }) => {
|
||||
return isNumber(netPerformancePercentageWithCurrencyEffect);
|
||||
}),
|
||||
'netPerformancePercentage'
|
||||
'netPerformancePercentageWithCurrencyEffect'
|
||||
).reverse();
|
||||
|
||||
this.top3 = positionsSorted.slice(0, 3);
|
||||
|
@ -18,136 +18,147 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (user?.settings?.isExperimentalFeatures) {
|
||||
<div class="mb-5 row">
|
||||
<div class="col">
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-content>
|
||||
<div class="d-flex py-1">
|
||||
<div class="flex-grow-1 mr-2 text-truncate" i18n>
|
||||
Absolute Asset Performance
|
||||
</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
position="end"
|
||||
[isCurrency]="true"
|
||||
[locale]="user?.settings?.locale"
|
||||
[unit]="user?.settings?.baseCurrency"
|
||||
[value]="
|
||||
isLoadingInvestmentChart
|
||||
? undefined
|
||||
: performance?.currentNetPerformance
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-5 row">
|
||||
<div class="col">
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-content>
|
||||
<div class="d-flex py-1">
|
||||
<div
|
||||
class="align-items-center d-flex flex-grow-1 mr-2 text-truncate"
|
||||
>
|
||||
<span i18n>Absolute Asset Performance</span>
|
||||
<gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
/>
|
||||
</div>
|
||||
<div class="d-flex mb-3 ml-3 py-1">
|
||||
<div class="flex-grow-1 mr-2 text-truncate" i18n>
|
||||
Asset Performance
|
||||
</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
position="end"
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="
|
||||
isLoadingInvestmentChart
|
||||
? undefined
|
||||
: performance?.currentNetPerformancePercent
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
position="end"
|
||||
[isCurrency]="true"
|
||||
[locale]="user?.settings?.locale"
|
||||
[unit]="user?.settings?.baseCurrency"
|
||||
[value]="
|
||||
isLoadingInvestmentChart
|
||||
? undefined
|
||||
: performance?.currentNetPerformance
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="d-flex py-1">
|
||||
<div class="flex-grow-1 mr-2 text-truncate" i18n>
|
||||
Absolute Currency Performance
|
||||
</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
position="end"
|
||||
[isCurrency]="true"
|
||||
[locale]="user?.settings?.locale"
|
||||
[unit]="user?.settings?.baseCurrency"
|
||||
[value]="
|
||||
isLoadingInvestmentChart
|
||||
? undefined
|
||||
: performance?.currentNetPerformanceWithCurrencyEffect ===
|
||||
null
|
||||
? null
|
||||
: performance?.currentNetPerformanceWithCurrencyEffect -
|
||||
performance?.currentNetPerformance
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex mb-3 ml-3 py-1">
|
||||
<div class="flex-grow-1 mr-2 text-truncate" i18n>
|
||||
Asset Performance
|
||||
</div>
|
||||
<div class="d-flex ml-3 py-1">
|
||||
<div class="flex-grow-1 mr-2 text-truncate" i18n>
|
||||
Currency Performance
|
||||
</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
position="end"
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="
|
||||
isLoadingInvestmentChart
|
||||
? undefined
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
position="end"
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="
|
||||
isLoadingInvestmentChart
|
||||
? undefined
|
||||
: performance?.currentNetPerformancePercent
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex py-1">
|
||||
<div
|
||||
class="align-items-center d-flex flex-grow-1 mr-2 text-truncate"
|
||||
>
|
||||
<span i18n>Absolute Currency Performance</span>
|
||||
<gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
/>
|
||||
</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
position="end"
|
||||
[isCurrency]="true"
|
||||
[locale]="user?.settings?.locale"
|
||||
[unit]="user?.settings?.baseCurrency"
|
||||
[value]="
|
||||
isLoadingInvestmentChart
|
||||
? undefined
|
||||
: performance?.currentNetPerformance === null
|
||||
? null
|
||||
: performance?.currentNetPerformanceWithCurrencyEffect -
|
||||
performance?.currentNetPerformance
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex ml-3 py-1">
|
||||
<div class="flex-grow-1 mr-2 text-truncate" i18n>
|
||||
Currency Performance
|
||||
</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
position="end"
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="
|
||||
isLoadingInvestmentChart
|
||||
? undefined
|
||||
: performance?.currentNetPerformancePercent === null
|
||||
? null
|
||||
: performance?.currentNetPerformancePercentWithCurrencyEffect -
|
||||
performance?.currentNetPerformancePercent
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div><hr /></div>
|
||||
<div class="d-flex py-1">
|
||||
<div class="flex-grow-1 mr-2 text-truncate" i18n>
|
||||
Absolute Net Performance
|
||||
</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
position="end"
|
||||
[isCurrency]="true"
|
||||
[locale]="user?.settings?.locale"
|
||||
[unit]="user?.settings?.baseCurrency"
|
||||
[value]="
|
||||
isLoadingInvestmentChart
|
||||
? undefined
|
||||
: performance?.currentNetPerformanceWithCurrencyEffect
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div><hr /></div>
|
||||
<div class="d-flex py-1">
|
||||
<div class="flex-grow-1 mr-2 text-truncate" i18n>
|
||||
Absolute Net Performance
|
||||
</div>
|
||||
<div class="d-flex ml-3 py-1">
|
||||
<div class="flex-grow-1 mr-2 text-truncate" i18n>
|
||||
Net Performance
|
||||
</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
position="end"
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="
|
||||
isLoadingInvestmentChart
|
||||
? undefined
|
||||
: performance?.currentNetPerformancePercentWithCurrencyEffect
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
position="end"
|
||||
[isCurrency]="true"
|
||||
[locale]="user?.settings?.locale"
|
||||
[unit]="user?.settings?.baseCurrency"
|
||||
[value]="
|
||||
isLoadingInvestmentChart
|
||||
? undefined
|
||||
: performance?.currentNetPerformanceWithCurrencyEffect
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex ml-3 py-1">
|
||||
<div class="flex-grow-1 mr-2 text-truncate" i18n>
|
||||
Net Performance
|
||||
</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
position="end"
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="
|
||||
isLoadingInvestmentChart
|
||||
? undefined
|
||||
: performance?.currentNetPerformancePercentWithCurrencyEffect
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="mb-5 row">
|
||||
<div class="col-md-6">
|
||||
@ -177,7 +188,9 @@
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="position.netPerformancePercentage"
|
||||
[value]="
|
||||
position.netPerformancePercentageWithCurrencyEffect
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
@ -223,7 +236,9 @@
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="position.netPerformancePercentage"
|
||||
[value]="
|
||||
position.netPerformancePercentageWithCurrencyEffect
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
|
@ -437,11 +437,13 @@ export class DataService {
|
||||
public fetchPortfolioPerformance({
|
||||
filters,
|
||||
range,
|
||||
withExcludedAccounts = false
|
||||
withExcludedAccounts = false,
|
||||
withItems = false
|
||||
}: {
|
||||
filters?: Filter[];
|
||||
range: DateRange;
|
||||
withExcludedAccounts?: boolean;
|
||||
withItems?: boolean;
|
||||
}): Observable<PortfolioPerformanceResponse> {
|
||||
let params = this.buildFiltersAsQueryParams({ filters });
|
||||
params = params.append('range', range);
|
||||
@ -450,6 +452,10 @@ export class DataService {
|
||||
params = params.append('withExcludedAccounts', withExcludedAccounts);
|
||||
}
|
||||
|
||||
if (withItems) {
|
||||
params = params.append('withItems', withItems);
|
||||
}
|
||||
|
||||
return this.http
|
||||
.get<any>(`/api/v2/portfolio/performance`, {
|
||||
params
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"createdAt": "2024-02-16T00:00:00.000Z",
|
||||
"createdAt": "2024-02-29T00:00:00.000Z",
|
||||
"data": [
|
||||
{
|
||||
"name": "Aptabase",
|
||||
@ -76,11 +76,6 @@
|
||||
"description": "Open-Source Webhooks-as-a-service (WaaS) that makes it easy for developers to send webhooks.",
|
||||
"href": "https://www.hook0.com"
|
||||
},
|
||||
{
|
||||
"name": "HTMX",
|
||||
"description": "HTMX is a dependency-free JavaScript library that allows you to access AJAX, CSS Transitions, WebSockets, and Server Sent Events directly in HTML.",
|
||||
"href": "https://htmx.org"
|
||||
},
|
||||
{
|
||||
"name": "Inbox Zero",
|
||||
"description": "Inbox Zero makes it easy to clean up your inbox and reach inbox zero fast. It provides bulk newsletter unsubscribe, cold email blocking, email analytics, and AI automations.",
|
||||
@ -116,11 +111,6 @@
|
||||
"description": "Open-source monitoring platform with beautiful status pages",
|
||||
"href": "https://www.openstatus.dev"
|
||||
},
|
||||
{
|
||||
"name": "Papermark",
|
||||
"description": "Open-Source Docsend Alternative to securely share documents with real-time analytics.",
|
||||
"href": "https://www.papermark.io"
|
||||
},
|
||||
{
|
||||
"name": "Prisma",
|
||||
"description": "Simplify working with databases. Build, optimize, and grow your app easily with an intuitive data model, type-safety, automated migrations, connection pooling, caching, and real-time db subscriptions.",
|
||||
@ -156,6 +146,11 @@
|
||||
"description": "The .NET Web Framework for Makers. Build production ready, full-stack web applications fast without sweating the small stuff.",
|
||||
"href": "https://spark-framework.net"
|
||||
},
|
||||
{
|
||||
"name": "Tiledesk",
|
||||
"description": "The innovative open-source framework for developing LLM-enabled chatbots, Tiledesk empowers developers to create advanced, conversational AI agents.",
|
||||
"href": "https://tiledesk.com"
|
||||
},
|
||||
{
|
||||
"name": "Tolgee",
|
||||
"description": "Software localization from A to Z made really easy.",
|
||||
|
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
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -393,6 +393,6 @@ export function resolveMarketCondition(
|
||||
} else if (aMarketCondition === 'BEAR_MARKET') {
|
||||
return { emoji: '🐻' };
|
||||
} else {
|
||||
return { emoji: '⚪' };
|
||||
return { emoji: undefined };
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ export interface Export {
|
||||
date: string;
|
||||
version: string;
|
||||
};
|
||||
accounts: Omit<Account, 'createdAt' | 'isDefault' | 'updatedAt' | 'userId'>[];
|
||||
accounts: Omit<Account, 'createdAt' | 'updatedAt' | 'userId'>[];
|
||||
activities: (Omit<
|
||||
Order,
|
||||
| 'accountUserId'
|
||||
|
@ -17,6 +17,8 @@ export interface PortfolioPosition {
|
||||
exchange?: string;
|
||||
grossPerformance: number;
|
||||
grossPerformancePercent: number;
|
||||
grossPerformancePercentWithCurrencyEffect: number;
|
||||
grossPerformanceWithCurrencyEffect: number;
|
||||
investment: number;
|
||||
marketChange?: number;
|
||||
marketChangePercent?: number;
|
||||
@ -27,6 +29,8 @@ export interface PortfolioPosition {
|
||||
name: string;
|
||||
netPerformance: number;
|
||||
netPerformancePercent: number;
|
||||
netPerformancePercentWithCurrencyEffect: number;
|
||||
netPerformanceWithCurrencyEffect: number;
|
||||
quantity: number;
|
||||
sectors: Sector[];
|
||||
symbol: string;
|
||||
|
@ -13,7 +13,7 @@ export interface PortfolioPublicDetails {
|
||||
| 'dateOfFirstActivity'
|
||||
| 'markets'
|
||||
| 'name'
|
||||
| 'netPerformancePercent'
|
||||
| 'netPerformancePercentWithCurrencyEffect'
|
||||
| 'sectors'
|
||||
| 'symbol'
|
||||
| 'url'
|
||||
|
@ -2,6 +2,7 @@ import { PortfolioPerformance } from './portfolio-performance.interface';
|
||||
|
||||
export interface PortfolioSummary extends PortfolioPerformance {
|
||||
annualizedPerformancePercent: number;
|
||||
annualizedPerformancePercentWithCurrencyEffect: number;
|
||||
cash: number;
|
||||
committedFunds: number;
|
||||
dividend: number;
|
||||
|
@ -18,6 +18,8 @@ export interface Position {
|
||||
name?: string;
|
||||
netPerformance?: number;
|
||||
netPerformancePercentage?: number;
|
||||
netPerformancePercentageWithCurrencyEffect?: number;
|
||||
netPerformanceWithCurrencyEffect?: number;
|
||||
quantity: number;
|
||||
symbol: string;
|
||||
transactionCount: number;
|
||||
|
@ -102,7 +102,7 @@
|
||||
<th *matHeaderCellDef mat-header-cell></th>
|
||||
<td *matCellDef="let element" class="px-0" mat-cell>
|
||||
@if (element?.marketCondition) {
|
||||
<div class="text-center" [title]="element?.marketCondition">
|
||||
<div class="text-center" [title]="translate(element.marketCondition)">
|
||||
{{ resolveMarketCondition(element.marketCondition).emoji }}
|
||||
</div>
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { getLocale, resolveMarketCondition } from '@ghostfolio/common/helper';
|
||||
import { Benchmark, User } from '@ghostfolio/common/interfaces';
|
||||
import { translate } from '@ghostfolio/ui/i18n';
|
||||
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
@ -21,6 +22,7 @@ export class BenchmarkComponent implements OnChanges {
|
||||
|
||||
public displayedColumns = ['name', 'date', 'change', 'marketCondition'];
|
||||
public resolveMarketCondition = resolveMarketCondition;
|
||||
public translate = translate;
|
||||
|
||||
public constructor() {}
|
||||
|
||||
|
@ -159,7 +159,7 @@ export class CurrencySelectorComponent
|
||||
|
||||
private validateRequired() {
|
||||
const requiredCheck = super.required
|
||||
? !super.value.label || !super.value.value
|
||||
? !super.value?.label || !super.value?.value
|
||||
: false;
|
||||
|
||||
if (requiredCheck) {
|
||||
|
@ -114,7 +114,7 @@
|
||||
*matHeaderCellDef
|
||||
class="justify-content-end px-1"
|
||||
mat-header-cell
|
||||
mat-sort-header="netPerformancePercent"
|
||||
mat-sort-header="netPerformancePercentWithCurrencyEffect"
|
||||
>
|
||||
<span class="d-none d-sm-block" i18n>Performance</span>
|
||||
<span class="d-block d-sm-none" title="Performance">±</span>
|
||||
@ -125,7 +125,11 @@
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : element.netPerformancePercent"
|
||||
[value]="
|
||||
isLoading
|
||||
? undefined
|
||||
: element.netPerformancePercentWithCurrencyEffect
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
|
@ -35,14 +35,14 @@ const locales = {
|
||||
LIABILITY: $localize`Liability`,
|
||||
SELL: $localize`Sell`,
|
||||
|
||||
// enum AssetClass
|
||||
// AssetClass (enum)
|
||||
CASH: $localize`Cash`,
|
||||
COMMODITY: $localize`Commodity`,
|
||||
EQUITY: $localize`Equity`,
|
||||
FIXED_INCOME: $localize`Fixed Income`,
|
||||
REAL_ESTATE: $localize`Real Estate`,
|
||||
|
||||
// enum AssetSubClass
|
||||
// AssetSubClass (enum)
|
||||
BOND: $localize`Bond`,
|
||||
CRYPTOCURRENCY: $localize`Cryptocurrency`,
|
||||
ETF: $localize`ETF`,
|
||||
@ -51,6 +51,10 @@ const locales = {
|
||||
PRIVATE_EQUITY: $localize`Private Equity`,
|
||||
STOCK: $localize`Stock`,
|
||||
|
||||
// Benchmark
|
||||
ALL_TIME_HIGH: 'All time high',
|
||||
BEAR_MARKET: 'Bear market',
|
||||
|
||||
// Continents
|
||||
Africa: $localize`Africa`,
|
||||
Asia: $localize`Asia`,
|
||||
|
@ -21,7 +21,11 @@
|
||||
h4: size === 'medium'
|
||||
}"
|
||||
>
|
||||
{{ formattedValue }}%
|
||||
@if (value === null) {
|
||||
<span class="text-monospace text-muted">*****</span>%
|
||||
} @else {
|
||||
{{ formattedValue }}%
|
||||
}
|
||||
</div>
|
||||
<div
|
||||
*ngIf="!isPercent"
|
||||
@ -31,12 +35,11 @@
|
||||
h4: size === 'medium'
|
||||
}"
|
||||
>
|
||||
<ng-container *ngIf="value === null">
|
||||
@if (value === null) {
|
||||
<span class="text-monospace text-muted">*****</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="value !== null">
|
||||
} @else {
|
||||
{{ formattedValue }}
|
||||
</ng-container>
|
||||
}
|
||||
</div>
|
||||
<small *ngIf="unit && size === 'medium'" class="ml-1">
|
||||
{{ unit }}
|
||||
|
13
package.json
13
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ghostfolio",
|
||||
"version": "2.55.0",
|
||||
"version": "2.60.0",
|
||||
"homepage": "https://ghostfol.io",
|
||||
"license": "AGPL-3.0",
|
||||
"repository": "https://github.com/ghostfolio/ghostfolio",
|
||||
@ -14,8 +14,7 @@
|
||||
"affected:lint": "nx affected:lint",
|
||||
"affected:test": "nx affected:test",
|
||||
"angular": "node --max_old_space_size=32768 ./node_modules/@angular/cli/bin/ng",
|
||||
"build:dev": "nx run api:build && nx run client:build && nx run client:copy-assets && yarn replace-placeholders-in-build",
|
||||
"build:production": "nx run api:build:production && nx run client:build:production && nx run client:copy-assets && yarn replace-placeholders-in-build",
|
||||
"build:production": "nx run api:copy-assets && nx run api:build:production && nx run client:copy-assets && nx run client:build:production && yarn replace-placeholders-in-build",
|
||||
"build:storybook": "nx run ui:build-storybook",
|
||||
"database:format-schema": "prisma format",
|
||||
"database:generate-typings": "prisma generate",
|
||||
@ -41,7 +40,7 @@
|
||||
"start": "node dist/apps/api/main",
|
||||
"start:client": "nx run client:copy-assets && nx run client:serve --configuration=development-en --hmr -o",
|
||||
"start:production": "yarn database:migrate && yarn database:seed && node main",
|
||||
"start:server": "nx run api:serve --watch",
|
||||
"start:server": "nx run api:copy-assets && nx run api:serve --watch",
|
||||
"start:storybook": "nx run ui:storybook",
|
||||
"test": "yarn test:api && yarn test:common",
|
||||
"test:api": "npx dotenv-cli -e .env.example -- nx test api",
|
||||
@ -49,7 +48,7 @@
|
||||
"test:single": "nx run api:test --test-file portfolio-calculator-novn-buy-and-sell.spec.ts",
|
||||
"ts-node": "ts-node",
|
||||
"update": "nx migrate latest",
|
||||
"watch:server": "nx run api:build --watch",
|
||||
"watch:server": "nx run api:copy-assets && nx run api:build --watch",
|
||||
"watch:test": "nx test --watch",
|
||||
"workspace-generator": "nx workspace-generator"
|
||||
},
|
||||
@ -83,7 +82,7 @@
|
||||
"@nestjs/platform-express": "10.1.3",
|
||||
"@nestjs/schedule": "3.0.2",
|
||||
"@nestjs/serve-static": "4.0.0",
|
||||
"@prisma/client": "5.9.1",
|
||||
"@prisma/client": "5.10.2",
|
||||
"@simplewebauthn/browser": "8.3.1",
|
||||
"@simplewebauthn/server": "8.3.2",
|
||||
"@stripe/stripe-js": "1.47.0",
|
||||
@ -125,7 +124,7 @@
|
||||
"passport": "0.6.0",
|
||||
"passport-google-oauth20": "2.0.0",
|
||||
"passport-jwt": "4.0.0",
|
||||
"prisma": "5.9.1",
|
||||
"prisma": "5.10.2",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rxjs": "7.5.6",
|
||||
"stripe": "11.12.0",
|
||||
|
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Account" DROP COLUMN "isDefault";
|
@ -0,0 +1,2 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Account_isExcluded_idx" ON "Account"("isExcluded");
|
@ -32,7 +32,6 @@ model Account {
|
||||
createdAt DateTime @default(now())
|
||||
currency String?
|
||||
id String @default(uuid())
|
||||
isDefault Boolean @default(false)
|
||||
isExcluded Boolean @default(false)
|
||||
name String?
|
||||
platformId String?
|
||||
@ -45,6 +44,7 @@ model Account {
|
||||
@@id([id, userId])
|
||||
@@index([currency])
|
||||
@@index([id])
|
||||
@@index([isExcluded])
|
||||
@@index([name])
|
||||
@@index([userId])
|
||||
}
|
||||
|
74
yarn.lock
74
yarn.lock
@ -5275,46 +5275,46 @@
|
||||
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
|
||||
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
|
||||
|
||||
"@prisma/client@5.9.1":
|
||||
version "5.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.9.1.tgz#d92bd2f7f006e0316cb4fda9d73f235965cf2c64"
|
||||
integrity sha512-caSOnG4kxcSkhqC/2ShV7rEoWwd3XrftokxJqOCMVvia4NYV/TPtJlS9C2os3Igxw/Qyxumj9GBQzcStzECvtQ==
|
||||
"@prisma/client@5.10.2":
|
||||
version "5.10.2"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.10.2.tgz#e087b40a4de8e3171eb9cbf0a873465cd2068e17"
|
||||
integrity sha512-ef49hzB2yJZCvM5gFHMxSFL9KYrIP9udpT5rYo0CsHD4P9IKj473MbhU1gjKKftiwWBTIyrt9jukprzZXazyag==
|
||||
|
||||
"@prisma/debug@5.9.1":
|
||||
version "5.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-5.9.1.tgz#906274e73d3267f88b69459199fa3c51cd9511a3"
|
||||
integrity sha512-yAHFSFCg8KVoL0oRUno3m60GAjsUKYUDkQ+9BA2X2JfVR3kRVSJFc/GpQ2fSORi4pSHZR9orfM4UC9OVXIFFTA==
|
||||
"@prisma/debug@5.10.2":
|
||||
version "5.10.2"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-5.10.2.tgz#74be81d8969978f4d53c1b4e76d61f04bfbc3951"
|
||||
integrity sha512-bkBOmH9dpEBbMKFJj8V+Zp8IZHIBjy3fSyhLhxj4FmKGb/UBSt9doyfA6k1UeUREsMJft7xgPYBbHSOYBr8XCA==
|
||||
|
||||
"@prisma/engines-version@5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64":
|
||||
version "5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64.tgz#54d2164f28d23e09d41cf9eb0bddbbe7f3aaa660"
|
||||
integrity sha512-HFl7275yF0FWbdcNvcSRbbu9JCBSLMcurYwvWc8WGDnpu7APxQo2ONtZrUggU3WxLxUJ2uBX+0GOFIcJeVeOOQ==
|
||||
"@prisma/engines-version@5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9":
|
||||
version "5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9.tgz#1502335d4d72d2014cb25b8ad8a740a3a13400ea"
|
||||
integrity sha512-uCy/++3Jx/O3ufM+qv2H1L4tOemTNqcP/gyEVOlZqTpBvYJUe0tWtW0y3o2Ueq04mll4aM5X3f6ugQftOSLdFQ==
|
||||
|
||||
"@prisma/engines@5.9.1":
|
||||
version "5.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.9.1.tgz#767539afc6f193a182d0495b30b027f61f279073"
|
||||
integrity sha512-gkdXmjxQ5jktxWNdDA5aZZ6R8rH74JkoKq6LD5mACSvxd2vbqWeWIOV0Py5wFC8vofOYShbt6XUeCIUmrOzOnQ==
|
||||
"@prisma/engines@5.10.2":
|
||||
version "5.10.2"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.10.2.tgz#a4851d90f76ad6d22e783d5fd2e2e8c0640f1e81"
|
||||
integrity sha512-HkSJvix6PW8YqEEt3zHfCYYJY69CXsNdhU+wna+4Y7EZ+AwzeupMnUThmvaDA7uqswiHkgm5/SZ6/4CStjaGmw==
|
||||
dependencies:
|
||||
"@prisma/debug" "5.9.1"
|
||||
"@prisma/engines-version" "5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64"
|
||||
"@prisma/fetch-engine" "5.9.1"
|
||||
"@prisma/get-platform" "5.9.1"
|
||||
"@prisma/debug" "5.10.2"
|
||||
"@prisma/engines-version" "5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9"
|
||||
"@prisma/fetch-engine" "5.10.2"
|
||||
"@prisma/get-platform" "5.10.2"
|
||||
|
||||
"@prisma/fetch-engine@5.9.1":
|
||||
version "5.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/fetch-engine/-/fetch-engine-5.9.1.tgz#5d3b2c9af54a242e37b3f9561b59ab72f8e92818"
|
||||
integrity sha512-l0goQOMcNVOJs1kAcwqpKq3ylvkD9F04Ioe1oJoCqmz05mw22bNAKKGWuDd3zTUoUZr97va0c/UfLNru+PDmNA==
|
||||
"@prisma/fetch-engine@5.10.2":
|
||||
version "5.10.2"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/fetch-engine/-/fetch-engine-5.10.2.tgz#a061f6727d395c7033b55f9c6e92f8741a70d5c5"
|
||||
integrity sha512-dSmXcqSt6DpTmMaLQ9K8ZKzVAMH3qwGCmYEZr/uVnzVhxRJ1EbT/w2MMwIdBNq1zT69Rvh0h75WMIi0mrIw7Hg==
|
||||
dependencies:
|
||||
"@prisma/debug" "5.9.1"
|
||||
"@prisma/engines-version" "5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64"
|
||||
"@prisma/get-platform" "5.9.1"
|
||||
"@prisma/debug" "5.10.2"
|
||||
"@prisma/engines-version" "5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9"
|
||||
"@prisma/get-platform" "5.10.2"
|
||||
|
||||
"@prisma/get-platform@5.9.1":
|
||||
version "5.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-5.9.1.tgz#a66bb46ab4d30db786c84150ef074ab0aad4549e"
|
||||
integrity sha512-6OQsNxTyhvG+T2Ksr8FPFpuPeL4r9u0JF0OZHUBI/Uy9SS43sPyAIutt4ZEAyqWQt104ERh70EZedkHZKsnNbg==
|
||||
"@prisma/get-platform@5.10.2":
|
||||
version "5.10.2"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-5.10.2.tgz#7af97b1d82e5574a474e3fbf6eaf04f4156bc535"
|
||||
integrity sha512-nqXP6vHiY2PIsebBAuDeWiUYg8h8mfjBckHh6Jezuwej0QJNnjDiOq30uesmg+JXxGk99nqyG3B7wpcOODzXvg==
|
||||
dependencies:
|
||||
"@prisma/debug" "5.9.1"
|
||||
"@prisma/debug" "5.10.2"
|
||||
|
||||
"@radix-ui/number@1.0.1":
|
||||
version "1.0.1"
|
||||
@ -16425,12 +16425,12 @@ pretty-hrtime@^1.0.3:
|
||||
resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
|
||||
integrity sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==
|
||||
|
||||
prisma@5.9.1:
|
||||
version "5.9.1"
|
||||
resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.9.1.tgz#baa3dd635fbf71504980978f10f55ea11068f6aa"
|
||||
integrity sha512-Hy/8KJZz0ELtkw4FnG9MS9rNWlXcJhf98Z2QMqi0QiVMoS8PzsBkpla0/Y5hTlob8F3HeECYphBjqmBxrluUrQ==
|
||||
prisma@5.10.2:
|
||||
version "5.10.2"
|
||||
resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.10.2.tgz#aa63085c49dc74cdb5c3816e8dd1fb4d74a2aadd"
|
||||
integrity sha512-hqb/JMz9/kymRE25pMWCxkdyhbnIWrq+h7S6WysJpdnCvhstbJSNP/S6mScEcqiB8Qv2F+0R3yG+osRaWqZacQ==
|
||||
dependencies:
|
||||
"@prisma/engines" "5.9.1"
|
||||
"@prisma/engines" "5.10.2"
|
||||
|
||||
prismjs@^1.28.0:
|
||||
version "1.29.0"
|
||||
|
Reference in New Issue
Block a user