Compare commits
35 Commits
Author | SHA1 | Date | |
---|---|---|---|
795a6a6799 | |||
2a854e2574 | |||
52d113e71f | |||
204c7360c3 | |||
fa41e25c8f | |||
ba765b9de6 | |||
fa79196278 | |||
d1230ca3ad | |||
69a1316cfe | |||
a256b783bc | |||
ebbdd47fa2 | |||
3d21e2eac6 | |||
bc117fe601 | |||
65f6bcb166 | |||
b8c43ecf89 | |||
1214127ec0 | |||
e986310302 | |||
6762572658 | |||
eb77652d6a | |||
a7b59f4ec6 | |||
dd71f2be45 | |||
d530cb38fa | |||
16b79a7e60 | |||
7f0c98cae6 | |||
57e4163848 | |||
14773bf1aa | |||
1a8fc5757a | |||
b4848be914 | |||
2b4319454d | |||
e2faaf6faa | |||
86a1589834 | |||
9f67993c03 | |||
32fb3551dc | |||
30411b1502 | |||
eb0444603b |
64
CHANGELOG.md
64
CHANGELOG.md
@ -5,6 +5,70 @@ 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.132.0 - 06.04.2022
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support for localization (date and number format) in user settings
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the label of the average price from _Ø Buy Price_ to _Average Unit Price_
|
||||||
|
|
||||||
|
## 1.131.1 - 04.04.2022
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the missing API version in the _Stripe_ success callback url
|
||||||
|
|
||||||
|
## 1.131.0 - 02.04.2022
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added API versioning
|
||||||
|
- Added more durations in the coupon system
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Display the value in base currency in the accounts table on mobile
|
||||||
|
- Display the value in base currency in the activities table on mobile
|
||||||
|
- Renamed `orders` to `activities` in import and export functionality
|
||||||
|
- Harmonized the algebraic sign of `currentGrossPerformancePercent` and `currentNetPerformancePercent` with `currentGrossPerformance` and `currentNetPerformance`
|
||||||
|
- Improved the pricing page
|
||||||
|
- Upgraded `prisma` from version `3.10.0` to `3.11.1`
|
||||||
|
- Upgraded `yahoo-finance2` from version `2.2.0` to `2.3.0`
|
||||||
|
|
||||||
|
## 1.130.0 - 30.03.2022
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added a _FIRE_ (Financial Independence, Retire Early) section including the 4% rule
|
||||||
|
- Added more durations in the coupon system
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue with the currency conversion (duplicate) in the account calculations
|
||||||
|
|
||||||
|
## 1.129.0 - 26.03.2022
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the calculation for developed vs. emerging markets to the allocations page
|
||||||
|
- Added a hover effect to the page tabs
|
||||||
|
- Extended the feature overview page by _Bonds_ and _Emergency Fund_
|
||||||
|
|
||||||
|
## 1.128.0 - 19.03.2022
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the attribute `defaultMarketPrice` to the scraper configuration to improve the support for bonds
|
||||||
|
- Added a hover effect to the table style
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue with the user currency of the public page
|
||||||
|
- Fixed an issue of the performance calculation with recent activities in the new calculation engine
|
||||||
|
|
||||||
## 1.127.0 - 16.03.2022
|
## 1.127.0 - 16.03.2022
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
86
README.md
86
README.md
@ -79,6 +79,7 @@ The frontend is built with [Angular](https://angular.io) and uses [Angular Mater
|
|||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- [Docker](https://www.docker.com/products/docker-desktop)
|
- [Docker](https://www.docker.com/products/docker-desktop)
|
||||||
|
- A local copy of this Git repository (clone)
|
||||||
|
|
||||||
### a. Run environment
|
### a. Run environment
|
||||||
|
|
||||||
@ -121,13 +122,11 @@ Open http://localhost:3333 in your browser and accomplish these steps:
|
|||||||
1. Go to the _Admin Control Panel_ and click _Gather All Data_ to fetch historical data
|
1. Go to the _Admin Control Panel_ and click _Gather All Data_ to fetch historical data
|
||||||
1. Click _Sign out_ and check out the _Live Demo_
|
1. Click _Sign out_ and check out the _Live Demo_
|
||||||
|
|
||||||
### Migrate Database
|
### Upgrade Version
|
||||||
|
|
||||||
With the following command you can keep your database schema in sync after a Ghostfolio version update:
|
1. Increase the version of the `ghostfolio/ghostfolio` Docker image in `docker/docker-compose.yml`
|
||||||
|
1. Run the following command to start the new Docker image: `docker-compose -f docker/docker-compose.yml up -d`
|
||||||
```bash
|
1. Then, run the following command to keep your database schema in sync: `docker-compose -f docker/docker-compose.yml exec ghostfolio yarn database:migrate`
|
||||||
docker-compose -f docker/docker-compose-build-local.yml exec ghostfolio yarn database:migrate
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
@ -136,6 +135,7 @@ docker-compose -f docker/docker-compose-build-local.yml exec ghostfolio yarn dat
|
|||||||
- [Docker](https://www.docker.com/products/docker-desktop)
|
- [Docker](https://www.docker.com/products/docker-desktop)
|
||||||
- [Node.js](https://nodejs.org/en/download) (version 14+)
|
- [Node.js](https://nodejs.org/en/download) (version 14+)
|
||||||
- [Yarn](https://yarnpkg.com/en/docs/install)
|
- [Yarn](https://yarnpkg.com/en/docs/install)
|
||||||
|
- A local copy of this Git repository (clone)
|
||||||
|
|
||||||
### Setup
|
### Setup
|
||||||
|
|
||||||
@ -162,10 +162,84 @@ Run `yarn start:client`
|
|||||||
|
|
||||||
Run `yarn start:storybook`
|
Run `yarn start:storybook`
|
||||||
|
|
||||||
|
### Migrate Database
|
||||||
|
|
||||||
|
With the following command you can keep your database schema in sync:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn database:push
|
||||||
|
```
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
Run `yarn test`
|
Run `yarn test`
|
||||||
|
|
||||||
|
## Public API (experimental)
|
||||||
|
|
||||||
|
### Import Activities
|
||||||
|
|
||||||
|
#### Request
|
||||||
|
|
||||||
|
`POST http://localhost:3333/api/v1/import`
|
||||||
|
|
||||||
|
#### Authorization: Bearer Token
|
||||||
|
|
||||||
|
Set the header as follows:
|
||||||
|
|
||||||
|
```
|
||||||
|
"Authorization": "Bearer eyJh..."
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Body
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"activities": [
|
||||||
|
{
|
||||||
|
"currency": "USD",
|
||||||
|
"dataSource": "YAHOO",
|
||||||
|
"date": "2021-09-15T00:00:00.000Z",
|
||||||
|
"fee": 19,
|
||||||
|
"quantity": 5,
|
||||||
|
"symbol": "MSFT"
|
||||||
|
"type": "BUY",
|
||||||
|
"unitPrice": 298.58
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
| ---------- | ------------------- | -------------------------------------------------- |
|
||||||
|
| accountId | string (`optional`) | Id of the account |
|
||||||
|
| currency | string | `CHF` \| `EUR` \| `USD` etc. |
|
||||||
|
| dataSource | string | `MANUAL` (for type `ITEM`) \| `YAHOO` |
|
||||||
|
| date | string | Date in the format `ISO-8601` |
|
||||||
|
| fee | number | Fee of the activity |
|
||||||
|
| quantity | number | Quantity of the activity |
|
||||||
|
| symbol | string | Symbol of the activity (suitable for `dataSource`) |
|
||||||
|
| type | string | `BUY` \| `DIVIDEND` \| `ITEM` \| `SELL` |
|
||||||
|
| unitPrice | number | Price per unit of the activity |
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
##### Success
|
||||||
|
|
||||||
|
`201 Created`
|
||||||
|
|
||||||
|
##### Error
|
||||||
|
|
||||||
|
`400 Bad Request`
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"error": "Bad Request",
|
||||||
|
"message": [
|
||||||
|
"activities.1 is a duplicate activity"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Ghostfolio is **100% free** and **open source**. We encourage and support an active and healthy community that accepts contributions from the public - including you.
|
Ghostfolio is **100% free** and **open source**. We encourage and support an active and healthy community that accepts contributions from the public - including you.
|
||||||
|
@ -9,7 +9,9 @@ import {
|
|||||||
Post,
|
Post,
|
||||||
Req,
|
Req,
|
||||||
Res,
|
Res,
|
||||||
UseGuards
|
UseGuards,
|
||||||
|
VERSION_NEUTRAL,
|
||||||
|
Version
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
@ -51,6 +53,7 @@ export class AuthController {
|
|||||||
|
|
||||||
@Get('google/callback')
|
@Get('google/callback')
|
||||||
@UseGuards(AuthGuard('google'))
|
@UseGuards(AuthGuard('google'))
|
||||||
|
@Version(VERSION_NEUTRAL)
|
||||||
public googleLoginCallback(@Req() req, @Res() res) {
|
public googleLoginCallback(@Req() req, @Res() res) {
|
||||||
// Handles the Google OAuth2 callback
|
// Handles the Google OAuth2 callback
|
||||||
const jwt: string = req.user.jwt;
|
const jwt: string = req.user.jwt;
|
||||||
|
@ -1,13 +1,6 @@
|
|||||||
import { Export } from '@ghostfolio/common/interfaces';
|
import { Export } from '@ghostfolio/common/interfaces';
|
||||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import {
|
import { Controller, Get, Inject, Query, UseGuards } from '@nestjs/common';
|
||||||
Controller,
|
|
||||||
Get,
|
|
||||||
Headers,
|
|
||||||
Inject,
|
|
||||||
Query,
|
|
||||||
UseGuards
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ export class ExportService {
|
|||||||
activityIds?: string[];
|
activityIds?: string[];
|
||||||
userId: string;
|
userId: string;
|
||||||
}): Promise<Export> {
|
}): Promise<Export> {
|
||||||
let orders = await this.prismaService.order.findMany({
|
let activities = await this.prismaService.order.findMany({
|
||||||
orderBy: { date: 'desc' },
|
orderBy: { date: 'desc' },
|
||||||
select: {
|
select: {
|
||||||
accountId: true,
|
accountId: true,
|
||||||
@ -30,14 +30,14 @@ export class ExportService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (activityIds) {
|
if (activityIds) {
|
||||||
orders = orders.filter((order) => {
|
activities = activities.filter((activity) => {
|
||||||
return activityIds.includes(order.id);
|
return activityIds.includes(activity.id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
meta: { date: new Date().toISOString(), version: environment.version },
|
meta: { date: new Date().toISOString(), version: environment.version },
|
||||||
orders: orders.map(
|
activities: activities.map(
|
||||||
({
|
({
|
||||||
accountId,
|
accountId,
|
||||||
date,
|
date,
|
||||||
|
@ -6,5 +6,5 @@ export class ImportDataDto {
|
|||||||
@IsArray()
|
@IsArray()
|
||||||
@Type(() => CreateOrderDto)
|
@Type(() => CreateOrderDto)
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
orders: CreateOrderDto[];
|
activities: CreateOrderDto[];
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ export class ImportController {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
return await this.importService.import({
|
return await this.importService.import({
|
||||||
orders: importData.orders,
|
activities: importData.activities,
|
||||||
userId: this.request.user.id
|
userId: this.request.user.id
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -16,23 +16,23 @@ export class ImportService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async import({
|
public async import({
|
||||||
orders,
|
activities,
|
||||||
userId
|
userId
|
||||||
}: {
|
}: {
|
||||||
orders: Partial<CreateOrderDto>[];
|
activities: Partial<CreateOrderDto>[];
|
||||||
userId: string;
|
userId: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
for (const order of orders) {
|
for (const activity of activities) {
|
||||||
if (!order.dataSource) {
|
if (!activity.dataSource) {
|
||||||
if (order.type === 'ITEM') {
|
if (activity.type === 'ITEM') {
|
||||||
order.dataSource = 'MANUAL';
|
activity.dataSource = 'MANUAL';
|
||||||
} else {
|
} else {
|
||||||
order.dataSource = this.dataProviderService.getPrimaryDataSource();
|
activity.dataSource = this.dataProviderService.getPrimaryDataSource();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.validateOrders({ orders, userId });
|
await this.validateActivities({ activities, userId });
|
||||||
|
|
||||||
const accountIds = (await this.accountService.getAccounts(userId)).map(
|
const accountIds = (await this.accountService.getAccounts(userId)).map(
|
||||||
(account) => {
|
(account) => {
|
||||||
@ -50,7 +50,7 @@ export class ImportService {
|
|||||||
symbol,
|
symbol,
|
||||||
type,
|
type,
|
||||||
unitPrice
|
unitPrice
|
||||||
} of orders) {
|
} of activities) {
|
||||||
await this.orderService.createOrder({
|
await this.orderService.createOrder({
|
||||||
fee,
|
fee,
|
||||||
quantity,
|
quantity,
|
||||||
@ -79,24 +79,24 @@ export class ImportService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async validateOrders({
|
private async validateActivities({
|
||||||
orders,
|
activities,
|
||||||
userId
|
userId
|
||||||
}: {
|
}: {
|
||||||
orders: Partial<CreateOrderDto>[];
|
activities: Partial<CreateOrderDto>[];
|
||||||
userId: string;
|
userId: string;
|
||||||
}) {
|
}) {
|
||||||
if (
|
if (
|
||||||
orders?.length > this.configurationService.get('MAX_ORDERS_TO_IMPORT')
|
activities?.length > this.configurationService.get('MAX_ORDERS_TO_IMPORT')
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Too many transactions (${this.configurationService.get(
|
`Too many activities (${this.configurationService.get(
|
||||||
'MAX_ORDERS_TO_IMPORT'
|
'MAX_ORDERS_TO_IMPORT'
|
||||||
)} at most)`
|
)} at most)`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingOrders = await this.orderService.orders({
|
const existingActivities = await this.orderService.orders({
|
||||||
include: { SymbolProfile: true },
|
include: { SymbolProfile: true },
|
||||||
orderBy: { date: 'desc' },
|
orderBy: { date: 'desc' },
|
||||||
where: { userId }
|
where: { userId }
|
||||||
@ -105,22 +105,22 @@ export class ImportService {
|
|||||||
for (const [
|
for (const [
|
||||||
index,
|
index,
|
||||||
{ currency, dataSource, date, fee, quantity, symbol, type, unitPrice }
|
{ currency, dataSource, date, fee, quantity, symbol, type, unitPrice }
|
||||||
] of orders.entries()) {
|
] of activities.entries()) {
|
||||||
const duplicateOrder = existingOrders.find((order) => {
|
const duplicateActivity = existingActivities.find((activity) => {
|
||||||
return (
|
return (
|
||||||
order.SymbolProfile.currency === currency &&
|
activity.SymbolProfile.currency === currency &&
|
||||||
order.SymbolProfile.dataSource === dataSource &&
|
activity.SymbolProfile.dataSource === dataSource &&
|
||||||
isSameDay(order.date, parseISO(<string>(<unknown>date))) &&
|
isSameDay(activity.date, parseISO(<string>(<unknown>date))) &&
|
||||||
order.fee === fee &&
|
activity.fee === fee &&
|
||||||
order.quantity === quantity &&
|
activity.quantity === quantity &&
|
||||||
order.SymbolProfile.symbol === symbol &&
|
activity.SymbolProfile.symbol === symbol &&
|
||||||
order.type === type &&
|
activity.type === type &&
|
||||||
order.unitPrice === unitPrice
|
activity.unitPrice === unitPrice
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (duplicateOrder) {
|
if (duplicateActivity) {
|
||||||
throw new Error(`orders.${index} is a duplicate transaction`);
|
throw new Error(`activities.${index} is a duplicate activity`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dataSource !== 'MANUAL') {
|
if (dataSource !== 'MANUAL') {
|
||||||
@ -130,13 +130,13 @@ export class ImportService {
|
|||||||
|
|
||||||
if (quotes[symbol] === undefined) {
|
if (quotes[symbol] === undefined) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`orders.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")`
|
`activities.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (quotes[symbol].currency !== currency) {
|
if (quotes[symbol].currency !== currency) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`orders.${index}.currency ("${currency}") does not match with "${quotes[symbol].currency}"`
|
`activities.${index}.currency ("${currency}") does not match with "${quotes[symbol].currency}"`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,9 @@ import { TransactionPointSymbol } from './interfaces/transaction-point-symbol.in
|
|||||||
import { TransactionPoint } from './interfaces/transaction-point.interface';
|
import { TransactionPoint } from './interfaces/transaction-point.interface';
|
||||||
|
|
||||||
export class PortfolioCalculatorNew {
|
export class PortfolioCalculatorNew {
|
||||||
|
private static readonly CALCULATE_PERCENTAGE_PERFORMANCE_WITH_MAX_INVESTMENT =
|
||||||
|
true;
|
||||||
|
|
||||||
private static readonly ENABLE_LOGGING = false;
|
private static readonly ENABLE_LOGGING = false;
|
||||||
|
|
||||||
private currency: string;
|
private currency: string;
|
||||||
@ -688,6 +691,7 @@ export class PortfolioCalculatorNew {
|
|||||||
let grossPerformanceAtStartDate = new Big(0);
|
let grossPerformanceAtStartDate = new Big(0);
|
||||||
let grossPerformanceFromSells = new Big(0);
|
let grossPerformanceFromSells = new Big(0);
|
||||||
let initialValue: Big;
|
let initialValue: Big;
|
||||||
|
let investmentAtStartDate: Big;
|
||||||
let lastAveragePrice = new Big(0);
|
let lastAveragePrice = new Big(0);
|
||||||
let lastTransactionInvestment = new Big(0);
|
let lastTransactionInvestment = new Big(0);
|
||||||
let lastValueOfInvestmentBeforeTransaction = new Big(0);
|
let lastValueOfInvestmentBeforeTransaction = new Big(0);
|
||||||
@ -697,6 +701,7 @@ export class PortfolioCalculatorNew {
|
|||||||
let totalInvestment = new Big(0);
|
let totalInvestment = new Big(0);
|
||||||
let totalInvestmentWithGrossPerformanceFromSell = new Big(0);
|
let totalInvestmentWithGrossPerformanceFromSell = new Big(0);
|
||||||
let totalUnits = new Big(0);
|
let totalUnits = new Big(0);
|
||||||
|
let valueAtStartDate: Big;
|
||||||
|
|
||||||
// Add a synthetic order at the start and the end date
|
// Add a synthetic order at the start and the end date
|
||||||
orders.push({
|
orders.push({
|
||||||
@ -774,13 +779,18 @@ export class PortfolioCalculatorNew {
|
|||||||
order.unitPrice
|
order.unitPrice
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!investmentAtStartDate && i >= indexOfStartOrder) {
|
||||||
|
investmentAtStartDate = totalInvestment ?? new Big(0);
|
||||||
|
valueAtStartDate = valueOfInvestmentBeforeTransaction;
|
||||||
|
}
|
||||||
|
|
||||||
const transactionInvestment = order.quantity
|
const transactionInvestment = order.quantity
|
||||||
.mul(order.unitPrice)
|
.mul(order.unitPrice)
|
||||||
.mul(this.getFactor(order.type));
|
.mul(this.getFactor(order.type));
|
||||||
|
|
||||||
totalInvestment = totalInvestment.plus(transactionInvestment);
|
totalInvestment = totalInvestment.plus(transactionInvestment);
|
||||||
|
|
||||||
if (totalInvestment.gt(maxTotalInvestment)) {
|
if (i >= indexOfStartOrder && totalInvestment.gt(maxTotalInvestment)) {
|
||||||
maxTotalInvestment = totalInvestment;
|
maxTotalInvestment = totalInvestment;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -898,12 +908,22 @@ export class PortfolioCalculatorNew {
|
|||||||
.minus(grossPerformanceAtStartDate)
|
.minus(grossPerformanceAtStartDate)
|
||||||
.minus(fees.minus(feesAtStartDate));
|
.minus(fees.minus(feesAtStartDate));
|
||||||
|
|
||||||
|
const maxInvestmentBetweenStartAndEndDate = valueAtStartDate.plus(
|
||||||
|
maxTotalInvestment.minus(investmentAtStartDate)
|
||||||
|
);
|
||||||
|
|
||||||
const grossPerformancePercentage =
|
const grossPerformancePercentage =
|
||||||
|
PortfolioCalculatorNew.CALCULATE_PERCENTAGE_PERFORMANCE_WITH_MAX_INVESTMENT ||
|
||||||
averagePriceAtStartDate.eq(0) ||
|
averagePriceAtStartDate.eq(0) ||
|
||||||
averagePriceAtEndDate.eq(0) ||
|
averagePriceAtEndDate.eq(0) ||
|
||||||
orders[indexOfStartOrder].unitPrice.eq(0)
|
orders[indexOfStartOrder].unitPrice.eq(0)
|
||||||
? totalGrossPerformance.div(maxTotalInvestment)
|
? maxInvestmentBetweenStartAndEndDate.gt(0)
|
||||||
: unitPriceAtEndDate
|
? totalGrossPerformance.div(maxInvestmentBetweenStartAndEndDate)
|
||||||
|
: new Big(0)
|
||||||
|
: // This formula has the issue that buying more units with a price
|
||||||
|
// lower than the average buying price results in a positive
|
||||||
|
// performance even if the market price stays constant
|
||||||
|
unitPriceAtEndDate
|
||||||
.div(averagePriceAtEndDate)
|
.div(averagePriceAtEndDate)
|
||||||
.div(
|
.div(
|
||||||
orders[indexOfStartOrder].unitPrice.div(averagePriceAtStartDate)
|
orders[indexOfStartOrder].unitPrice.div(averagePriceAtStartDate)
|
||||||
@ -915,11 +935,17 @@ export class PortfolioCalculatorNew {
|
|||||||
: new Big(0);
|
: new Big(0);
|
||||||
|
|
||||||
const netPerformancePercentage =
|
const netPerformancePercentage =
|
||||||
|
PortfolioCalculatorNew.CALCULATE_PERCENTAGE_PERFORMANCE_WITH_MAX_INVESTMENT ||
|
||||||
averagePriceAtStartDate.eq(0) ||
|
averagePriceAtStartDate.eq(0) ||
|
||||||
averagePriceAtEndDate.eq(0) ||
|
averagePriceAtEndDate.eq(0) ||
|
||||||
orders[indexOfStartOrder].unitPrice.eq(0)
|
orders[indexOfStartOrder].unitPrice.eq(0)
|
||||||
? totalNetPerformance.div(maxTotalInvestment)
|
? maxInvestmentBetweenStartAndEndDate.gt(0)
|
||||||
: unitPriceAtEndDate
|
? totalNetPerformance.div(maxInvestmentBetweenStartAndEndDate)
|
||||||
|
: new Big(0)
|
||||||
|
: // This formula has the issue that buying more units with a price
|
||||||
|
// lower than the average buying price results in a positive
|
||||||
|
// performance even if the market price stays constant
|
||||||
|
unitPriceAtEndDate
|
||||||
.minus(feesPerUnit)
|
.minus(feesPerUnit)
|
||||||
.div(averagePriceAtEndDate)
|
.div(averagePriceAtEndDate)
|
||||||
.div(
|
.div(
|
||||||
|
@ -13,8 +13,9 @@ export class PortfolioServiceStrategy {
|
|||||||
@Inject(REQUEST) private readonly request: RequestWithUser
|
@Inject(REQUEST) private readonly request: RequestWithUser
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public get() {
|
public get(newCalculationEngine?: boolean) {
|
||||||
if (
|
if (
|
||||||
|
newCalculationEngine ||
|
||||||
this.request.user?.Settings?.settings?.['isNewCalculationEngine'] === true
|
this.request.user?.Settings?.settings?.['isNewCalculationEngine'] === true
|
||||||
) {
|
) {
|
||||||
return this.portfolioServiceNew;
|
return this.portfolioServiceNew;
|
||||||
|
@ -120,7 +120,7 @@ export class PortfolioController {
|
|||||||
|
|
||||||
const { accounts, holdings, hasErrors } =
|
const { accounts, holdings, hasErrors } =
|
||||||
await this.portfolioServiceStrategy
|
await this.portfolioServiceStrategy
|
||||||
.get()
|
.get(true)
|
||||||
.getDetails(impersonationId, this.request.user.id, range);
|
.getDetails(impersonationId, this.request.user.id, range);
|
||||||
|
|
||||||
if (hasErrors || hasNotDefinedValuesInObject(holdings)) {
|
if (hasErrors || hasNotDefinedValuesInObject(holdings)) {
|
||||||
@ -277,7 +277,7 @@ export class PortfolioController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { holdings } = await this.portfolioServiceStrategy
|
const { holdings } = await this.portfolioServiceStrategy
|
||||||
.get()
|
.get(true)
|
||||||
.getDetails(access.userId, access.userId);
|
.getDetails(access.userId, access.userId);
|
||||||
|
|
||||||
const portfolioPublicDetails: PortfolioPublicDetails = {
|
const portfolioPublicDetails: PortfolioPublicDetails = {
|
||||||
@ -304,6 +304,7 @@ export class PortfolioController {
|
|||||||
allocationCurrent: portfolioPosition.allocationCurrent,
|
allocationCurrent: portfolioPosition.allocationCurrent,
|
||||||
countries: hasDetails ? portfolioPosition.countries : [],
|
countries: hasDetails ? portfolioPosition.countries : [],
|
||||||
currency: portfolioPosition.currency,
|
currency: portfolioPosition.currency,
|
||||||
|
markets: portfolioPosition.markets,
|
||||||
name: portfolioPosition.name,
|
name: portfolioPosition.name,
|
||||||
sectors: hasDetails ? portfolioPosition.sectors : [],
|
sectors: hasDetails ? portfolioPosition.sectors : [],
|
||||||
value: portfolioPosition.value / totalValue
|
value: portfolioPosition.value / totalValue
|
||||||
@ -319,6 +320,16 @@ export class PortfolioController {
|
|||||||
public async getSummary(
|
public async getSummary(
|
||||||
@Headers('impersonation-id') impersonationId
|
@Headers('impersonation-id') impersonationId
|
||||||
): Promise<PortfolioSummary> {
|
): Promise<PortfolioSummary> {
|
||||||
|
if (
|
||||||
|
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
|
||||||
|
this.request.user.subscription.type === 'Basic'
|
||||||
|
) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
|
StatusCodes.FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let summary = await this.portfolioServiceStrategy
|
let summary = await this.portfolioServiceStrategy
|
||||||
.get()
|
.get()
|
||||||
.getSummary(impersonationId);
|
.getSummary(impersonationId);
|
||||||
|
@ -40,6 +40,7 @@ import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.in
|
|||||||
import type {
|
import type {
|
||||||
AccountWithValue,
|
AccountWithValue,
|
||||||
DateRange,
|
DateRange,
|
||||||
|
Market,
|
||||||
OrderWithAccount,
|
OrderWithAccount,
|
||||||
RequestWithUser
|
RequestWithUser
|
||||||
} from '@ghostfolio/common/types';
|
} from '@ghostfolio/common/types';
|
||||||
@ -71,6 +72,9 @@ import {
|
|||||||
import { PortfolioCalculatorNew } from './portfolio-calculator-new';
|
import { PortfolioCalculatorNew } from './portfolio-calculator-new';
|
||||||
import { RulesService } from './rules.service';
|
import { RulesService } from './rules.service';
|
||||||
|
|
||||||
|
const developedMarkets = require('../../assets/countries/developed-markets.json');
|
||||||
|
const emergingMarkets = require('../../assets/countries/emerging-markets.json');
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PortfolioServiceNew {
|
export class PortfolioServiceNew {
|
||||||
public constructor(
|
public constructor(
|
||||||
@ -107,21 +111,21 @@ export class PortfolioServiceNew {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = details.accounts[account.id]?.current ?? 0;
|
const valueInBaseCurrency = details.accounts[account.id]?.current ?? 0;
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
...account,
|
...account,
|
||||||
transactionCount,
|
transactionCount,
|
||||||
value,
|
valueInBaseCurrency,
|
||||||
balanceInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
balanceInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
||||||
account.balance,
|
account.balance,
|
||||||
account.currency,
|
account.currency,
|
||||||
userCurrency
|
userCurrency
|
||||||
),
|
),
|
||||||
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
value: this.exchangeRateDataService.toCurrency(
|
||||||
value,
|
valueInBaseCurrency,
|
||||||
account.currency,
|
userCurrency,
|
||||||
userCurrency
|
account.currency
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -307,7 +311,10 @@ export class PortfolioServiceNew {
|
|||||||
const emergencyFund = new Big(
|
const emergencyFund = new Big(
|
||||||
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
|
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
|
||||||
);
|
);
|
||||||
const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency;
|
const userCurrency =
|
||||||
|
this.request.user?.Settings?.currency ??
|
||||||
|
user.Settings?.currency ??
|
||||||
|
baseCurrency;
|
||||||
|
|
||||||
const { orders, portfolioOrders, transactionPoints } =
|
const { orders, portfolioOrders, transactionPoints } =
|
||||||
await this.getTransactionPoints({
|
await this.getTransactionPoints({
|
||||||
@ -377,7 +384,31 @@ export class PortfolioServiceNew {
|
|||||||
const value = item.quantity.mul(item.marketPrice);
|
const value = item.quantity.mul(item.marketPrice);
|
||||||
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 } = {
|
||||||
|
developedMarkets: 0,
|
||||||
|
emergingMarkets: 0,
|
||||||
|
otherMarkets: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const country of symbolProfile.countries) {
|
||||||
|
if (developedMarkets.includes(country.code)) {
|
||||||
|
markets.developedMarkets = new Big(markets.developedMarkets)
|
||||||
|
.plus(country.weight)
|
||||||
|
.toNumber();
|
||||||
|
} else if (emergingMarkets.includes(country.code)) {
|
||||||
|
markets.emergingMarkets = new Big(markets.emergingMarkets)
|
||||||
|
.plus(country.weight)
|
||||||
|
.toNumber();
|
||||||
|
} else {
|
||||||
|
markets.otherMarkets = new Big(markets.otherMarkets)
|
||||||
|
.plus(country.weight)
|
||||||
|
.toNumber();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
holdings[item.symbol] = {
|
holdings[item.symbol] = {
|
||||||
|
markets,
|
||||||
allocationCurrent: value.div(totalValue).toNumber(),
|
allocationCurrent: value.div(totalValue).toNumber(),
|
||||||
allocationInvestment: item.investment.div(totalInvestment).toNumber(),
|
allocationInvestment: item.investment.div(totalInvestment).toNumber(),
|
||||||
assetClass: symbolProfile.assetClass,
|
assetClass: symbolProfile.assetClass,
|
||||||
@ -779,23 +810,33 @@ export class PortfolioServiceNew {
|
|||||||
|
|
||||||
const hasErrors = currentPositions.hasErrors;
|
const hasErrors = currentPositions.hasErrors;
|
||||||
const currentValue = currentPositions.currentValue.toNumber();
|
const currentValue = currentPositions.currentValue.toNumber();
|
||||||
const currentGrossPerformance =
|
const currentGrossPerformance = currentPositions.grossPerformance;
|
||||||
currentPositions.grossPerformance.toNumber();
|
let currentGrossPerformancePercent =
|
||||||
const currentGrossPerformancePercent =
|
currentPositions.grossPerformancePercentage;
|
||||||
currentPositions.grossPerformancePercentage.toNumber();
|
const currentNetPerformance = currentPositions.netPerformance;
|
||||||
const currentNetPerformance = currentPositions.netPerformance.toNumber();
|
let currentNetPerformancePercent =
|
||||||
const currentNetPerformancePercent =
|
currentPositions.netPerformancePercentage;
|
||||||
currentPositions.netPerformancePercentage.toNumber();
|
|
||||||
|
if (currentGrossPerformance.mul(currentGrossPerformancePercent).lt(0)) {
|
||||||
|
// If algebraic sign is different, harmonize it
|
||||||
|
currentGrossPerformancePercent = currentGrossPerformancePercent.mul(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentNetPerformance.mul(currentNetPerformancePercent).lt(0)) {
|
||||||
|
// If algebraic sign is different, harmonize it
|
||||||
|
currentNetPerformancePercent = currentNetPerformancePercent.mul(-1);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
errors: currentPositions.errors,
|
errors: currentPositions.errors,
|
||||||
hasErrors: currentPositions.hasErrors || hasErrors,
|
hasErrors: currentPositions.hasErrors || hasErrors,
|
||||||
performance: {
|
performance: {
|
||||||
currentGrossPerformance,
|
currentValue,
|
||||||
currentGrossPerformancePercent,
|
currentGrossPerformance: currentGrossPerformance.toNumber(),
|
||||||
currentNetPerformance,
|
currentGrossPerformancePercent:
|
||||||
currentNetPerformancePercent,
|
currentGrossPerformancePercent.toNumber(),
|
||||||
currentValue
|
currentNetPerformance: currentNetPerformance.toNumber(),
|
||||||
|
currentNetPerformancePercent: currentNetPerformancePercent.toNumber()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -106,21 +106,21 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = details.accounts[account.id]?.current ?? 0;
|
const valueInBaseCurrency = details.accounts[account.id]?.current ?? 0;
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
...account,
|
...account,
|
||||||
transactionCount,
|
transactionCount,
|
||||||
value,
|
valueInBaseCurrency,
|
||||||
balanceInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
balanceInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
||||||
account.balance,
|
account.balance,
|
||||||
account.currency,
|
account.currency,
|
||||||
userCurrency
|
userCurrency
|
||||||
),
|
),
|
||||||
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
value: this.exchangeRateDataService.toCurrency(
|
||||||
value,
|
valueInBaseCurrency,
|
||||||
account.currency,
|
userCurrency,
|
||||||
userCurrency
|
account.currency
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -298,7 +298,10 @@ export class PortfolioService {
|
|||||||
const emergencyFund = new Big(
|
const emergencyFund = new Big(
|
||||||
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
|
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
|
||||||
);
|
);
|
||||||
const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency;
|
const userCurrency =
|
||||||
|
this.request.user?.Settings?.currency ??
|
||||||
|
user.Settings?.currency ??
|
||||||
|
baseCurrency;
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
this.currentRateService,
|
this.currentRateService,
|
||||||
userCurrency
|
userCurrency
|
||||||
|
@ -45,7 +45,7 @@ export class SubscriptionService {
|
|||||||
payment_method_types: ['card'],
|
payment_method_types: ['card'],
|
||||||
success_url: `${this.configurationService.get(
|
success_url: `${this.configurationService.get(
|
||||||
'ROOT_URL'
|
'ROOT_URL'
|
||||||
)}/api/subscription/stripe/callback?checkoutSessionId={CHECKOUT_SESSION_ID}`
|
)}/api/v1/subscription/stripe/callback?checkoutSessionId={CHECKOUT_SESSION_ID}`
|
||||||
};
|
};
|
||||||
|
|
||||||
if (couponId) {
|
if (couponId) {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export interface UserSettings {
|
export interface UserSettings {
|
||||||
emergencyFund?: number;
|
emergencyFund?: number;
|
||||||
|
locale?: string;
|
||||||
isNewCalculationEngine?: boolean;
|
isNewCalculationEngine?: boolean;
|
||||||
isRestrictedView?: boolean;
|
isRestrictedView?: boolean;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { IsBoolean, IsNumber, IsOptional } from 'class-validator';
|
import { IsBoolean, IsNumber, IsOptional, IsString } from 'class-validator';
|
||||||
|
|
||||||
export class UpdateUserSettingDto {
|
export class UpdateUserSettingDto {
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@ -12,4 +12,8 @@ export class UpdateUserSettingDto {
|
|||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
isRestrictedView?: boolean;
|
isRestrictedView?: boolean;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
locale?: string;
|
||||||
}
|
}
|
||||||
|
@ -2,17 +2,14 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration.ser
|
|||||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||||
import { PROPERTY_IS_READ_ONLY_MODE } from '@ghostfolio/common/config';
|
import { PROPERTY_IS_READ_ONLY_MODE } from '@ghostfolio/common/config';
|
||||||
import { User } from '@ghostfolio/common/interfaces';
|
import { User } from '@ghostfolio/common/interfaces';
|
||||||
import {
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
hasPermission,
|
|
||||||
hasRole,
|
|
||||||
permissions
|
|
||||||
} from '@ghostfolio/common/permissions';
|
|
||||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
Delete,
|
Delete,
|
||||||
Get,
|
Get,
|
||||||
|
Headers,
|
||||||
HttpException,
|
HttpException,
|
||||||
Inject,
|
Inject,
|
||||||
Param,
|
Param,
|
||||||
@ -63,8 +60,13 @@ export class UserController {
|
|||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
public async getUser(@Param('id') id: string): Promise<User> {
|
public async getUser(
|
||||||
return this.userService.getUser(this.request.user);
|
@Headers('accept-language') acceptLanguage: string
|
||||||
|
): Promise<User> {
|
||||||
|
return this.userService.getUser(
|
||||||
|
this.request.user,
|
||||||
|
acceptLanguage?.split(',')?.[0]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@ -118,7 +120,7 @@ export class UserController {
|
|||||||
};
|
};
|
||||||
|
|
||||||
for (const key in userSettings) {
|
for (const key in userSettings) {
|
||||||
if (userSettings[key] === false) {
|
if (userSettings[key] === false || userSettings[key] === null) {
|
||||||
delete userSettings[key];
|
delete userSettings[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,14 +33,17 @@ export class UserService {
|
|||||||
private readonly subscriptionService: SubscriptionService
|
private readonly subscriptionService: SubscriptionService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async getUser({
|
public async getUser(
|
||||||
|
{
|
||||||
Account,
|
Account,
|
||||||
alias,
|
alias,
|
||||||
id,
|
id,
|
||||||
permissions,
|
permissions,
|
||||||
Settings,
|
Settings,
|
||||||
subscription
|
subscription
|
||||||
}: UserWithSettings): Promise<IUser> {
|
}: UserWithSettings,
|
||||||
|
aLocale = locale
|
||||||
|
): Promise<IUser> {
|
||||||
const access = await this.prismaService.access.findMany({
|
const access = await this.prismaService.access.findMany({
|
||||||
include: {
|
include: {
|
||||||
User: true
|
User: true
|
||||||
@ -63,8 +66,8 @@ export class UserService {
|
|||||||
accounts: Account,
|
accounts: Account,
|
||||||
settings: {
|
settings: {
|
||||||
...(<UserSettings>Settings.settings),
|
...(<UserSettings>Settings.settings),
|
||||||
locale,
|
|
||||||
baseCurrency: Settings?.currency ?? UserService.DEFAULT_CURRENCY,
|
baseCurrency: Settings?.currency ?? UserService.DEFAULT_CURRENCY,
|
||||||
|
locale: (<UserSettings>Settings.settings)?.locale ?? aLocale,
|
||||||
viewMode: Settings?.viewMode ?? ViewMode.DEFAULT
|
viewMode: Settings?.viewMode ?? ViewMode.DEFAULT
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
26
apps/api/src/assets/countries/developed-markets.json
Normal file
26
apps/api/src/assets/countries/developed-markets.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
[
|
||||||
|
"AT",
|
||||||
|
"AU",
|
||||||
|
"BE",
|
||||||
|
"CA",
|
||||||
|
"CH",
|
||||||
|
"DE",
|
||||||
|
"DK",
|
||||||
|
"ES",
|
||||||
|
"FI",
|
||||||
|
"FR",
|
||||||
|
"GB",
|
||||||
|
"HK",
|
||||||
|
"IE",
|
||||||
|
"IL",
|
||||||
|
"IT",
|
||||||
|
"JP",
|
||||||
|
"LU",
|
||||||
|
"NL",
|
||||||
|
"NO",
|
||||||
|
"NZ",
|
||||||
|
"PT",
|
||||||
|
"SE",
|
||||||
|
"SG",
|
||||||
|
"US"
|
||||||
|
]
|
28
apps/api/src/assets/countries/emerging-markets.json
Normal file
28
apps/api/src/assets/countries/emerging-markets.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
[
|
||||||
|
"AE",
|
||||||
|
"BR",
|
||||||
|
"CL",
|
||||||
|
"CN",
|
||||||
|
"CO",
|
||||||
|
"CY",
|
||||||
|
"CZ",
|
||||||
|
"EG",
|
||||||
|
"GR",
|
||||||
|
"HK",
|
||||||
|
"HU",
|
||||||
|
"ID",
|
||||||
|
"IN",
|
||||||
|
"KR",
|
||||||
|
"KW",
|
||||||
|
"MX",
|
||||||
|
"MY",
|
||||||
|
"PE",
|
||||||
|
"PH",
|
||||||
|
"PL",
|
||||||
|
"QA",
|
||||||
|
"SA",
|
||||||
|
"TH",
|
||||||
|
"TR",
|
||||||
|
"TW",
|
||||||
|
"ZA"
|
||||||
|
]
|
@ -1,4 +1,4 @@
|
|||||||
import { Logger, ValidationPipe } from '@nestjs/common';
|
import { Logger, ValidationPipe, VersioningType } from '@nestjs/common';
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
|
||||||
import { AppModule } from './app/app.module';
|
import { AppModule } from './app/app.module';
|
||||||
@ -7,8 +7,11 @@ import { environment } from './environments/environment';
|
|||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
app.enableCors();
|
app.enableCors();
|
||||||
const globalPrefix = 'api';
|
app.enableVersioning({
|
||||||
app.setGlobalPrefix(globalPrefix);
|
defaultVersion: '1',
|
||||||
|
type: VersioningType.URI
|
||||||
|
});
|
||||||
|
app.setGlobalPrefix('api');
|
||||||
app.useGlobalPipes(
|
app.useGlobalPipes(
|
||||||
new ValidationPipe({
|
new ValidationPipe({
|
||||||
forbidNonWhitelisted: true,
|
forbidNonWhitelisted: true,
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
||||||
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import {
|
import {
|
||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
PortfolioPosition
|
PortfolioPosition
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
|
||||||
|
|
||||||
import { Rule } from '../../rule';
|
import { Rule } from '../../rule';
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
||||||
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { PortfolioDetails } from '@ghostfolio/common/interfaces';
|
import { PortfolioDetails } from '@ghostfolio/common/interfaces';
|
||||||
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
|
||||||
|
|
||||||
import { Rule } from '../../rule';
|
import { Rule } from '../../rule';
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
|
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
|
||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
||||||
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
|
|
||||||
import { Rule } from '../../rule';
|
import { Rule } from '../../rule';
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
|
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
|
||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
||||||
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
|
|
||||||
import { Rule } from '../../rule';
|
import { Rule } from '../../rule';
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
|
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
|
||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
||||||
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
|
|
||||||
import { Rule } from '../../rule';
|
import { Rule } from '../../rule';
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
||||||
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
|
|
||||||
import { Rule } from '../../rule';
|
import { Rule } from '../../rule';
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ import { Injectable, Logger } from '@nestjs/common';
|
|||||||
import { DataSource, SymbolProfile } from '@prisma/client';
|
import { DataSource, SymbolProfile } from '@prisma/client';
|
||||||
import * as bent from 'bent';
|
import * as bent from 'bent';
|
||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
import { format } from 'date-fns';
|
import { addDays, format, isBefore } from 'date-fns';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GhostfolioScraperApiService implements DataProviderInterface {
|
export class GhostfolioScraperApiService implements DataProviderInterface {
|
||||||
@ -50,9 +50,27 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
|||||||
const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles(
|
const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles(
|
||||||
[symbol]
|
[symbol]
|
||||||
);
|
);
|
||||||
const { selector, url } = symbolProfile.scraperConfiguration;
|
const { defaultMarketPrice, selector, url } =
|
||||||
|
symbolProfile.scraperConfiguration;
|
||||||
|
|
||||||
if (selector === undefined || url === undefined) {
|
if (defaultMarketPrice) {
|
||||||
|
const historical: {
|
||||||
|
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
||||||
|
} = {
|
||||||
|
[symbol]: {}
|
||||||
|
};
|
||||||
|
let date = from;
|
||||||
|
|
||||||
|
while (isBefore(date, to)) {
|
||||||
|
historical[symbol][format(date, DATE_FORMAT)] = {
|
||||||
|
marketPrice: defaultMarketPrice
|
||||||
|
};
|
||||||
|
|
||||||
|
date = addDays(date, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return historical;
|
||||||
|
} else if (selector === undefined || url === undefined) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
export interface ScraperConfiguration {
|
export interface ScraperConfiguration {
|
||||||
|
defaultMarketPrice?: number;
|
||||||
selector: string;
|
selector: string;
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
@ -79,6 +79,7 @@ export class SymbolProfileService {
|
|||||||
|
|
||||||
if (scraperConfiguration) {
|
if (scraperConfiguration) {
|
||||||
return {
|
return {
|
||||||
|
defaultMarketPrice: scraperConfiguration.defaultMarketPrice as number,
|
||||||
selector: scraperConfiguration.selector as string,
|
selector: scraperConfiguration.selector as string,
|
||||||
url: scraperConfiguration.url as string
|
url: scraperConfiguration.url as string
|
||||||
};
|
};
|
||||||
|
@ -1,16 +1,14 @@
|
|||||||
import {
|
import { DEFAULT_DATE_FORMAT_MONTH_YEAR } from '@ghostfolio/common/config';
|
||||||
DEFAULT_DATE_FORMAT,
|
import { getDateFormatString } from '@ghostfolio/common/helper';
|
||||||
DEFAULT_DATE_FORMAT_MONTH_YEAR
|
|
||||||
} from '@ghostfolio/common/config';
|
|
||||||
|
|
||||||
export const DateFormats = {
|
export const DateFormats = {
|
||||||
display: {
|
display: {
|
||||||
dateInput: DEFAULT_DATE_FORMAT,
|
dateInput: getDateFormatString(),
|
||||||
monthYearLabel: DEFAULT_DATE_FORMAT_MONTH_YEAR,
|
monthYearLabel: DEFAULT_DATE_FORMAT_MONTH_YEAR,
|
||||||
dateA11yLabel: DEFAULT_DATE_FORMAT,
|
dateA11yLabel: getDateFormatString(),
|
||||||
monthYearA11yLabel: DEFAULT_DATE_FORMAT_MONTH_YEAR
|
monthYearA11yLabel: DEFAULT_DATE_FORMAT_MONTH_YEAR
|
||||||
},
|
},
|
||||||
parse: {
|
parse: {
|
||||||
dateInput: DEFAULT_DATE_FORMAT
|
dateInput: getDateFormatString()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -113,6 +113,13 @@ const routes: Routes = [
|
|||||||
(m) => m.AnalysisPageModule
|
(m) => m.AnalysisPageModule
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'portfolio/fire',
|
||||||
|
loadChildren: () =>
|
||||||
|
import('./pages/portfolio/fire/fire-page.module').then(
|
||||||
|
(m) => m.FirePageModule
|
||||||
|
)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'portfolio/report',
|
path: 'portfolio/report',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
|
@ -78,10 +78,19 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="balance">
|
<ng-container matColumnDef="balance">
|
||||||
<th *matHeaderCellDef class="px-1 text-right" i18n mat-header-cell>
|
<th
|
||||||
|
*matHeaderCellDef
|
||||||
|
class="d-none d-lg-table-cell px-1 text-right"
|
||||||
|
i18n
|
||||||
|
mat-header-cell
|
||||||
|
>
|
||||||
Cash Balance
|
Cash Balance
|
||||||
</th>
|
</th>
|
||||||
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
<td
|
||||||
|
*matCellDef="let element"
|
||||||
|
class="d-none d-lg-table-cell px-1 text-right"
|
||||||
|
mat-cell
|
||||||
|
>
|
||||||
<gf-value
|
<gf-value
|
||||||
class="d-inline-block justify-content-end"
|
class="d-inline-block justify-content-end"
|
||||||
[isCurrency]="true"
|
[isCurrency]="true"
|
||||||
@ -89,7 +98,11 @@
|
|||||||
[value]="element.balance"
|
[value]="element.balance"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
</td>
|
</td>
|
||||||
<td *matFooterCellDef class="px-1 text-right" mat-footer-cell>
|
<td
|
||||||
|
*matFooterCellDef
|
||||||
|
class="d-none d-lg-table-cell px-1 text-right"
|
||||||
|
mat-footer-cell
|
||||||
|
>
|
||||||
<gf-value
|
<gf-value
|
||||||
class="d-inline-block justify-content-end"
|
class="d-inline-block justify-content-end"
|
||||||
[isCurrency]="true"
|
[isCurrency]="true"
|
||||||
@ -100,10 +113,19 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="value">
|
<ng-container matColumnDef="value">
|
||||||
<th *matHeaderCellDef class="px-1 text-right" i18n mat-header-cell>
|
<th
|
||||||
|
*matHeaderCellDef
|
||||||
|
class="d-none d-lg-table-cell px-1 text-right"
|
||||||
|
i18n
|
||||||
|
mat-header-cell
|
||||||
|
>
|
||||||
Value
|
Value
|
||||||
</th>
|
</th>
|
||||||
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
<td
|
||||||
|
*matCellDef="let element"
|
||||||
|
class="d-none d-lg-table-cell px-1 text-right"
|
||||||
|
mat-cell
|
||||||
|
>
|
||||||
<gf-value
|
<gf-value
|
||||||
class="d-inline-block justify-content-end"
|
class="d-inline-block justify-content-end"
|
||||||
[isCurrency]="true"
|
[isCurrency]="true"
|
||||||
@ -111,7 +133,46 @@
|
|||||||
[value]="element.value"
|
[value]="element.value"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
</td>
|
</td>
|
||||||
<td *matFooterCellDef class="px-1 text-right" mat-footer-cell>
|
<td
|
||||||
|
*matFooterCellDef
|
||||||
|
class="d-none d-lg-table-cell px-1 text-right"
|
||||||
|
mat-footer-cell
|
||||||
|
>
|
||||||
|
<gf-value
|
||||||
|
class="d-inline-block justify-content-end"
|
||||||
|
[isCurrency]="true"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="totalValueInBaseCurrency"
|
||||||
|
></gf-value>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="valueInBaseCurrency">
|
||||||
|
<th
|
||||||
|
*matHeaderCellDef
|
||||||
|
class="d-lg-none d-xl-none px-1 text-right"
|
||||||
|
i18n
|
||||||
|
mat-header-cell
|
||||||
|
>
|
||||||
|
Value
|
||||||
|
</th>
|
||||||
|
<td
|
||||||
|
*matCellDef="let element"
|
||||||
|
class="d-lg-none d-xl-none px-1 text-right"
|
||||||
|
mat-cell
|
||||||
|
>
|
||||||
|
<gf-value
|
||||||
|
class="d-inline-block justify-content-end"
|
||||||
|
[isCurrency]="true"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="element.valueInBaseCurrency"
|
||||||
|
></gf-value>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
*matFooterCellDef
|
||||||
|
class="d-lg-none d-xl-none px-1 text-right"
|
||||||
|
mat-footer-cell
|
||||||
|
>
|
||||||
<gf-value
|
<gf-value
|
||||||
class="d-inline-block justify-content-end"
|
class="d-inline-block justify-content-end"
|
||||||
[isCurrency]="true"
|
[isCurrency]="true"
|
||||||
|
@ -50,7 +50,8 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
'transactions',
|
'transactions',
|
||||||
'balance',
|
'balance',
|
||||||
'value',
|
'value',
|
||||||
'currency'
|
'currency',
|
||||||
|
'valueInBaseCurrency'
|
||||||
];
|
];
|
||||||
|
|
||||||
if (this.showActions) {
|
if (this.showActions) {
|
||||||
|
@ -8,8 +8,11 @@ import {
|
|||||||
Output
|
Output
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
|
import {
|
||||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
DATE_FORMAT,
|
||||||
|
getDateFormatString,
|
||||||
|
getLocale
|
||||||
|
} from '@ghostfolio/common/helper';
|
||||||
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
|
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
|
||||||
import { DataSource, MarketData } from '@prisma/client';
|
import { DataSource, MarketData } from '@prisma/client';
|
||||||
import {
|
import {
|
||||||
@ -35,13 +38,14 @@ import { MarketDataDetailDialog } from './market-data-detail-dialog/market-data-
|
|||||||
export class AdminMarketDataDetailComponent implements OnChanges, OnInit {
|
export class AdminMarketDataDetailComponent implements OnChanges, OnInit {
|
||||||
@Input() dataSource: DataSource;
|
@Input() dataSource: DataSource;
|
||||||
@Input() dateOfFirstActivity: string;
|
@Input() dateOfFirstActivity: string;
|
||||||
|
@Input() locale = getLocale();
|
||||||
@Input() marketData: MarketData[];
|
@Input() marketData: MarketData[];
|
||||||
@Input() symbol: string;
|
@Input() symbol: string;
|
||||||
|
|
||||||
@Output() marketDataChanged = new EventEmitter<boolean>();
|
@Output() marketDataChanged = new EventEmitter<boolean>();
|
||||||
|
|
||||||
public days = Array(31);
|
public days = Array(31);
|
||||||
public defaultDateFormat = DEFAULT_DATE_FORMAT;
|
public defaultDateFormat: string;
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
public historicalDataItems: LineChartItem[];
|
public historicalDataItems: LineChartItem[];
|
||||||
public marketDataByMonth: {
|
public marketDataByMonth: {
|
||||||
@ -62,6 +66,8 @@ export class AdminMarketDataDetailComponent implements OnChanges, OnInit {
|
|||||||
public ngOnInit() {}
|
public ngOnInit() {}
|
||||||
|
|
||||||
public ngOnChanges() {
|
public ngOnChanges() {
|
||||||
|
this.defaultDateFormat = getDateFormatString(this.locale);
|
||||||
|
|
||||||
this.historicalDataItems = this.marketData.map((marketDataItem) => {
|
this.historicalDataItems = this.marketData.map((marketDataItem) => {
|
||||||
return {
|
return {
|
||||||
date: format(marketDataItem.date, DATE_FORMAT),
|
date: format(marketDataItem.date, DATE_FORMAT),
|
||||||
|
@ -7,8 +7,9 @@ import {
|
|||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
import { getDateFormatString } from '@ghostfolio/common/helper';
|
||||||
|
import { 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';
|
||||||
import { DataSource, MarketData } from '@prisma/client';
|
import { DataSource, MarketData } from '@prisma/client';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
@ -23,9 +24,10 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
export class AdminMarketDataComponent implements OnDestroy, OnInit {
|
export class AdminMarketDataComponent implements OnDestroy, OnInit {
|
||||||
public currentDataSource: DataSource;
|
public currentDataSource: DataSource;
|
||||||
public currentSymbol: string;
|
public currentSymbol: string;
|
||||||
public defaultDateFormat = DEFAULT_DATE_FORMAT;
|
public defaultDateFormat: string;
|
||||||
public marketData: AdminMarketDataItem[] = [];
|
public marketData: AdminMarketDataItem[] = [];
|
||||||
public marketDataDetails: MarketData[] = [];
|
public marketDataDetails: MarketData[] = [];
|
||||||
|
public user: User;
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
@ -35,8 +37,21 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
|
|||||||
public constructor(
|
public constructor(
|
||||||
private adminService: AdminService,
|
private adminService: AdminService,
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private dataService: DataService
|
private dataService: DataService,
|
||||||
) {}
|
private userService: UserService
|
||||||
|
) {
|
||||||
|
this.userService.stateChanged
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((state) => {
|
||||||
|
if (state?.user) {
|
||||||
|
this.user = state.user;
|
||||||
|
|
||||||
|
this.defaultDateFormat = getDateFormatString(
|
||||||
|
this.user.settings.locale
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the controller
|
* Initializes the controller
|
||||||
|
@ -65,6 +65,7 @@
|
|||||||
<gf-admin-market-data-detail
|
<gf-admin-market-data-detail
|
||||||
[dataSource]="item.dataSource"
|
[dataSource]="item.dataSource"
|
||||||
[dateOfFirstActivity]="item.date"
|
[dateOfFirstActivity]="item.date"
|
||||||
|
[locale]="user?.settings?.locale"
|
||||||
[marketData]="marketDataDetails"
|
[marketData]="marketDataDetails"
|
||||||
[symbol]="item.symbol"
|
[symbol]="item.symbol"
|
||||||
(marketDataChanged)="onMarketDataChanged($event)"
|
(marketDataChanged)="onMarketDataChanged($event)"
|
||||||
|
@ -5,7 +5,6 @@ import { CacheService } from '@ghostfolio/client/services/cache.service';
|
|||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
import {
|
import {
|
||||||
DEFAULT_DATE_FORMAT,
|
|
||||||
PROPERTY_COUPONS,
|
PROPERTY_COUPONS,
|
||||||
PROPERTY_CURRENCIES,
|
PROPERTY_CURRENCIES,
|
||||||
PROPERTY_IS_READ_ONLY_MODE,
|
PROPERTY_IS_READ_ONLY_MODE,
|
||||||
@ -35,7 +34,6 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
|||||||
public customCurrencies: string[];
|
public customCurrencies: string[];
|
||||||
public dataGatheringInProgress: boolean;
|
public dataGatheringInProgress: boolean;
|
||||||
public dataGatheringProgress: number;
|
public dataGatheringProgress: number;
|
||||||
public defaultDateFormat = DEFAULT_DATE_FORMAT;
|
|
||||||
public exchangeRates: { label1: string; label2: string; value: number }[];
|
public exchangeRates: { label1: string; label2: string; value: number }[];
|
||||||
public hasPermissionForSubscription: boolean;
|
public hasPermissionForSubscription: boolean;
|
||||||
public hasPermissionForSystemMessage: boolean;
|
public hasPermissionForSystemMessage: boolean;
|
||||||
|
@ -180,6 +180,8 @@
|
|||||||
[value]="couponDuration"
|
[value]="couponDuration"
|
||||||
(selectionChange)="onChangeCouponDuration($event.value)"
|
(selectionChange)="onChangeCouponDuration($event.value)"
|
||||||
>
|
>
|
||||||
|
<mat-option value="7 days">7 Days</mat-option>
|
||||||
|
<mat-option value="14 days">14 Days</mat-option>
|
||||||
<mat-option value="30 days">30 Days</mat-option>
|
<mat-option value="30 days">30 Days</mat-option>
|
||||||
<mat-option value="1 year">1 Year</mat-option>
|
<mat-option value="1 year">1 Year</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<ng-container *ngIf="data.hasPermissionToUseSocialLogin">
|
<ng-container *ngIf="data.hasPermissionToUseSocialLogin">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<a color="accent" href="/api/auth/google" mat-flat-button
|
<a color="accent" href="/api/v1/auth/google" mat-flat-button
|
||||||
><ion-icon class="mr-1" name="logo-google"></ion-icon
|
><ion-icon class="mr-1" name="logo-google"></ion-icon
|
||||||
><span i18n>Sign in with Google</span></a
|
><span i18n>Sign in with Google</span></a
|
||||||
>
|
>
|
||||||
|
@ -7,6 +7,10 @@ import {
|
|||||||
OnInit,
|
OnInit,
|
||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
import {
|
||||||
|
getNumberFormatDecimal,
|
||||||
|
getNumberFormatGroup
|
||||||
|
} from '@ghostfolio/common/helper';
|
||||||
import {
|
import {
|
||||||
PortfolioPerformance,
|
PortfolioPerformance,
|
||||||
ResponseError
|
ResponseError
|
||||||
@ -50,13 +54,14 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
|
|||||||
this.unit = this.baseCurrency;
|
this.unit = this.baseCurrency;
|
||||||
|
|
||||||
new CountUp('value', this.performance?.currentValue, {
|
new CountUp('value', this.performance?.currentValue, {
|
||||||
|
decimal: getNumberFormatDecimal(this.locale),
|
||||||
decimalPlaces:
|
decimalPlaces:
|
||||||
this.deviceType === 'mobile' &&
|
this.deviceType === 'mobile' &&
|
||||||
this.performance?.currentValue >= 100000
|
this.performance?.currentValue >= 100000
|
||||||
? 0
|
? 0
|
||||||
: 2,
|
: 2,
|
||||||
duration: 1,
|
duration: 1,
|
||||||
separator: `'`
|
separator: getNumberFormatGroup(this.locale)
|
||||||
}).start();
|
}).start();
|
||||||
} else if (this.performance?.currentValue === null) {
|
} else if (this.performance?.currentValue === null) {
|
||||||
this.unit = '%';
|
this.unit = '%';
|
||||||
@ -65,9 +70,10 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
|
|||||||
'value',
|
'value',
|
||||||
this.performance?.currentNetPerformancePercent * 100,
|
this.performance?.currentNetPerformancePercent * 100,
|
||||||
{
|
{
|
||||||
|
decimal: getNumberFormatDecimal(this.locale),
|
||||||
decimalPlaces: 2,
|
decimalPlaces: 2,
|
||||||
duration: 0.75,
|
duration: 1,
|
||||||
separator: `'`
|
separator: getNumberFormatGroup(this.locale)
|
||||||
}
|
}
|
||||||
).start();
|
).start();
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
<gf-line-chart
|
<gf-line-chart
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
benchmarkLabel="Buy Price"
|
benchmarkLabel="Average Unit Price"
|
||||||
[benchmarkDataItems]="benchmarkDataItems"
|
[benchmarkDataItems]="benchmarkDataItems"
|
||||||
[historicalDataItems]="historicalDataItems"
|
[historicalDataItems]="historicalDataItems"
|
||||||
[showGradient]="true"
|
[showGradient]="true"
|
||||||
@ -53,7 +53,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
<gf-value
|
<gf-value
|
||||||
label="Ø Buy Price"
|
label="Average Unit Price"
|
||||||
size="medium"
|
size="medium"
|
||||||
[currency]="SymbolProfile?.currency"
|
[currency]="SymbolProfile?.currency"
|
||||||
[locale]="data.locale"
|
[locale]="data.locale"
|
||||||
|
@ -17,12 +17,14 @@ import { DataService } from '@ghostfolio/client/services/data.service';
|
|||||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||||
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
|
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
|
||||||
import { InfoItem } from '@ghostfolio/common/interfaces';
|
import { InfoItem } from '@ghostfolio/common/interfaces';
|
||||||
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { StatusCodes } from 'http-status-codes';
|
import { StatusCodes } from 'http-status-codes';
|
||||||
import { Observable, throwError } from 'rxjs';
|
import { Observable, throwError } from 'rxjs';
|
||||||
import { catchError, tap } from 'rxjs/operators';
|
import { catchError, tap } from 'rxjs/operators';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class HttpResponseInterceptor implements HttpInterceptor {
|
export class HttpResponseInterceptor implements HttpInterceptor {
|
||||||
|
public hasPermissionForSubscription: boolean;
|
||||||
public info: InfoItem;
|
public info: InfoItem;
|
||||||
public snackBarRef: MatSnackBarRef<TextOnlySnackBar>;
|
public snackBarRef: MatSnackBarRef<TextOnlySnackBar>;
|
||||||
|
|
||||||
@ -34,6 +36,11 @@ export class HttpResponseInterceptor implements HttpInterceptor {
|
|||||||
private webAuthnService: WebAuthnService
|
private webAuthnService: WebAuthnService
|
||||||
) {
|
) {
|
||||||
this.info = this.dataService.fetchInfo();
|
this.info = this.dataService.fetchInfo();
|
||||||
|
|
||||||
|
this.hasPermissionForSubscription = hasPermission(
|
||||||
|
this.info?.globalPermissions,
|
||||||
|
permissions.enableSubscription
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public intercept(
|
public intercept(
|
||||||
@ -56,7 +63,7 @@ export class HttpResponseInterceptor implements HttpInterceptor {
|
|||||||
} else {
|
} else {
|
||||||
this.snackBarRef = this.snackBar.open(
|
this.snackBarRef = this.snackBar.open(
|
||||||
'This feature requires a subscription.',
|
'This feature requires a subscription.',
|
||||||
'Upgrade Plan',
|
this.hasPermissionForSubscription ? 'Upgrade Plan' : undefined,
|
||||||
{ duration: 6000 }
|
{ duration: 6000 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -20,9 +20,11 @@ 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 { 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 { DEFAULT_DATE_FORMAT, baseCurrency } from '@ghostfolio/common/config';
|
import { baseCurrency } from '@ghostfolio/common/config';
|
||||||
|
import { getDateFormatString } from '@ghostfolio/common/helper';
|
||||||
import { Access, User } from '@ghostfolio/common/interfaces';
|
import { Access, User } from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
|
import { uniq } from 'lodash';
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { StripeService } from 'ngx-stripe';
|
import { StripeService } from 'ngx-stripe';
|
||||||
import { EMPTY, Subject } from 'rxjs';
|
import { EMPTY, Subject } from 'rxjs';
|
||||||
@ -45,13 +47,14 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
|||||||
public coupon: number;
|
public coupon: number;
|
||||||
public couponId: string;
|
public couponId: string;
|
||||||
public currencies: string[] = [];
|
public currencies: string[] = [];
|
||||||
public defaultDateFormat = DEFAULT_DATE_FORMAT;
|
public defaultDateFormat: string;
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
public hasPermissionForSubscription: boolean;
|
public hasPermissionForSubscription: boolean;
|
||||||
public hasPermissionToCreateAccess: boolean;
|
public hasPermissionToCreateAccess: boolean;
|
||||||
public hasPermissionToDeleteAccess: boolean;
|
public hasPermissionToDeleteAccess: boolean;
|
||||||
public hasPermissionToUpdateViewMode: boolean;
|
public hasPermissionToUpdateViewMode: boolean;
|
||||||
public hasPermissionToUpdateUserSettings: boolean;
|
public hasPermissionToUpdateUserSettings: boolean;
|
||||||
|
public locales = ['de', 'de-CH', 'en-GB', 'en-US'];
|
||||||
public price: number;
|
public price: number;
|
||||||
public priceId: string;
|
public priceId: string;
|
||||||
public snackBarRef: MatSnackBarRef<TextOnlySnackBar>;
|
public snackBarRef: MatSnackBarRef<TextOnlySnackBar>;
|
||||||
@ -101,6 +104,10 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
|||||||
if (state?.user) {
|
if (state?.user) {
|
||||||
this.user = state.user;
|
this.user = state.user;
|
||||||
|
|
||||||
|
this.defaultDateFormat = getDateFormatString(
|
||||||
|
this.user.settings.locale
|
||||||
|
);
|
||||||
|
|
||||||
this.hasPermissionToCreateAccess = hasPermission(
|
this.hasPermissionToCreateAccess = hasPermission(
|
||||||
this.user.permissions,
|
this.user.permissions,
|
||||||
permissions.createAccess
|
permissions.createAccess
|
||||||
@ -121,6 +128,9 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
|||||||
permissions.updateViewMode
|
permissions.updateViewMode
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.locales.push(this.user.settings.locale);
|
||||||
|
this.locales = uniq(this.locales.sort());
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -143,6 +153,24 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
|||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onChangeUserSetting(aKey: string, aValue: string) {
|
||||||
|
this.dataService
|
||||||
|
.putUserSetting({ [aKey]: aValue })
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.userService.remove();
|
||||||
|
|
||||||
|
this.userService
|
||||||
|
.get()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((user) => {
|
||||||
|
this.user = user;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public onChangeUserSettings(aKey: string, aValue: string) {
|
public onChangeUserSettings(aKey: string, aValue: string) {
|
||||||
const settings = { ...this.user.settings, [aKey]: aValue };
|
const settings = { ...this.user.settings, [aKey]: aValue };
|
||||||
|
|
||||||
|
@ -27,9 +27,8 @@
|
|||||||
Valid until {{ user?.subscription?.expiresAt | date:
|
Valid until {{ user?.subscription?.expiresAt | date:
|
||||||
defaultDateFormat }}
|
defaultDateFormat }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div *ngIf="user?.subscription?.type === 'Basic'">
|
||||||
*ngIf="hasPermissionForSubscription && user?.subscription?.type === 'Basic'"
|
<ng-container *ngIf="hasPermissionForSubscription">
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
color="primary"
|
color="primary"
|
||||||
i18n
|
i18n
|
||||||
@ -39,14 +38,17 @@
|
|||||||
Upgrade
|
Upgrade
|
||||||
</button>
|
</button>
|
||||||
<div *ngIf="price" class="mt-1">
|
<div *ngIf="price" class="mt-1">
|
||||||
{{ baseCurrency }}
|
|
||||||
<ng-container *ngIf="coupon"
|
<ng-container *ngIf="coupon"
|
||||||
>{{ price - coupon | number : '1.2-2' }}
|
><del class="text-muted"
|
||||||
<del>{{ price }}</del>
|
>{{ baseCurrency }} {{ price }}</del
|
||||||
</ng-container>
|
> {{ baseCurrency }} {{ price - coupon
|
||||||
<ng-container *ngIf="!coupon">{{ price }}</ng-container>
|
}}</ng-container
|
||||||
<span i18n> per year</span>
|
>
|
||||||
|
<ng-container *ngIf="!coupon"
|
||||||
|
>{{ baseCurrency }} {{ price }}</ng-container
|
||||||
|
> <span i18n>per year</span>
|
||||||
</div>
|
</div>
|
||||||
|
</ng-container>
|
||||||
<a
|
<a
|
||||||
*ngIf="!user?.subscription?.expiresAt"
|
*ngIf="!user?.subscription?.expiresAt"
|
||||||
class="mr-2 my-2"
|
class="mr-2 my-2"
|
||||||
@ -109,6 +111,31 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="align-items-center d-flex mb-2">
|
||||||
|
<div class="pr-1 w-50">
|
||||||
|
<div i18n>Locale</div>
|
||||||
|
<div class="hint-text text-muted" i18n>
|
||||||
|
Date and number format
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pl-1 w-50">
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-select
|
||||||
|
name="locale"
|
||||||
|
[disabled]="!hasPermissionToUpdateUserSettings"
|
||||||
|
[value]="user.settings.locale"
|
||||||
|
(selectionChange)="onChangeUserSetting('locale', $event.value)"
|
||||||
|
>
|
||||||
|
<mat-option [value]="null"></mat-option>
|
||||||
|
<mat-option
|
||||||
|
*ngFor="let locale of locales"
|
||||||
|
[value]="locale"
|
||||||
|
>{{ locale }}</mat-option
|
||||||
|
>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<div class="align-items-center d-flex pr-1 pt-1 w-50" i18n>
|
<div class="align-items-center d-flex pr-1 pt-1 w-50" i18n>
|
||||||
View Mode
|
View Mode
|
||||||
|
@ -10,6 +10,7 @@ import { MatSelectModule } from '@angular/material/select';
|
|||||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { GfPortfolioAccessTableModule } from '@ghostfolio/client/components/access-table/access-table.module';
|
import { GfPortfolioAccessTableModule } from '@ghostfolio/client/components/access-table/access-table.module';
|
||||||
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
|
|
||||||
import { AccountPageRoutingModule } from './account-page-routing.module';
|
import { AccountPageRoutingModule } from './account-page-routing.module';
|
||||||
import { AccountPageComponent } from './account-page.component';
|
import { AccountPageComponent } from './account-page.component';
|
||||||
@ -24,6 +25,7 @@ import { GfCreateOrUpdateAccessDialogModule } from './create-or-update-access-di
|
|||||||
FormsModule,
|
FormsModule,
|
||||||
GfCreateOrUpdateAccessDialogModule,
|
GfCreateOrUpdateAccessDialogModule,
|
||||||
GfPortfolioAccessTableModule,
|
GfPortfolioAccessTableModule,
|
||||||
|
GfValueModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
MatDialogModule,
|
MatDialogModule,
|
||||||
|
@ -29,6 +29,12 @@
|
|||||||
color: rgba(var(--palette-primary-500), 1);
|
color: rgba(var(--palette-primary-500), 1);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mat-tab-link {
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-xs-12 col-md-4 mb-3">
|
||||||
|
<mat-card class="d-flex flex-column h-100">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h4 i18n>Bonds</h4>
|
||||||
|
<p class="m-0">
|
||||||
|
Manage your investment in bonds and other assets with fixed
|
||||||
|
income.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
<div class="col-xs-12 col-md-4 mb-3">
|
<div class="col-xs-12 col-md-4 mb-3">
|
||||||
<mat-card class="d-flex flex-column h-100">
|
<mat-card class="d-flex flex-column h-100">
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
@ -64,6 +75,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-xs-12 col-md-4 mb-3">
|
||||||
|
<mat-card class="d-flex flex-column h-100">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h4 class="align-items-center d-flex" i18n>Emergency Fund</h4>
|
||||||
|
<p class="m-0">
|
||||||
|
Define your emergency fund you are comfortable with for
|
||||||
|
difficult times.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
<div class="col-xs-12 col-md-4 mb-3">
|
<div class="col-xs-12 col-md-4 mb-3">
|
||||||
<mat-card class="d-flex flex-column h-100">
|
<mat-card class="d-flex flex-column h-100">
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
|
@ -30,6 +30,12 @@
|
|||||||
color: rgba(var(--palette-primary-500), 1);
|
color: rgba(var(--palette-primary-500), 1);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mat-tab-link {
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,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 { ToggleOption } from '@ghostfolio/common/types';
|
import { Market, ToggleOption } from '@ghostfolio/common/types';
|
||||||
import { Account, AssetClass, DataSource } from '@prisma/client';
|
import { Account, AssetClass, DataSource } from '@prisma/client';
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { Subject, Subscription } from 'rxjs';
|
import { Subject, Subscription } from 'rxjs';
|
||||||
@ -42,6 +42,9 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
public hasImpersonationId: boolean;
|
public hasImpersonationId: boolean;
|
||||||
public hasPermissionToCreateOrder: boolean;
|
public hasPermissionToCreateOrder: boolean;
|
||||||
|
public markets: {
|
||||||
|
[key in Market]: { name: string; value: number };
|
||||||
|
};
|
||||||
public period = 'current';
|
public period = 'current';
|
||||||
public periodOptions: ToggleOption[] = [
|
public periodOptions: ToggleOption[] = [
|
||||||
{ label: 'Initial', value: 'original' },
|
{ label: 'Initial', value: 'original' },
|
||||||
@ -160,6 +163,20 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
value: 0
|
value: 0
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
this.markets = {
|
||||||
|
developedMarkets: {
|
||||||
|
name: 'developedMarkets',
|
||||||
|
value: 0
|
||||||
|
},
|
||||||
|
emergingMarkets: {
|
||||||
|
name: 'emergingMarkets',
|
||||||
|
value: 0
|
||||||
|
},
|
||||||
|
otherMarkets: {
|
||||||
|
name: 'otherMarkets',
|
||||||
|
value: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
this.positions = {};
|
this.positions = {};
|
||||||
this.positionsArray = [];
|
this.positionsArray = [];
|
||||||
this.sectors = {
|
this.sectors = {
|
||||||
@ -219,6 +236,16 @@ 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) {
|
||||||
|
this.markets.developedMarkets.value +=
|
||||||
|
position.markets.developedMarkets *
|
||||||
|
(aPeriod === 'original' ? position.investment : position.value);
|
||||||
|
this.markets.emergingMarkets.value +=
|
||||||
|
position.markets.emergingMarkets *
|
||||||
|
(aPeriod === 'original' ? position.investment : position.value);
|
||||||
|
this.markets.otherMarkets.value +=
|
||||||
|
position.markets.otherMarkets *
|
||||||
|
(aPeriod === 'original' ? position.investment : position.value);
|
||||||
|
|
||||||
for (const country of position.countries) {
|
for (const country of position.countries) {
|
||||||
const { code, continent, name, weight } = country;
|
const { code, continent, name, weight } = country;
|
||||||
|
|
||||||
@ -294,6 +321,18 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const marketsTotal =
|
||||||
|
this.markets.developedMarkets.value +
|
||||||
|
this.markets.emergingMarkets.value +
|
||||||
|
this.markets.otherMarkets.value;
|
||||||
|
|
||||||
|
this.markets.developedMarkets.value =
|
||||||
|
this.markets.developedMarkets.value / marketsTotal;
|
||||||
|
this.markets.emergingMarkets.value =
|
||||||
|
this.markets.emergingMarkets.value / marketsTotal;
|
||||||
|
this.markets.otherMarkets.value =
|
||||||
|
this.markets.otherMarkets.value / marketsTotal;
|
||||||
}
|
}
|
||||||
|
|
||||||
public onChangePeriod(aValue: string) {
|
public onChangePeriod(aValue: string) {
|
||||||
|
@ -190,6 +190,32 @@
|
|||||||
[countries]="countries"
|
[countries]="countries"
|
||||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||||
></gf-world-map-chart>
|
></gf-world-map-chart>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
|
<gf-value
|
||||||
|
label="Developed Markets"
|
||||||
|
size="large"
|
||||||
|
[isPercent]="true"
|
||||||
|
[value]="markets?.developedMarkets?.value"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
|
<gf-value
|
||||||
|
label="Emerging Markets"
|
||||||
|
size="large"
|
||||||
|
[isPercent]="true"
|
||||||
|
[value]="markets?.emergingMarkets?.value"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
|
<gf-value
|
||||||
|
label="Other Markets"
|
||||||
|
size="large"
|
||||||
|
[isPercent]="true"
|
||||||
|
[value]="markets?.otherMarkets?.value"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,6 +5,7 @@ import { GfPositionsTableModule } from '@ghostfolio/client/components/positions-
|
|||||||
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
||||||
import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module';
|
import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module';
|
||||||
import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module';
|
import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module';
|
||||||
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
|
|
||||||
import { AllocationsPageRoutingModule } from './allocations-page-routing.module';
|
import { AllocationsPageRoutingModule } from './allocations-page-routing.module';
|
||||||
import { AllocationsPageComponent } from './allocations-page.component';
|
import { AllocationsPageComponent } from './allocations-page.component';
|
||||||
@ -19,6 +20,7 @@ import { AllocationsPageComponent } from './allocations-page.component';
|
|||||||
GfPositionsTableModule,
|
GfPositionsTableModule,
|
||||||
GfToggleModule,
|
GfToggleModule,
|
||||||
GfWorldMapChartModule,
|
GfWorldMapChartModule,
|
||||||
|
GfValueModule,
|
||||||
MatCardModule
|
MatCardModule
|
||||||
],
|
],
|
||||||
providers: [],
|
providers: [],
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||||
|
|
||||||
|
import { FirePageComponent } from './fire-page.component';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{ path: '', component: FirePageComponent, canActivate: [AuthGuard] }
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class FirePageRoutingModule {}
|
@ -0,0 +1,86 @@
|
|||||||
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
|
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||||
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
|
import { User } from '@ghostfolio/common/interfaces';
|
||||||
|
import Big from 'big.js';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
host: { class: 'page' },
|
||||||
|
selector: 'gf-fire-page',
|
||||||
|
styleUrls: ['./fire-page.scss'],
|
||||||
|
templateUrl: './fire-page.html'
|
||||||
|
})
|
||||||
|
export class FirePageComponent implements OnDestroy, OnInit {
|
||||||
|
public fireWealth: number;
|
||||||
|
public hasImpersonationId: boolean;
|
||||||
|
public isLoading = false;
|
||||||
|
public user: User;
|
||||||
|
public withdrawalRatePerMonth: number;
|
||||||
|
public withdrawalRatePerYear: number;
|
||||||
|
|
||||||
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
public constructor(
|
||||||
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
|
private dataService: DataService,
|
||||||
|
private impersonationStorageService: ImpersonationStorageService,
|
||||||
|
private userService: UserService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the controller
|
||||||
|
*/
|
||||||
|
public ngOnInit() {
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
|
this.impersonationStorageService
|
||||||
|
.onChangeHasImpersonation()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((aId) => {
|
||||||
|
this.hasImpersonationId = !!aId;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.dataService
|
||||||
|
.fetchPortfolioSummary()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(({ cash, currentValue }) => {
|
||||||
|
if (cash === null || currentValue === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fireWealth = new Big(currentValue).plus(cash).toNumber();
|
||||||
|
this.withdrawalRatePerYear = new Big(this.fireWealth)
|
||||||
|
.mul(4)
|
||||||
|
.div(100)
|
||||||
|
.toNumber();
|
||||||
|
this.withdrawalRatePerMonth = new Big(this.withdrawalRatePerYear)
|
||||||
|
.div(12)
|
||||||
|
.toNumber();
|
||||||
|
|
||||||
|
this.isLoading = false;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.userService.stateChanged
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((state) => {
|
||||||
|
if (state?.user) {
|
||||||
|
this.user = state.user;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
|
}
|
||||||
|
}
|
53
apps/client/src/app/pages/portfolio/fire/fire-page.html
Normal file
53
apps/client/src/app/pages/portfolio/fire/fire-page.html
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg">
|
||||||
|
<h3 class="d-flex justify-content-center mb-3" i18n>FIRE</h3>
|
||||||
|
<div class="mb-4">
|
||||||
|
<h4 i18n>4% Rule</h4>
|
||||||
|
<div *ngIf="isLoading">
|
||||||
|
<ngx-skeleton-loader
|
||||||
|
animation="pulse"
|
||||||
|
class="my-1"
|
||||||
|
[theme]="{
|
||||||
|
height: '1rem',
|
||||||
|
width: '100%'
|
||||||
|
}"
|
||||||
|
></ngx-skeleton-loader>
|
||||||
|
<ngx-skeleton-loader
|
||||||
|
animation="pulse"
|
||||||
|
[theme]="{
|
||||||
|
height: '1rem',
|
||||||
|
width: '10rem'
|
||||||
|
}"
|
||||||
|
></ngx-skeleton-loader>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="!isLoading">
|
||||||
|
If you retire today, you would be able to withdraw
|
||||||
|
<span class="font-weight-bold"
|
||||||
|
><gf-value
|
||||||
|
class="d-inline-block"
|
||||||
|
[currency]="user?.settings?.baseCurrency"
|
||||||
|
[value]="withdrawalRatePerYear"
|
||||||
|
></gf-value>
|
||||||
|
per year</span
|
||||||
|
>
|
||||||
|
or
|
||||||
|
<span class="font-weight-bold"
|
||||||
|
><gf-value
|
||||||
|
class="d-inline-block"
|
||||||
|
[currency]="user?.settings?.baseCurrency"
|
||||||
|
[value]="withdrawalRatePerMonth"
|
||||||
|
></gf-value>
|
||||||
|
per month</span
|
||||||
|
>, based on your net worth of
|
||||||
|
<gf-value
|
||||||
|
class="d-inline-block"
|
||||||
|
[currency]="user?.settings?.baseCurrency"
|
||||||
|
[value]="fireWealth"
|
||||||
|
></gf-value>
|
||||||
|
(excluding emergency fund) and a withdrawal rate of 4%.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
19
apps/client/src/app/pages/portfolio/fire/fire-page.module.ts
Normal file
19
apps/client/src/app/pages/portfolio/fire/fire-page.module.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
|
import { FirePageRoutingModule } from './fire-page-routing.module';
|
||||||
|
import { FirePageComponent } from './fire-page.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [FirePageComponent],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FirePageRoutingModule,
|
||||||
|
GfValueModule,
|
||||||
|
NgxSkeletonLoaderModule
|
||||||
|
],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
})
|
||||||
|
export class FirePageModule {}
|
3
apps/client/src/app/pages/portfolio/fire/fire-page.scss
Normal file
3
apps/client/src/app/pages/portfolio/fire/fire-page.scss
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
@ -25,7 +25,7 @@
|
|||||||
<h4 class="align-items-center d-flex">
|
<h4 class="align-items-center d-flex">
|
||||||
<span i18n>Allocations</span>
|
<span i18n>Allocations</span>
|
||||||
<ion-icon
|
<ion-icon
|
||||||
*ngIf="hasPermissionForSubscription"
|
*ngIf="user?.subscription?.type === 'Basic'"
|
||||||
class="ml-1 text-muted"
|
class="ml-1 text-muted"
|
||||||
name="diamond-outline"
|
name="diamond-outline"
|
||||||
></ion-icon>
|
></ion-icon>
|
||||||
@ -38,7 +38,6 @@
|
|||||||
<a
|
<a
|
||||||
color="primary"
|
color="primary"
|
||||||
mat-button
|
mat-button
|
||||||
[disabled]="hasPermissionForSubscription && user?.settings?.viewMode !== 'DEFAULT'"
|
|
||||||
[routerLink]="['/portfolio', 'allocations']"
|
[routerLink]="['/portfolio', 'allocations']"
|
||||||
>
|
>
|
||||||
<span i18n>Open Allocations</span>
|
<span i18n>Open Allocations</span>
|
||||||
@ -52,7 +51,7 @@
|
|||||||
<h4 class="align-items-center d-flex">
|
<h4 class="align-items-center d-flex">
|
||||||
<span i18n>Analysis</span>
|
<span i18n>Analysis</span>
|
||||||
<ion-icon
|
<ion-icon
|
||||||
*ngIf="hasPermissionForSubscription"
|
*ngIf="user?.subscription?.type === 'Basic'"
|
||||||
class="ml-1 text-muted"
|
class="ml-1 text-muted"
|
||||||
name="diamond-outline"
|
name="diamond-outline"
|
||||||
></ion-icon>
|
></ion-icon>
|
||||||
@ -65,7 +64,6 @@
|
|||||||
<a
|
<a
|
||||||
color="primary"
|
color="primary"
|
||||||
mat-button
|
mat-button
|
||||||
[disabled]="hasPermissionForSubscription && user?.settings?.viewMode !== 'DEFAULT'"
|
|
||||||
[routerLink]="['/portfolio', 'analysis']"
|
[routerLink]="['/portfolio', 'analysis']"
|
||||||
>
|
>
|
||||||
<span i18n>Open Analysis</span>
|
<span i18n>Open Analysis</span>
|
||||||
@ -79,7 +77,7 @@
|
|||||||
<h4 class="align-items-center d-flex">
|
<h4 class="align-items-center d-flex">
|
||||||
<span i18n>X-ray</span>
|
<span i18n>X-ray</span>
|
||||||
<ion-icon
|
<ion-icon
|
||||||
*ngIf="hasPermissionForSubscription"
|
*ngIf="user?.subscription?.type === 'Basic'"
|
||||||
class="ml-1 text-muted"
|
class="ml-1 text-muted"
|
||||||
name="diamond-outline"
|
name="diamond-outline"
|
||||||
></ion-icon>
|
></ion-icon>
|
||||||
@ -89,17 +87,34 @@
|
|||||||
risks in your portfolio.
|
risks in your portfolio.
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 text-right">
|
<div class="mt-2 text-right">
|
||||||
<a
|
<a color="primary" mat-button [routerLink]="['/portfolio', 'report']">
|
||||||
color="primary"
|
|
||||||
mat-button
|
|
||||||
[disabled]="hasPermissionForSubscription && user?.settings?.viewMode !== 'DEFAULT'"
|
|
||||||
[routerLink]="['/portfolio', 'report']"
|
|
||||||
>
|
|
||||||
<span i18n>Open X-ray</span>
|
<span i18n>Open X-ray</span>
|
||||||
<ion-icon class="ml-1" name="arrow-forward-outline"></ion-icon>
|
<ion-icon class="ml-1" name="arrow-forward-outline"></ion-icon>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-xs-12 col-md-6 mb-3">
|
||||||
|
<mat-card class="d-flex flex-column h-100">
|
||||||
|
<h4 class="align-items-center d-flex">
|
||||||
|
<span i18n>FIRE</span>
|
||||||
|
<ion-icon
|
||||||
|
*ngIf="user?.subscription?.type === 'Basic'"
|
||||||
|
class="ml-1 text-muted"
|
||||||
|
name="diamond-outline"
|
||||||
|
></ion-icon>
|
||||||
|
</h4>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
Ghostfolio FIRE calculates metrics for the
|
||||||
|
<i>Financial Independence, Retire Early</i> lifestyle.
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-right">
|
||||||
|
<a color="primary" mat-button [routerLink]="['/portfolio', 'fire']">
|
||||||
|
<span i18n>Open FIRE</span>
|
||||||
|
<ion-icon class="ml-1" name="arrow-forward-outline"></ion-icon>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -27,12 +27,12 @@ export class ImportTransactionDialog implements OnDestroy {
|
|||||||
|
|
||||||
public ngOnInit() {
|
public ngOnInit() {
|
||||||
for (const message of this.data.messages) {
|
for (const message of this.data.messages) {
|
||||||
if (message.includes('orders.')) {
|
if (message.includes('activities.')) {
|
||||||
let [index] = message.split(' ');
|
let [index] = message.split(' ');
|
||||||
index = index.replace('orders.', '');
|
index = index.replace('activities.', '');
|
||||||
[index] = index.split('.');
|
[index] = index.split('.');
|
||||||
|
|
||||||
this.details.push(this.data.orders[index]);
|
this.details.push(this.data.activities[index]);
|
||||||
} else {
|
} else {
|
||||||
this.details.push('');
|
this.details.push('');
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
export interface ImportTransactionDialogParams {
|
export interface ImportTransactionDialogParams {
|
||||||
|
activities: any[];
|
||||||
deviceType: string;
|
deviceType: string;
|
||||||
messages: string[];
|
messages: string[];
|
||||||
orders: any[];
|
|
||||||
}
|
}
|
||||||
|
@ -185,19 +185,31 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
|||||||
if (file.name.endsWith('.json')) {
|
if (file.name.endsWith('.json')) {
|
||||||
const content = JSON.parse(fileContent);
|
const content = JSON.parse(fileContent);
|
||||||
|
|
||||||
if (!isArray(content.orders)) {
|
if (!isArray(content.activities)) {
|
||||||
|
if (isArray(content.orders)) {
|
||||||
|
this.handleImportError({
|
||||||
|
activities: [],
|
||||||
|
error: {
|
||||||
|
error: {
|
||||||
|
message: [`orders needs to be renamed to activities`]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
throw new Error();
|
throw new Error();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.importTransactionsService.importJson({
|
await this.importTransactionsService.importJson({
|
||||||
content: content.orders
|
content: content.activities
|
||||||
});
|
});
|
||||||
|
|
||||||
this.handleImportSuccess();
|
this.handleImportSuccess();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
this.handleImportError({ error, orders: content.orders });
|
this.handleImportError({ error, activities: content.activities });
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@ -212,10 +224,10 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
this.handleImportError({
|
this.handleImportError({
|
||||||
|
activities: error?.activities ?? [],
|
||||||
error: {
|
error: {
|
||||||
error: { message: error?.error?.message ?? [error?.message] }
|
error: { message: error?.error?.message ?? [error?.message] }
|
||||||
},
|
}
|
||||||
orders: error?.orders ?? []
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,8 +238,8 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
this.handleImportError({
|
this.handleImportError({
|
||||||
error: { error: { message: ['Unexpected format'] } },
|
activities: [],
|
||||||
orders: []
|
error: { error: { message: ['Unexpected format'] } }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -281,12 +293,18 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
|||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleImportError({ error, orders }: { error: any; orders: any[] }) {
|
private handleImportError({
|
||||||
|
activities,
|
||||||
|
error
|
||||||
|
}: {
|
||||||
|
activities: any[];
|
||||||
|
error: any;
|
||||||
|
}) {
|
||||||
this.snackBar.dismiss();
|
this.snackBar.dismiss();
|
||||||
|
|
||||||
this.dialog.open(ImportTransactionDialog, {
|
this.dialog.open(ImportTransactionDialog, {
|
||||||
data: {
|
data: {
|
||||||
orders,
|
activities,
|
||||||
deviceType: this.deviceType,
|
deviceType: this.deviceType,
|
||||||
messages: error?.error?.message
|
messages: error?.error?.message
|
||||||
},
|
},
|
||||||
|
@ -178,16 +178,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<p>Fully managed <strong>Ghostfolio</strong> cloud offering.</p>
|
<p>Fully managed <strong>Ghostfolio</strong> cloud offering.</p>
|
||||||
<p class="h5 text-right" [hidden]="!price">
|
<p class="h5 text-right" [hidden]="!price">
|
||||||
<span class="font-weight-normal"
|
<span class="font-weight-normal">
|
||||||
>{{ baseCurrency }}
|
|
||||||
<ng-container *ngIf="coupon"
|
<ng-container *ngIf="coupon"
|
||||||
><strong>{{ price - coupon | number : '1.2-2' }} </strong>
|
><del class="text-muted"
|
||||||
<del>{{ price }}</del>
|
>{{ baseCurrency }} {{ price }}</del
|
||||||
|
> {{ baseCurrency }} <strong
|
||||||
|
>{{ price - coupon }}</strong
|
||||||
|
>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="!coupon"
|
<ng-container *ngIf="!coupon"
|
||||||
><strong>{{ price }}</strong></ng-container
|
>{{ baseCurrency }} <strong
|
||||||
>
|
>{{ price }}</strong
|
||||||
<span i18n> per year</span></span
|
></ng-container
|
||||||
|
> <span i18n>per year</span></span
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
PortfolioPosition,
|
PortfolioPosition,
|
||||||
PortfolioPublicDetails
|
PortfolioPublicDetails
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
|
import { Market } from '@ghostfolio/common/types';
|
||||||
import { StatusCodes } from 'http-status-codes';
|
import { StatusCodes } from 'http-status-codes';
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { EMPTY, Subject } from 'rxjs';
|
import { EMPTY, Subject } from 'rxjs';
|
||||||
@ -26,6 +27,9 @@ export class PublicPageComponent implements OnInit {
|
|||||||
[code: string]: { name: string; value: number };
|
[code: string]: { name: string; value: number };
|
||||||
};
|
};
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
|
public markets: {
|
||||||
|
[key in Market]: { name: string; value: number };
|
||||||
|
};
|
||||||
public portfolioPublicDetails: PortfolioPublicDetails;
|
public portfolioPublicDetails: PortfolioPublicDetails;
|
||||||
public positions: {
|
public positions: {
|
||||||
[symbol: string]: Pick<PortfolioPosition, 'currency' | 'name' | 'value'>;
|
[symbol: string]: Pick<PortfolioPosition, 'currency' | 'name' | 'value'>;
|
||||||
@ -96,6 +100,20 @@ export class PublicPageComponent implements OnInit {
|
|||||||
value: 0
|
value: 0
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
this.markets = {
|
||||||
|
developedMarkets: {
|
||||||
|
name: 'developedMarkets',
|
||||||
|
value: 0
|
||||||
|
},
|
||||||
|
emergingMarkets: {
|
||||||
|
name: 'emergingMarkets',
|
||||||
|
value: 0
|
||||||
|
},
|
||||||
|
otherMarkets: {
|
||||||
|
name: 'otherMarkets',
|
||||||
|
value: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
this.positions = {};
|
this.positions = {};
|
||||||
this.sectors = {
|
this.sectors = {
|
||||||
[UNKNOWN_KEY]: {
|
[UNKNOWN_KEY]: {
|
||||||
@ -123,6 +141,13 @@ export class PublicPageComponent implements OnInit {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (position.countries.length > 0) {
|
if (position.countries.length > 0) {
|
||||||
|
this.markets.developedMarkets.value +=
|
||||||
|
position.markets.developedMarkets * position.value;
|
||||||
|
this.markets.emergingMarkets.value +=
|
||||||
|
position.markets.emergingMarkets * position.value;
|
||||||
|
this.markets.otherMarkets.value +=
|
||||||
|
position.markets.otherMarkets * position.value;
|
||||||
|
|
||||||
for (const country of position.countries) {
|
for (const country of position.countries) {
|
||||||
const { code, continent, name, weight } = country;
|
const { code, continent, name, weight } = country;
|
||||||
|
|
||||||
@ -176,6 +201,18 @@ export class PublicPageComponent implements OnInit {
|
|||||||
value: position.value
|
value: position.value
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const marketsTotal =
|
||||||
|
this.markets.developedMarkets.value +
|
||||||
|
this.markets.emergingMarkets.value +
|
||||||
|
this.markets.otherMarkets.value;
|
||||||
|
|
||||||
|
this.markets.developedMarkets.value =
|
||||||
|
this.markets.developedMarkets.value / marketsTotal;
|
||||||
|
this.markets.emergingMarkets.value =
|
||||||
|
this.markets.emergingMarkets.value / marketsTotal;
|
||||||
|
this.markets.otherMarkets.value =
|
||||||
|
this.markets.otherMarkets.value / marketsTotal;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
|
@ -79,12 +79,38 @@
|
|||||||
[countries]="countries"
|
[countries]="countries"
|
||||||
[isInPercent]="true"
|
[isInPercent]="true"
|
||||||
></gf-world-map-chart>
|
></gf-world-map-chart>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
|
<gf-value
|
||||||
|
label="Developed Markets"
|
||||||
|
size="large"
|
||||||
|
[isPercent]="true"
|
||||||
|
[value]="markets?.developedMarkets?.value"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
|
<gf-value
|
||||||
|
label="Emerging Markets"
|
||||||
|
size="large"
|
||||||
|
[isPercent]="true"
|
||||||
|
[value]="markets?.emergingMarkets?.value"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
|
<gf-value
|
||||||
|
label="Other Markets"
|
||||||
|
size="large"
|
||||||
|
[isPercent]="true"
|
||||||
|
[value]="markets?.otherMarkets?.value"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row my-5">
|
<div class="row my-5">
|
||||||
<div class="col-md-8 offset-md-2">
|
<div class="col-md-10 offset-md-1">
|
||||||
<h2 class="h4 mb-1 text-center">
|
<h2 class="h4 mb-1 text-center">
|
||||||
Would you like to <strong>refine</strong> your
|
Would you like to <strong>refine</strong> your
|
||||||
<strong>personal investment strategy</strong>?
|
<strong>personal investment strategy</strong>?
|
||||||
|
@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
|
|||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module';
|
import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module';
|
||||||
import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module';
|
import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module';
|
||||||
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
|
|
||||||
import { PublicPageRoutingModule } from './public-page-routing.module';
|
import { PublicPageRoutingModule } from './public-page-routing.module';
|
||||||
import { PublicPageComponent } from './public-page.component';
|
import { PublicPageComponent } from './public-page.component';
|
||||||
@ -14,6 +15,7 @@ import { PublicPageComponent } from './public-page.component';
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
GfPortfolioProportionChartModule,
|
GfPortfolioProportionChartModule,
|
||||||
|
GfValueModule,
|
||||||
GfWorldMapChartModule,
|
GfWorldMapChartModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
|
@ -35,7 +35,7 @@
|
|||||||
>
|
>
|
||||||
or
|
or
|
||||||
</div>
|
</div>
|
||||||
<a color="accent" href="/api/auth/google" mat-flat-button
|
<a color="accent" href="/api/v1/auth/google" mat-flat-button
|
||||||
><ion-icon class="mr-1" name="logo-google"></ion-icon
|
><ion-icon class="mr-1" name="logo-google"></ion-icon
|
||||||
><span i18n>Continue with Google</span></a
|
><span i18n>Continue with Google</span></a
|
||||||
>
|
>
|
||||||
|
@ -28,6 +28,12 @@
|
|||||||
color: rgba(var(--palette-primary-500), 1);
|
color: rgba(var(--palette-primary-500), 1);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mat-tab-link {
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ export class AdminService {
|
|||||||
|
|
||||||
public deleteProfileData({ dataSource, symbol }: UniqueAsset) {
|
public deleteProfileData({ dataSource, symbol }: UniqueAsset) {
|
||||||
return this.http.delete<void>(
|
return this.http.delete<void>(
|
||||||
`/api/admin/profile-data/${dataSource}/${symbol}`
|
`/api/v1/admin/profile-data/${dataSource}/${symbol}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ export class AdminService {
|
|||||||
symbol: string;
|
symbol: string;
|
||||||
}): Observable<AdminMarketDataDetails> {
|
}): Observable<AdminMarketDataDetails> {
|
||||||
return this.http
|
return this.http
|
||||||
.get<any>(`/api/admin/market-data/${dataSource}/${symbol}`)
|
.get<any>(`/api/v1/admin/market-data/${dataSource}/${symbol}`)
|
||||||
.pipe(
|
.pipe(
|
||||||
map((data) => {
|
map((data) => {
|
||||||
for (const item of data.marketData) {
|
for (const item of data.marketData) {
|
||||||
@ -43,16 +43,16 @@ export class AdminService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public gatherMax() {
|
public gatherMax() {
|
||||||
return this.http.post<void>(`/api/admin/gather/max`, {});
|
return this.http.post<void>(`/api/v1/admin/gather/max`, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
public gatherProfileData() {
|
public gatherProfileData() {
|
||||||
return this.http.post<void>(`/api/admin/gather/profile-data`, {});
|
return this.http.post<void>(`/api/v1/admin/gather/profile-data`, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
public gatherProfileDataBySymbol({ dataSource, symbol }: UniqueAsset) {
|
public gatherProfileDataBySymbol({ dataSource, symbol }: UniqueAsset) {
|
||||||
return this.http.post<void>(
|
return this.http.post<void>(
|
||||||
`/api/admin/gather/profile-data/${dataSource}/${symbol}`,
|
`/api/v1/admin/gather/profile-data/${dataSource}/${symbol}`,
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -64,7 +64,7 @@ export class AdminService {
|
|||||||
}: UniqueAsset & {
|
}: UniqueAsset & {
|
||||||
date?: Date;
|
date?: Date;
|
||||||
}) {
|
}) {
|
||||||
let url = `/api/admin/gather/${dataSource}/${symbol}`;
|
let url = `/api/v1/admin/gather/${dataSource}/${symbol}`;
|
||||||
|
|
||||||
if (date) {
|
if (date) {
|
||||||
url = `${url}/${format(date, DATE_FORMAT)}`;
|
url = `${url}/${format(date, DATE_FORMAT)}`;
|
||||||
@ -82,7 +82,7 @@ export class AdminService {
|
|||||||
date: Date;
|
date: Date;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
}) {
|
}) {
|
||||||
const url = `/api/symbol/${dataSource}/${symbol}/${format(
|
const url = `/api/v1/symbol/${dataSource}/${symbol}/${format(
|
||||||
date,
|
date,
|
||||||
DATE_FORMAT
|
DATE_FORMAT
|
||||||
)}`;
|
)}`;
|
||||||
@ -101,7 +101,7 @@ export class AdminService {
|
|||||||
marketData: UpdateMarketDataDto;
|
marketData: UpdateMarketDataDto;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
}) {
|
}) {
|
||||||
const url = `/api/admin/market-data/${dataSource}/${symbol}/${format(
|
const url = `/api/v1/admin/market-data/${dataSource}/${symbol}/${format(
|
||||||
date,
|
date,
|
||||||
DATE_FORMAT
|
DATE_FORMAT
|
||||||
)}`;
|
)}`;
|
||||||
|
@ -8,6 +8,6 @@ export class CacheService {
|
|||||||
public constructor(private http: HttpClient) {}
|
public constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
public flush() {
|
public flush() {
|
||||||
return this.http.post<any>(`/api/cache/flush`, {});
|
return this.http.post<any>(`/api/v1/cache/flush`, {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,12 +23,10 @@ import {
|
|||||||
PortfolioChart,
|
PortfolioChart,
|
||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
PortfolioInvestments,
|
PortfolioInvestments,
|
||||||
PortfolioPerformance,
|
|
||||||
PortfolioPerformanceResponse,
|
PortfolioPerformanceResponse,
|
||||||
PortfolioPublicDetails,
|
PortfolioPublicDetails,
|
||||||
PortfolioReport,
|
PortfolioReport,
|
||||||
PortfolioSummary,
|
PortfolioSummary,
|
||||||
UniqueAsset,
|
|
||||||
User
|
User
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { permissions } from '@ghostfolio/common/permissions';
|
import { permissions } from '@ghostfolio/common/permissions';
|
||||||
@ -52,46 +50,46 @@ export class DataService {
|
|||||||
couponId?: string;
|
couponId?: string;
|
||||||
priceId: string;
|
priceId: string;
|
||||||
}) {
|
}) {
|
||||||
return this.http.post('/api/subscription/stripe/checkout-session', {
|
return this.http.post('/api/v1/subscription/stripe/checkout-session', {
|
||||||
couponId,
|
couponId,
|
||||||
priceId
|
priceId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchAccounts() {
|
public fetchAccounts() {
|
||||||
return this.http.get<Accounts>('/api/account');
|
return this.http.get<Accounts>('/api/v1/account');
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchAdminData() {
|
public fetchAdminData() {
|
||||||
return this.http.get<AdminData>('/api/admin');
|
return this.http.get<AdminData>('/api/v1/admin');
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchAdminMarketData() {
|
public fetchAdminMarketData() {
|
||||||
return this.http.get<AdminMarketData>('/api/admin/market-data');
|
return this.http.get<AdminMarketData>('/api/v1/admin/market-data');
|
||||||
}
|
}
|
||||||
|
|
||||||
public deleteAccess(aId: string) {
|
public deleteAccess(aId: string) {
|
||||||
return this.http.delete<any>(`/api/access/${aId}`);
|
return this.http.delete<any>(`/api/v1/access/${aId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public deleteAccount(aId: string) {
|
public deleteAccount(aId: string) {
|
||||||
return this.http.delete<any>(`/api/account/${aId}`);
|
return this.http.delete<any>(`/api/v1/account/${aId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public deleteOrder(aId: string) {
|
public deleteOrder(aId: string) {
|
||||||
return this.http.delete<any>(`/api/order/${aId}`);
|
return this.http.delete<any>(`/api/v1/order/${aId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public deleteUser(aId: string) {
|
public deleteUser(aId: string) {
|
||||||
return this.http.delete<any>(`/api/user/${aId}`);
|
return this.http.delete<any>(`/api/v1/user/${aId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchAccesses() {
|
public fetchAccesses() {
|
||||||
return this.http.get<Access[]>('/api/access');
|
return this.http.get<Access[]>('/api/v1/access');
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchChart({ range }: { range: DateRange }) {
|
public fetchChart({ range }: { range: DateRange }) {
|
||||||
return this.http.get<PortfolioChart>('/api/portfolio/chart', {
|
return this.http.get<PortfolioChart>('/api/v1/portfolio/chart', {
|
||||||
params: { range }
|
params: { range }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -103,7 +101,7 @@ export class DataService {
|
|||||||
params = params.append('activityIds', activityIds.join(','));
|
params = params.append('activityIds', activityIds.join(','));
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.http.get<Export>('/api/export', {
|
return this.http.get<Export>('/api/v1/export', {
|
||||||
params
|
params
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -121,7 +119,7 @@ export class DataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public fetchInvestments(): Observable<PortfolioInvestments> {
|
public fetchInvestments(): Observable<PortfolioInvestments> {
|
||||||
return this.http.get<any>('/api/portfolio/investments').pipe(
|
return this.http.get<any>('/api/v1/portfolio/investments').pipe(
|
||||||
map((response) => {
|
map((response) => {
|
||||||
if (response.firstOrderDate) {
|
if (response.firstOrderDate) {
|
||||||
response.firstOrderDate = parseISO(response.firstOrderDate);
|
response.firstOrderDate = parseISO(response.firstOrderDate);
|
||||||
@ -147,7 +145,7 @@ export class DataService {
|
|||||||
params = params.append('includeHistoricalData', includeHistoricalData);
|
params = params.append('includeHistoricalData', includeHistoricalData);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.http.get<SymbolItem>(`/api/symbol/${dataSource}/${symbol}`, {
|
return this.http.get<SymbolItem>(`/api/v1/symbol/${dataSource}/${symbol}`, {
|
||||||
params
|
params
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -157,14 +155,14 @@ export class DataService {
|
|||||||
}: {
|
}: {
|
||||||
range: DateRange;
|
range: DateRange;
|
||||||
}): Observable<PortfolioPositions> {
|
}): Observable<PortfolioPositions> {
|
||||||
return this.http.get<PortfolioPositions>('/api/portfolio/positions', {
|
return this.http.get<PortfolioPositions>('/api/v1/portfolio/positions', {
|
||||||
params: { range }
|
params: { range }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchSymbols(aQuery: string) {
|
public fetchSymbols(aQuery: string) {
|
||||||
return this.http
|
return this.http
|
||||||
.get<{ items: LookupItem[] }>(`/api/symbol/lookup?query=${aQuery}`)
|
.get<{ items: LookupItem[] }>(`/api/v1/symbol/lookup?query=${aQuery}`)
|
||||||
.pipe(
|
.pipe(
|
||||||
map((respose) => {
|
map((respose) => {
|
||||||
return respose.items;
|
return respose.items;
|
||||||
@ -173,7 +171,7 @@ export class DataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public fetchOrders(): Observable<Activities> {
|
public fetchOrders(): Observable<Activities> {
|
||||||
return this.http.get<any>('/api/order').pipe(
|
return this.http.get<any>('/api/v1/order').pipe(
|
||||||
map(({ activities }) => {
|
map(({ activities }) => {
|
||||||
for (const activity of activities) {
|
for (const activity of activities) {
|
||||||
activity.createdAt = parseISO(activity.createdAt);
|
activity.createdAt = parseISO(activity.createdAt);
|
||||||
@ -185,14 +183,14 @@ export class DataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public fetchPortfolioDetails(aParams: { [param: string]: any }) {
|
public fetchPortfolioDetails(aParams: { [param: string]: any }) {
|
||||||
return this.http.get<PortfolioDetails>('/api/portfolio/details', {
|
return this.http.get<PortfolioDetails>('/api/v1/portfolio/details', {
|
||||||
params: aParams
|
params: aParams
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchPortfolioPerformance(params: { [param: string]: any }) {
|
public fetchPortfolioPerformance(params: { [param: string]: any }) {
|
||||||
return this.http.get<PortfolioPerformanceResponse>(
|
return this.http.get<PortfolioPerformanceResponse>(
|
||||||
'/api/portfolio/performance',
|
'/api/v1/portfolio/performance',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@ -201,16 +199,16 @@ export class DataService {
|
|||||||
|
|
||||||
public fetchPortfolioPublic(aId: string) {
|
public fetchPortfolioPublic(aId: string) {
|
||||||
return this.http.get<PortfolioPublicDetails>(
|
return this.http.get<PortfolioPublicDetails>(
|
||||||
`/api/portfolio/public/${aId}`
|
`/api/v1/portfolio/public/${aId}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchPortfolioReport() {
|
public fetchPortfolioReport() {
|
||||||
return this.http.get<PortfolioReport>('/api/portfolio/report');
|
return this.http.get<PortfolioReport>('/api/v1/portfolio/report');
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchPortfolioSummary(): Observable<PortfolioSummary> {
|
public fetchPortfolioSummary(): Observable<PortfolioSummary> {
|
||||||
return this.http.get<any>('/api/portfolio/summary').pipe(
|
return this.http.get<any>('/api/v1/portfolio/summary').pipe(
|
||||||
map((summary) => {
|
map((summary) => {
|
||||||
if (summary.firstOrderDate) {
|
if (summary.firstOrderDate) {
|
||||||
summary.firstOrderDate = parseISO(summary.firstOrderDate);
|
summary.firstOrderDate = parseISO(summary.firstOrderDate);
|
||||||
@ -229,7 +227,7 @@ export class DataService {
|
|||||||
symbol: string;
|
symbol: string;
|
||||||
}) {
|
}) {
|
||||||
return this.http
|
return this.http
|
||||||
.get<any>(`/api/portfolio/position/${dataSource}/${symbol}`)
|
.get<any>(`/api/v1/portfolio/position/${dataSource}/${symbol}`)
|
||||||
.pipe(
|
.pipe(
|
||||||
map((data) => {
|
map((data) => {
|
||||||
if (data.orders) {
|
if (data.orders) {
|
||||||
@ -245,47 +243,47 @@ export class DataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public loginAnonymous(accessToken: string) {
|
public loginAnonymous(accessToken: string) {
|
||||||
return this.http.get<any>(`/api/auth/anonymous/${accessToken}`);
|
return this.http.get<any>(`/api/v1/auth/anonymous/${accessToken}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public postAccess(aAccess: CreateAccessDto) {
|
public postAccess(aAccess: CreateAccessDto) {
|
||||||
return this.http.post<OrderModel>(`/api/access`, aAccess);
|
return this.http.post<OrderModel>(`/api/v1/access`, aAccess);
|
||||||
}
|
}
|
||||||
|
|
||||||
public postAccount(aAccount: CreateAccountDto) {
|
public postAccount(aAccount: CreateAccountDto) {
|
||||||
return this.http.post<OrderModel>(`/api/account`, aAccount);
|
return this.http.post<OrderModel>(`/api/v1/account`, aAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
public postOrder(aOrder: CreateOrderDto) {
|
public postOrder(aOrder: CreateOrderDto) {
|
||||||
return this.http.post<OrderModel>(`/api/order`, aOrder);
|
return this.http.post<OrderModel>(`/api/v1/order`, aOrder);
|
||||||
}
|
}
|
||||||
|
|
||||||
public postUser() {
|
public postUser() {
|
||||||
return this.http.post<UserItem>(`/api/user`, {});
|
return this.http.post<UserItem>(`/api/v1/user`, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
public putAccount(aAccount: UpdateAccountDto) {
|
public putAccount(aAccount: UpdateAccountDto) {
|
||||||
return this.http.put<UserItem>(`/api/account/${aAccount.id}`, aAccount);
|
return this.http.put<UserItem>(`/api/v1/account/${aAccount.id}`, aAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
public putAdminSetting(key: string, aData: PropertyDto) {
|
public putAdminSetting(key: string, aData: PropertyDto) {
|
||||||
return this.http.put<void>(`/api/admin/settings/${key}`, aData);
|
return this.http.put<void>(`/api/v1/admin/settings/${key}`, aData);
|
||||||
}
|
}
|
||||||
|
|
||||||
public putOrder(aOrder: UpdateOrderDto) {
|
public putOrder(aOrder: UpdateOrderDto) {
|
||||||
return this.http.put<UserItem>(`/api/order/${aOrder.id}`, aOrder);
|
return this.http.put<UserItem>(`/api/v1/order/${aOrder.id}`, aOrder);
|
||||||
}
|
}
|
||||||
|
|
||||||
public putUserSetting(aData: UpdateUserSettingDto) {
|
public putUserSetting(aData: UpdateUserSettingDto) {
|
||||||
return this.http.put<User>(`/api/user/setting`, aData);
|
return this.http.put<User>(`/api/v1/user/setting`, aData);
|
||||||
}
|
}
|
||||||
|
|
||||||
public putUserSettings(aData: UpdateUserSettingsDto) {
|
public putUserSettings(aData: UpdateUserSettingsDto) {
|
||||||
return this.http.put<User>(`/api/user/settings`, aData);
|
return this.http.put<User>(`/api/v1/user/settings`, aData);
|
||||||
}
|
}
|
||||||
|
|
||||||
public redeemCoupon(couponCode: string) {
|
public redeemCoupon(couponCode: string) {
|
||||||
return this.http.post('/api/subscription/redeem-coupon', {
|
return this.http.post('/api/v1/subscription/redeem-coupon', {
|
||||||
couponCode
|
couponCode
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -37,9 +37,9 @@ export class ImportTransactionsService {
|
|||||||
skipEmptyLines: true
|
skipEmptyLines: true
|
||||||
}).data;
|
}).data;
|
||||||
|
|
||||||
const orders: CreateOrderDto[] = [];
|
const activities: CreateOrderDto[] = [];
|
||||||
for (const [index, item] of content.entries()) {
|
for (const [index, item] of content.entries()) {
|
||||||
orders.push({
|
activities.push({
|
||||||
accountId: this.parseAccount({ item, userAccounts }),
|
accountId: this.parseAccount({ item, userAccounts }),
|
||||||
currency: this.parseCurrency({ content, index, item }),
|
currency: this.parseCurrency({ content, index, item }),
|
||||||
dataSource: this.parseDataSource({ item }),
|
dataSource: this.parseDataSource({ item }),
|
||||||
@ -52,13 +52,13 @@ export class ImportTransactionsService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.importJson({ content: orders });
|
await this.importJson({ content: activities });
|
||||||
}
|
}
|
||||||
|
|
||||||
public importJson({ content }: { content: CreateOrderDto[] }): Promise<void> {
|
public importJson({ content }: { content: CreateOrderDto[] }): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.postImport({
|
this.postImport({
|
||||||
orders: content
|
activities: content
|
||||||
})
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
catchError((error) => {
|
catchError((error) => {
|
||||||
@ -121,7 +121,10 @@ export class ImportTransactionsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw { message: `orders.${index}.currency is not valid`, orders: content };
|
throw {
|
||||||
|
activities: content,
|
||||||
|
message: `activities.${index}.currency is not valid`
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseDataSource({ item }: { item: any }) {
|
private parseDataSource({ item }: { item: any }) {
|
||||||
@ -164,7 +167,10 @@ export class ImportTransactionsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw { message: `orders.${index}.date is not valid`, orders: content };
|
throw {
|
||||||
|
activities: content,
|
||||||
|
message: `activities.${index}.date is not valid`
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseFee({
|
private parseFee({
|
||||||
@ -184,7 +190,10 @@ export class ImportTransactionsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw { message: `orders.${index}.fee is not valid`, orders: content };
|
throw {
|
||||||
|
activities: content,
|
||||||
|
message: `activities.${index}.fee is not valid`
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseQuantity({
|
private parseQuantity({
|
||||||
@ -204,7 +213,10 @@ export class ImportTransactionsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw { message: `orders.${index}.quantity is not valid`, orders: content };
|
throw {
|
||||||
|
activities: content,
|
||||||
|
message: `activities.${index}.quantity is not valid`
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseSymbol({
|
private parseSymbol({
|
||||||
@ -224,7 +236,10 @@ export class ImportTransactionsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw { message: `orders.${index}.symbol is not valid`, orders: content };
|
throw {
|
||||||
|
activities: content,
|
||||||
|
message: `activities.${index}.symbol is not valid`
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseType({
|
private parseType({
|
||||||
@ -255,7 +270,10 @@ export class ImportTransactionsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw { message: `orders.${index}.type is not valid`, orders: content };
|
throw {
|
||||||
|
activities: content,
|
||||||
|
message: `activities.${index}.type is not valid`
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseUnitPrice({
|
private parseUnitPrice({
|
||||||
@ -276,12 +294,12 @@ export class ImportTransactionsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
throw {
|
throw {
|
||||||
message: `orders.${index}.unitPrice is not valid`,
|
activities: content,
|
||||||
orders: content
|
message: `activities.${index}.unitPrice is not valid`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private postImport(aImportData: { orders: CreateOrderDto[] }) {
|
private postImport(aImportData: { activities: CreateOrderDto[] }) {
|
||||||
return this.http.post<void>('/api/import', aImportData);
|
return this.http.post<void>('/api/v1/import', aImportData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ export class UserService extends ObservableStore<UserStoreState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fetchUser() {
|
private fetchUser() {
|
||||||
return this.http.get<User>('/api/user').pipe(
|
return this.http.get<User>('/api/v1/user').pipe(
|
||||||
map((user) => {
|
map((user) => {
|
||||||
this.setState({ user }, UserStoreActions.GetUser);
|
this.setState({ user }, UserStoreActions.GetUser);
|
||||||
return user;
|
return user;
|
||||||
|
@ -35,7 +35,7 @@ export class WebAuthnService {
|
|||||||
public register() {
|
public register() {
|
||||||
return this.http
|
return this.http
|
||||||
.get<PublicKeyCredentialCreationOptionsJSON>(
|
.get<PublicKeyCredentialCreationOptionsJSON>(
|
||||||
`/api/auth/webauthn/generate-registration-options`,
|
`/api/v1/auth/webauthn/generate-registration-options`,
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
.pipe(
|
.pipe(
|
||||||
@ -48,7 +48,7 @@ export class WebAuthnService {
|
|||||||
}),
|
}),
|
||||||
switchMap((attResp) => {
|
switchMap((attResp) => {
|
||||||
return this.http.post<AuthDeviceDto>(
|
return this.http.post<AuthDeviceDto>(
|
||||||
`/api/auth/webauthn/verify-attestation`,
|
`/api/v1/auth/webauthn/verify-attestation`,
|
||||||
{
|
{
|
||||||
credential: attResp
|
credential: attResp
|
||||||
}
|
}
|
||||||
@ -65,7 +65,9 @@ export class WebAuthnService {
|
|||||||
|
|
||||||
public deregister() {
|
public deregister() {
|
||||||
const deviceId = this.getDeviceId();
|
const deviceId = this.getDeviceId();
|
||||||
return this.http.delete<AuthDeviceDto>(`/api/auth-device/${deviceId}`).pipe(
|
return this.http
|
||||||
|
.delete<AuthDeviceDto>(`/api/v1/auth-device/${deviceId}`)
|
||||||
|
.pipe(
|
||||||
catchError((error) => {
|
catchError((error) => {
|
||||||
console.warn(`Could not deregister device ${deviceId}`, error);
|
console.warn(`Could not deregister device ${deviceId}`, error);
|
||||||
return of(null);
|
return of(null);
|
||||||
@ -82,14 +84,14 @@ export class WebAuthnService {
|
|||||||
const deviceId = this.getDeviceId();
|
const deviceId = this.getDeviceId();
|
||||||
return this.http
|
return this.http
|
||||||
.post<PublicKeyCredentialRequestOptionsJSON>(
|
.post<PublicKeyCredentialRequestOptionsJSON>(
|
||||||
`/api/auth/webauthn/generate-assertion-options`,
|
`/api/v1/auth/webauthn/generate-assertion-options`,
|
||||||
{ deviceId }
|
{ deviceId }
|
||||||
)
|
)
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap(startAuthentication),
|
switchMap(startAuthentication),
|
||||||
switchMap((assertionResponse) => {
|
switchMap((assertionResponse) => {
|
||||||
return this.http.post<{ authToken: string }>(
|
return this.http.post<{ authToken: string }>(
|
||||||
`/api/auth/webauthn/verify-assertion`,
|
`/api/v1/auth/webauthn/verify-assertion`,
|
||||||
{
|
{
|
||||||
credential: assertionResponse,
|
credential: assertionResponse,
|
||||||
deviceId
|
deviceId
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { enableProdMode } from '@angular/core';
|
import { enableProdMode } from '@angular/core';
|
||||||
import { LOCALE_ID } from '@angular/core';
|
import { LOCALE_ID } from '@angular/core';
|
||||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||||
|
import { locale } from '@ghostfolio/common/config';
|
||||||
import { InfoItem } from '@ghostfolio/common/interfaces';
|
import { InfoItem } from '@ghostfolio/common/interfaces';
|
||||||
import { permissions } from '@ghostfolio/common/permissions';
|
import { permissions } from '@ghostfolio/common/permissions';
|
||||||
|
|
||||||
@ -8,7 +9,7 @@ import { AppModule } from './app/app.module';
|
|||||||
import { environment } from './environments/environment';
|
import { environment } from './environments/environment';
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const response = await fetch('/api/info');
|
const response = await fetch('/api/v1/info');
|
||||||
const info: InfoItem = await response.json();
|
const info: InfoItem = await response.json();
|
||||||
|
|
||||||
if (window.localStorage.getItem('utm_source') === 'trusted-web-activity') {
|
if (window.localStorage.getItem('utm_source') === 'trusted-web-activity') {
|
||||||
@ -27,7 +28,7 @@ import { environment } from './environments/environment';
|
|||||||
|
|
||||||
platformBrowserDynamic()
|
platformBrowserDynamic()
|
||||||
.bootstrapModule(AppModule, {
|
.bootstrapModule(AppModule, {
|
||||||
providers: [{ provide: LOCALE_ID, useValue: 'de-CH' }]
|
providers: [{ provide: LOCALE_ID, useValue: locale }]
|
||||||
})
|
})
|
||||||
.catch((error) => console.error(error));
|
.catch((error) => console.error(error));
|
||||||
})();
|
})();
|
||||||
|
@ -11,20 +11,22 @@
|
|||||||
|
|
||||||
.mat-row {
|
.mat-row {
|
||||||
&:nth-child(even) {
|
&:nth-child(even) {
|
||||||
background-color: rgba(
|
background-color: rgba(var(--palette-foreground-base), 0.02);
|
||||||
var(--palette-foreground-base),
|
}
|
||||||
var(--palette-background-hover-alpha)
|
|
||||||
);
|
&:hover {
|
||||||
|
background-color: rgba(var(--palette-foreground-base), 0.05);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@if $darkTheme {
|
@if $darkTheme {
|
||||||
.mat-row {
|
.mat-row {
|
||||||
&:nth-child(even) {
|
&:nth-child(even) {
|
||||||
background-color: rgba(
|
background-color: rgba(var(--palette-foreground-base-dark), 0.02);
|
||||||
var(--palette-foreground-base-dark),
|
}
|
||||||
var(--palette-background-hover-alpha)
|
|
||||||
);
|
&:hover {
|
||||||
|
background-color: rgba(var(--palette-foreground-base-dark), 0.05);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
version: '3.7'
|
version: '3.7'
|
||||||
services:
|
services:
|
||||||
ghostfolio:
|
ghostfolio:
|
||||||
image: ghostfolio/ghostfolio
|
image: ghostfolio/ghostfolio:latest
|
||||||
env_file:
|
env_file:
|
||||||
- ../.env
|
- ../.env
|
||||||
environment:
|
environment:
|
||||||
|
@ -19,7 +19,7 @@ export const ghostfolioCashSymbol = `${ghostfolioScraperApiSymbolPrefix}CASH`;
|
|||||||
export const ghostfolioFearAndGreedIndexDataSource = DataSource.RAKUTEN;
|
export const ghostfolioFearAndGreedIndexDataSource = DataSource.RAKUTEN;
|
||||||
export const ghostfolioFearAndGreedIndexSymbol = `${ghostfolioScraperApiSymbolPrefix}FEAR_AND_GREED_INDEX`;
|
export const ghostfolioFearAndGreedIndexSymbol = `${ghostfolioScraperApiSymbolPrefix}FEAR_AND_GREED_INDEX`;
|
||||||
|
|
||||||
export const locale = 'de-CH';
|
export const locale = 'en-US';
|
||||||
|
|
||||||
export const primaryColorHex = '#36cfcc';
|
export const primaryColorHex = '#36cfcc';
|
||||||
export const primaryColorRgb = {
|
export const primaryColorRgb = {
|
||||||
@ -44,7 +44,6 @@ export const warnColorRgb = {
|
|||||||
|
|
||||||
export const ASSET_SUB_CLASS_EMERGENCY_FUND = 'EMERGENCY_FUND';
|
export const ASSET_SUB_CLASS_EMERGENCY_FUND = 'EMERGENCY_FUND';
|
||||||
|
|
||||||
export const DEFAULT_DATE_FORMAT = 'dd.MM.yyyy';
|
|
||||||
export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy';
|
export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy';
|
||||||
|
|
||||||
export const PROPERTY_COUPONS = 'COUPONS';
|
export const PROPERTY_COUPONS = 'COUPONS';
|
||||||
|
@ -2,7 +2,7 @@ import * as currencies from '@dinero.js/currencies';
|
|||||||
import { DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
import { getDate, getMonth, getYear, parse, subDays } from 'date-fns';
|
import { getDate, getMonth, getYear, parse, subDays } from 'date-fns';
|
||||||
|
|
||||||
import { ghostfolioScraperApiSymbolPrefix } from './config';
|
import { ghostfolioScraperApiSymbolPrefix, locale } from './config';
|
||||||
|
|
||||||
export function capitalize(aString: string) {
|
export function capitalize(aString: string) {
|
||||||
return aString.charAt(0).toUpperCase() + aString.slice(1).toLowerCase();
|
return aString.charAt(0).toUpperCase() + aString.slice(1).toLowerCase();
|
||||||
@ -44,6 +44,49 @@ export function getCssVariable(aCssVariable: string) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getDateFormatString(aLocale?: string) {
|
||||||
|
const formatObject = new Intl.DateTimeFormat(aLocale).formatToParts(
|
||||||
|
new Date()
|
||||||
|
);
|
||||||
|
|
||||||
|
return formatObject
|
||||||
|
.map((object) => {
|
||||||
|
switch (object.type) {
|
||||||
|
case 'day':
|
||||||
|
return 'dd';
|
||||||
|
case 'month':
|
||||||
|
return 'MM';
|
||||||
|
case 'year':
|
||||||
|
return 'yyyy';
|
||||||
|
default:
|
||||||
|
return object.value;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLocale() {
|
||||||
|
return navigator.languages?.length
|
||||||
|
? navigator.languages[0]
|
||||||
|
: navigator.language ?? locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNumberFormatDecimal(aLocale?: string) {
|
||||||
|
const formatObject = new Intl.NumberFormat(aLocale).formatToParts(9999.99);
|
||||||
|
|
||||||
|
return formatObject.find((object) => {
|
||||||
|
return object.type === 'decimal';
|
||||||
|
}).value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNumberFormatGroup(aLocale?: string) {
|
||||||
|
const formatObject = new Intl.NumberFormat(aLocale).formatToParts(9999.99);
|
||||||
|
|
||||||
|
return formatObject.find((object) => {
|
||||||
|
return object.type === 'group';
|
||||||
|
}).value;
|
||||||
|
}
|
||||||
|
|
||||||
export function getTextColor() {
|
export function getTextColor() {
|
||||||
const cssVariable = getCssVariable(
|
const cssVariable = getCssVariable(
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').matches
|
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
|
@ -5,5 +5,5 @@ export interface Export {
|
|||||||
date: string;
|
date: string;
|
||||||
version: string;
|
version: string;
|
||||||
};
|
};
|
||||||
orders: Partial<Order>[];
|
activities: Partial<Order>[];
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces';
|
import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { AssetClass, AssetSubClass, DataSource } from '@prisma/client';
|
import { AssetClass, AssetSubClass, DataSource } from '@prisma/client';
|
||||||
|
|
||||||
|
import { Market } from '../types';
|
||||||
import { Country } from './country.interface';
|
import { Country } from './country.interface';
|
||||||
import { Sector } from './sector.interface';
|
import { Sector } from './sector.interface';
|
||||||
|
|
||||||
@ -19,6 +20,7 @@ export interface PortfolioPosition {
|
|||||||
marketChange?: number;
|
marketChange?: number;
|
||||||
marketChangePercent?: number;
|
marketChangePercent?: number;
|
||||||
marketPrice: number;
|
marketPrice: number;
|
||||||
|
markets?: { [key in Market]: number };
|
||||||
marketState: MarketState;
|
marketState: MarketState;
|
||||||
name: string;
|
name: string;
|
||||||
netPerformance: number;
|
netPerformance: number;
|
||||||
|
@ -8,6 +8,7 @@ export interface PortfolioPublicDetails {
|
|||||||
| 'allocationCurrent'
|
| 'allocationCurrent'
|
||||||
| 'countries'
|
| 'countries'
|
||||||
| 'currency'
|
| 'currency'
|
||||||
|
| 'markets'
|
||||||
| 'name'
|
| 'name'
|
||||||
| 'sectors'
|
| 'sectors'
|
||||||
| 'value'
|
| 'value'
|
||||||
|
@ -2,6 +2,7 @@ import type { AccessWithGranteeUser } from './access-with-grantee-user.type';
|
|||||||
import { AccountWithValue } from './account-with-value.type';
|
import { AccountWithValue } from './account-with-value.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 { Market } from './market.type';
|
||||||
import type { OrderWithAccount } from './order-with-account.type';
|
import type { OrderWithAccount } from './order-with-account.type';
|
||||||
import type { RequestWithUser } from './request-with-user.type';
|
import type { RequestWithUser } from './request-with-user.type';
|
||||||
import { ToggleOption } from './toggle-option.type';
|
import { ToggleOption } from './toggle-option.type';
|
||||||
@ -11,6 +12,7 @@ export type {
|
|||||||
AccountWithValue,
|
AccountWithValue,
|
||||||
DateRange,
|
DateRange,
|
||||||
Granularity,
|
Granularity,
|
||||||
|
Market,
|
||||||
OrderWithAccount,
|
OrderWithAccount,
|
||||||
RequestWithUser,
|
RequestWithUser,
|
||||||
ToggleOption
|
ToggleOption
|
||||||
|
1
libs/common/src/lib/types/market.type.ts
Normal file
1
libs/common/src/lib/types/market.type.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type Market = 'developedMarkets' | 'emergingMarkets' | 'otherMarkets';
|
@ -248,14 +248,18 @@
|
|||||||
<ng-container matColumnDef="value">
|
<ng-container matColumnDef="value">
|
||||||
<th
|
<th
|
||||||
*matHeaderCellDef
|
*matHeaderCellDef
|
||||||
class="justify-content-end px-1"
|
class="d-none d-lg-table-cell justify-content-end px-1"
|
||||||
i18n
|
i18n
|
||||||
mat-header-cell
|
mat-header-cell
|
||||||
mat-sort-header
|
mat-sort-header
|
||||||
>
|
>
|
||||||
Value
|
Value
|
||||||
</th>
|
</th>
|
||||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
<td
|
||||||
|
*matCellDef="let element"
|
||||||
|
class="d-none d-lg-table-cell px-1"
|
||||||
|
mat-cell
|
||||||
|
>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<gf-value
|
<gf-value
|
||||||
[isCurrency]="true"
|
[isCurrency]="true"
|
||||||
@ -264,7 +268,38 @@
|
|||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td *matFooterCellDef class="px-1" mat-footer-cell>
|
<td *matFooterCellDef class="d-none d-lg-table-cell px-1" mat-footer-cell>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<gf-value
|
||||||
|
[isAbsolute]="true"
|
||||||
|
[isCurrency]="true"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="isLoading ? undefined : totalValue"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="valueInBaseCurrency">
|
||||||
|
<th
|
||||||
|
*matHeaderCellDef
|
||||||
|
class="d-lg-none d-xl-none justify-content-end px-1"
|
||||||
|
i18n
|
||||||
|
mat-header-cell
|
||||||
|
mat-sort-header
|
||||||
|
>
|
||||||
|
Value
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="d-lg-none d-xl-none px-1" mat-cell>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<gf-value
|
||||||
|
[isCurrency]="true"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="isLoading ? undefined : element.valueInBaseCurrency"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td *matFooterCellDef class="d-lg-none d-xl-none px-1" mat-footer-cell>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<gf-value
|
<gf-value
|
||||||
[isAbsolute]="true"
|
[isAbsolute]="true"
|
||||||
|
@ -20,10 +20,9 @@ import { MatSort } from '@angular/material/sort';
|
|||||||
import { MatTableDataSource } from '@angular/material/table';
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
|
import { getDateFormatString } from '@ghostfolio/common/helper';
|
||||||
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
import { OrderWithAccount } from '@ghostfolio/common/types';
|
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||||
import { DataSource } from '@prisma/client';
|
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import { isUUID } from 'class-validator';
|
import { isUUID } from 'class-validator';
|
||||||
import { endOfToday, format, isAfter } from 'date-fns';
|
import { endOfToday, format, isAfter } from 'date-fns';
|
||||||
@ -64,7 +63,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
|||||||
@ViewChild(MatSort) sort: MatSort;
|
@ViewChild(MatSort) sort: MatSort;
|
||||||
|
|
||||||
public dataSource: MatTableDataSource<Activity> = new MatTableDataSource();
|
public dataSource: MatTableDataSource<Activity> = new MatTableDataSource();
|
||||||
public defaultDateFormat = DEFAULT_DATE_FORMAT;
|
public defaultDateFormat: string;
|
||||||
public displayedColumns = [];
|
public displayedColumns = [];
|
||||||
public endOfToday = endOfToday();
|
public endOfToday = endOfToday();
|
||||||
public filters$: Subject<string[]> = new BehaviorSubject([]);
|
public filters$: Subject<string[]> = new BehaviorSubject([]);
|
||||||
@ -141,6 +140,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
|||||||
'fee',
|
'fee',
|
||||||
'value',
|
'value',
|
||||||
'currency',
|
'currency',
|
||||||
|
'valueInBaseCurrency',
|
||||||
'account',
|
'account',
|
||||||
'actions'
|
'actions'
|
||||||
];
|
];
|
||||||
@ -153,6 +153,8 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
|||||||
|
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
|
||||||
|
this.defaultDateFormat = getDateFormatString(this.locale);
|
||||||
|
|
||||||
if (this.activities) {
|
if (this.activities) {
|
||||||
this.dataSource = new MatTableDataSource(this.activities);
|
this.dataSource = new MatTableDataSource(this.activities);
|
||||||
this.dataSource.filterPredicate = (data, filter) => {
|
this.dataSource.filterPredicate = (data, filter) => {
|
||||||
|
@ -43,10 +43,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
<small *ngIf="label">
|
<ng-container *ngIf="label">
|
||||||
|
<div *ngIf="size === 'large'">
|
||||||
|
{{ label }}
|
||||||
|
</div>
|
||||||
|
<small *ngIf="size !== 'large'">
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</small>
|
</small>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<ngx-skeleton-loader
|
<ngx-skeleton-loader
|
||||||
*ngIf="value === undefined"
|
*ngIf="value === undefined"
|
||||||
|
@ -4,8 +4,8 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
OnChanges
|
OnChanges
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
|
import { getLocale } from '@ghostfolio/common/helper';
|
||||||
import { format, isDate, parseISO } from 'date-fns';
|
import { isDate, parseISO } from 'date-fns';
|
||||||
import { isNumber } from 'lodash';
|
import { isNumber } from 'lodash';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -21,7 +21,7 @@ export class ValueComponent implements OnChanges {
|
|||||||
@Input() isCurrency = false;
|
@Input() isCurrency = false;
|
||||||
@Input() isPercent = false;
|
@Input() isPercent = false;
|
||||||
@Input() label = '';
|
@Input() label = '';
|
||||||
@Input() locale = '';
|
@Input() locale = getLocale();
|
||||||
@Input() position = '';
|
@Input() position = '';
|
||||||
@Input() precision: number | undefined;
|
@Input() precision: number | undefined;
|
||||||
@Input() size: 'large' | 'medium' | 'small' = 'small';
|
@Input() size: 'large' | 'medium' | 'small' = 'small';
|
||||||
@ -102,10 +102,13 @@ export class ValueComponent implements OnChanges {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (isDate(parseISO(this.value))) {
|
if (isDate(parseISO(this.value))) {
|
||||||
this.formattedValue = format(
|
this.formattedValue = new Date(
|
||||||
new Date(<string>this.value),
|
<string>this.value
|
||||||
DEFAULT_DATE_FORMAT
|
).toLocaleDateString(this.locale, {
|
||||||
);
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
this.formattedValue = this.value;
|
this.formattedValue = this.value;
|
||||||
@ -116,5 +119,9 @@ export class ValueComponent implements OnChanges {
|
|||||||
if (this.formattedValue === '0.00') {
|
if (this.formattedValue === '0.00') {
|
||||||
this.useAbsoluteValue = true;
|
this.useAbsoluteValue = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.isPercent) {
|
||||||
|
this.formattedValue = '– ';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ghostfolio",
|
"name": "ghostfolio",
|
||||||
"version": "1.127.0",
|
"version": "1.132.0",
|
||||||
"homepage": "https://ghostfol.io",
|
"homepage": "https://ghostfol.io",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -71,7 +71,7 @@
|
|||||||
"@nestjs/schedule": "1.0.2",
|
"@nestjs/schedule": "1.0.2",
|
||||||
"@nestjs/serve-static": "2.2.2",
|
"@nestjs/serve-static": "2.2.2",
|
||||||
"@nrwl/angular": "13.8.5",
|
"@nrwl/angular": "13.8.5",
|
||||||
"@prisma/client": "3.10.0",
|
"@prisma/client": "3.11.1",
|
||||||
"@simplewebauthn/browser": "4.1.0",
|
"@simplewebauthn/browser": "4.1.0",
|
||||||
"@simplewebauthn/server": "4.1.0",
|
"@simplewebauthn/server": "4.1.0",
|
||||||
"@simplewebauthn/typescript-types": "4.0.0",
|
"@simplewebauthn/typescript-types": "4.0.0",
|
||||||
@ -109,7 +109,7 @@
|
|||||||
"passport": "0.4.1",
|
"passport": "0.4.1",
|
||||||
"passport-google-oauth20": "2.0.0",
|
"passport-google-oauth20": "2.0.0",
|
||||||
"passport-jwt": "4.0.0",
|
"passport-jwt": "4.0.0",
|
||||||
"prisma": "3.10.0",
|
"prisma": "3.11.1",
|
||||||
"reflect-metadata": "0.1.13",
|
"reflect-metadata": "0.1.13",
|
||||||
"round-to": "5.0.0",
|
"round-to": "5.0.0",
|
||||||
"rxjs": "7.4.0",
|
"rxjs": "7.4.0",
|
||||||
@ -118,7 +118,7 @@
|
|||||||
"tslib": "2.0.0",
|
"tslib": "2.0.0",
|
||||||
"twitter-api-v2": "1.10.3",
|
"twitter-api-v2": "1.10.3",
|
||||||
"uuid": "8.3.2",
|
"uuid": "8.3.2",
|
||||||
"yahoo-finance2": "2.2.0",
|
"yahoo-finance2": "2.3.0",
|
||||||
"zone.js": "0.11.4"
|
"zone.js": "0.11.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"date": "2021-01-01T00:00:00.000Z",
|
"date": "2021-01-01T00:00:00.000Z",
|
||||||
"version": "dev"
|
"version": "dev"
|
||||||
},
|
},
|
||||||
"orders": [
|
"activities": [
|
||||||
{
|
{
|
||||||
"currency": "USD",
|
"currency": "USD",
|
||||||
"dataSource": "YAHOO",
|
"dataSource": "YAHOO",
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"date": "2021-01-01T00:00:00.000Z",
|
"date": "2021-01-01T00:00:00.000Z",
|
||||||
"version": "dev"
|
"version": "dev"
|
||||||
},
|
},
|
||||||
"orders": [
|
"activities": [
|
||||||
{
|
{
|
||||||
"currency": "USD",
|
"currency": "USD",
|
||||||
"dataSource": "YAHOO",
|
"dataSource": "YAHOO",
|
||||||
|
39
test/import/ok.json
Normal file
39
test/import/ok.json
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"date": "2022-04-01T00:00:00.000Z",
|
||||||
|
"version": "dev"
|
||||||
|
},
|
||||||
|
"activities": [
|
||||||
|
{
|
||||||
|
"accountId": null,
|
||||||
|
"date": "2021-12-31T23:00:00.000Z",
|
||||||
|
"fee": 0,
|
||||||
|
"quantity": 1,
|
||||||
|
"type": "ITEM",
|
||||||
|
"unitPrice": 500000,
|
||||||
|
"currency": "USD",
|
||||||
|
"dataSource": "MANUAL",
|
||||||
|
"symbol": "Penthouse Apartment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2021-11-16T23:00:00.000Z",
|
||||||
|
"fee": 0,
|
||||||
|
"quantity": 5,
|
||||||
|
"type": "DIVIDEND",
|
||||||
|
"unitPrice": 0.62,
|
||||||
|
"currency": "USD",
|
||||||
|
"dataSource": "YAHOO",
|
||||||
|
"symbol": "MSFT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2021-09-15T22:00:00.000Z",
|
||||||
|
"fee": 19,
|
||||||
|
"quantity": 5,
|
||||||
|
"type": "BUY",
|
||||||
|
"unitPrice": 298.58,
|
||||||
|
"currency": "USD",
|
||||||
|
"dataSource": "YAHOO",
|
||||||
|
"symbol": "MSFT"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
44
yarn.lock
44
yarn.lock
@ -3487,22 +3487,22 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.10.1.tgz#728ecd95ab207aab8a9a4e421f0422db329232be"
|
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.10.1.tgz#728ecd95ab207aab8a9a4e421f0422db329232be"
|
||||||
integrity sha512-HnUhk1Sy9IuKrxEMdIRCxpIqPw6BFsbYSEUO9p/hNw5sMld/+3OLMWQP80F8/db9qsv3qUjs7ZR5bS/R+iinXw==
|
integrity sha512-HnUhk1Sy9IuKrxEMdIRCxpIqPw6BFsbYSEUO9p/hNw5sMld/+3OLMWQP80F8/db9qsv3qUjs7ZR5bS/R+iinXw==
|
||||||
|
|
||||||
"@prisma/client@3.10.0":
|
"@prisma/client@3.11.1":
|
||||||
version "3.10.0"
|
version "3.11.1"
|
||||||
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.10.0.tgz#4782fe6f1b0e43c2a11a75ad4bb1098599d1dfb1"
|
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.11.1.tgz#bde6dec71ae133d04ce1c6658e3d76627a3c6dc7"
|
||||||
integrity sha512-6P4sV7WFuODSfSoSEzCH1qfmWMrCUBk1LIIuTbQf6m1LI/IOpLN4lnqGDmgiBGprEzuWobnGLfe9YsXLn0inrg==
|
integrity sha512-B3C7zQG4HbjJzUr2Zg9UVkBJutbqq9/uqkl1S138+keZCubJrwizx3RuIvGwI+s+pm3qbsyNqXiZgL3Ir0fSng==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@prisma/engines-version" "3.10.0-50.73e60b76d394f8d37d8ebd1f8918c79029f0db86"
|
"@prisma/engines-version" "3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9"
|
||||||
|
|
||||||
"@prisma/engines-version@3.10.0-50.73e60b76d394f8d37d8ebd1f8918c79029f0db86":
|
"@prisma/engines-version@3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9":
|
||||||
version "3.10.0-50.73e60b76d394f8d37d8ebd1f8918c79029f0db86"
|
version "3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9"
|
||||||
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.10.0-50.73e60b76d394f8d37d8ebd1f8918c79029f0db86.tgz#82750856fa637dd89b8f095d2dcc6ac0631231c6"
|
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9.tgz#81a1835b495ad287ad7824dbd62f74e9eee90fb9"
|
||||||
integrity sha512-cVYs5gyQH/qyut24hUvDznCfPrWiNMKNfPb9WmEoiU6ihlkscIbCfkmuKTtspVLWRdl0LqjYEC7vfnPv17HWhw==
|
integrity sha512-HkcsDniA4iNb/gi0iuyOJNAM7nD/LwQ0uJm15v360O5dee3TM4lWdSQiTYBMK6FF68ACUItmzSur7oYuUZ2zkQ==
|
||||||
|
|
||||||
"@prisma/engines@3.10.0-50.73e60b76d394f8d37d8ebd1f8918c79029f0db86":
|
"@prisma/engines@3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9":
|
||||||
version "3.10.0-50.73e60b76d394f8d37d8ebd1f8918c79029f0db86"
|
version "3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9"
|
||||||
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.10.0-50.73e60b76d394f8d37d8ebd1f8918c79029f0db86.tgz#2964113729a78b8b21e186b5592affd1fde73c16"
|
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9.tgz#09ac23f8f615a8586d8d44538060ada199fe872c"
|
||||||
integrity sha512-LjRssaWu9w2SrXitofnutRIyURI7l0veQYIALz7uY4shygM9nMcK3omXcObRm7TAcw3Z+9ytfK1B+ySOsOesxQ==
|
integrity sha512-MILbsGnvmnhCbFGa2/iSnsyGyazU3afzD7ldjCIeLIGKkNBMSZgA2IvpYsAXl+6qFHKGrS3B2otKfV31dwMSQw==
|
||||||
|
|
||||||
"@samverschueren/stream-to-observable@^0.3.0":
|
"@samverschueren/stream-to-observable@^0.3.0":
|
||||||
version "0.3.1"
|
version "0.3.1"
|
||||||
@ -15334,12 +15334,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 sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=
|
integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=
|
||||||
|
|
||||||
prisma@3.10.0:
|
prisma@3.11.1:
|
||||||
version "3.10.0"
|
version "3.11.1"
|
||||||
resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.10.0.tgz#872d87afbeb1cbcaa77c3d6a63c125e0d704b04d"
|
resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.11.1.tgz#fff9c0bcf83cb30c2e1d650882d5eb3c5565e028"
|
||||||
integrity sha512-dAld12vtwdz9Rz01nOjmnXe+vHana5PSog8t0XGgLemKsUVsaupYpr74AHaS3s78SaTS5s2HOghnJF+jn91ZrA==
|
integrity sha512-aYn8bQwt1xwR2oSsVNHT4PXU7EhsThIwmpNB/MNUaaMx5OPLTro6VdNJe/sJssXFLxhamfWeMjwmpXjljo6xkg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@prisma/engines" "3.10.0-50.73e60b76d394f8d37d8ebd1f8918c79029f0db86"
|
"@prisma/engines" "3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9"
|
||||||
|
|
||||||
prismjs@^1.21.0, prismjs@~1.24.0:
|
prismjs@^1.21.0, prismjs@~1.24.0:
|
||||||
version "1.24.1"
|
version "1.24.1"
|
||||||
@ -18836,10 +18836,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.2.0:
|
yahoo-finance2@2.3.0:
|
||||||
version "2.2.0"
|
version "2.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/yahoo-finance2/-/yahoo-finance2-2.2.0.tgz#8694b04e69f4a79996812b6d082e5b738c51cee6"
|
resolved "https://registry.yarnpkg.com/yahoo-finance2/-/yahoo-finance2-2.3.0.tgz#81bd76732dfd38aa5d7019a97caf0f938c0127c2"
|
||||||
integrity sha512-ZxLCcoh+J51F7Tol1jpVBmy50IBQSoxsECWYDToBxjZwPloFNHtEVOXNqJlyzTysnzVbPA5TeCNT6G0DoaJnNQ==
|
integrity sha512-7oj8n/WJH9MtX+q99WbHdjEVPdobTX8IyYjg7v4sDOh4f9ByT2Frxmp+Uj+rctrO0EiiD9QWTuwV4h8AemGuCg==
|
||||||
dependencies:
|
dependencies:
|
||||||
ajv "8.10.0"
|
ajv "8.10.0"
|
||||||
ajv-formats "2.1.1"
|
ajv-formats "2.1.1"
|
||||||
|
Reference in New Issue
Block a user