Compare commits
44 Commits
Author | SHA1 | Date | |
---|---|---|---|
d735e4db75 | |||
e10707fde4 | |||
ac953df809 | |||
bb4ee50738 | |||
4f41bac328 | |||
cd07802400 | |||
b692b7432c | |||
a4efbc0131 | |||
55b0fe232c | |||
46432edce9 | |||
990028316e | |||
37871fbabc | |||
0fdeef7953 | |||
cdbe6eedeb | |||
a6dde8ad43 | |||
39bd4a349b | |||
ab59eb5c92 | |||
1132dc9bdd | |||
2d70b18593 | |||
b6ea7d23fa | |||
95382581f1 | |||
551b83a6e3 | |||
895c4fe299 | |||
ccb1bf881e | |||
7788b5a987 | |||
47fd029e0c | |||
458ee159e1 | |||
dfacbed66d | |||
22d63c6102 | |||
212aa6a63b | |||
bed9ae916c | |||
b6ad362850 | |||
ab86dd5318 | |||
dba73d80a3 | |||
92fb05320a | |||
73d62bb51f | |||
127b7d4f25 | |||
e79d607ab8 | |||
5f7d083f7c | |||
15857118fe | |||
ff91ed21df | |||
9241c04d5a | |||
5d4e2fba8c | |||
6c57609db8 |
75
CHANGELOG.md
75
CHANGELOG.md
@ -5,6 +5,81 @@ 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).
|
||||||
|
|
||||||
|
## 2.77.1 - 2024-04-27
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Extended the content of the _Self-Hosting_ section by the custom asset instructions on the Frequently Asked Questions (FAQ) page
|
||||||
|
- Added the caching to the portfolio calculator (experimental)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Migrated the `@ghostfolio/ui` components to control flow
|
||||||
|
- Updated the browserslist database
|
||||||
|
- Upgraded `prisma` from version `5.12.1` to `5.13.0`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the form submit in the asset profile details dialog of the admin control due to the `url` validation
|
||||||
|
- Fixed the historical market data gathering for asset profiles with `MANUAL` data source
|
||||||
|
|
||||||
|
## 2.76.0 - 2024-04-23
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Changed `CASH` to `LIQUIDITY` in the asset class enum
|
||||||
|
|
||||||
|
## 2.75.1 - 2024-04-21
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added `accountId` and `date` as a unique constraint to the `AccountBalance` database schema
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the chart in the account detail dialog
|
||||||
|
- Improved the account balance management
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue with `totalValueInBaseCurrency` in the value redaction interceptor for the impersonation mode
|
||||||
|
|
||||||
|
## 2.74.0 - 2024-04-20
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the date range support to the portfolio holdings page
|
||||||
|
- Added support to create an account balance
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Removed the date range support in the activities table on the portfolio activities page (experimental)
|
||||||
|
- Improved the language localization for German (`de`)
|
||||||
|
- Upgraded `angular` from version `17.3.3` to `17.3.5`
|
||||||
|
- Upgraded `Nx` from version `18.2.3` to `18.3.3`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed gaps in the portfolio performance charts by considering `BUY` and `SELL` activities
|
||||||
|
|
||||||
|
## 2.73.0 - 2024-04-17
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added a form validation against the DTO in the create or update account dialog
|
||||||
|
- Added a form validation against the DTO in the create or update activity dialog
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Moved the dividend calculations into the portfolio calculator
|
||||||
|
- Moved the fee calculations into the portfolio calculator
|
||||||
|
- Moved the interest calculations into the portfolio calculator
|
||||||
|
- Moved the liability calculations into the portfolio calculator
|
||||||
|
- Moved the (wealth) item calculations into the portfolio calculator
|
||||||
|
- Let queue jobs for asset profile data gathering fail by throwing an error
|
||||||
|
- Let queue jobs for historical market data gathering fail by throwing an error
|
||||||
|
- Upgraded `yahoo-finance2` from version `2.11.1` to `2.11.2`
|
||||||
|
|
||||||
## 2.72.0 - 2024-04-13
|
## 2.72.0 - 2024-04-13
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -13,8 +13,6 @@
|
|||||||
[](#contributing)
|
[](#contributing)
|
||||||
[](https://www.gnu.org/licenses/agpl-3.0)
|
[](https://www.gnu.org/licenses/agpl-3.0)
|
||||||
|
|
||||||
New: [Ghostfolio 2.0](https://ghostfol.io/en/blog/2023/09/ghostfolio-2)
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
**Ghostfolio** is an open source wealth management software built with web technology. The application empowers busy people to keep track of stocks, ETFs or cryptocurrencies and make solid, data-driven investment decisions. The software is designed for personal use in continuous operation.
|
**Ghostfolio** is an open source wealth management software built with web technology. The application empowers busy people to keep track of stocks, ETFs or cryptocurrencies and make solid, data-driven investment decisions. The software is designed for personal use in continuous operation.
|
||||||
@ -144,7 +142,7 @@ docker compose --env-file ./.env -f docker/docker-compose.build.yml up -d
|
|||||||
|
|
||||||
### Home Server Systems (Community)
|
### Home Server Systems (Community)
|
||||||
|
|
||||||
Ghostfolio is available for various home server systems, including [CasaOS](https://github.com/bigbeartechworld/big-bear-casaos), [Runtipi](https://www.runtipi.io/docs/apps-available), [TrueCharts](https://truecharts.org/charts/stable/ghostfolio), [Umbrel](https://apps.umbrel.com/app/ghostfolio), and [Unraid](https://unraid.net/community/apps?q=ghostfolio).
|
Ghostfolio is available for various home server systems, including [CasaOS](https://github.com/bigbeartechworld/big-bear-casaos), Home Assistant, [Runtipi](https://www.runtipi.io/docs/apps-available), [TrueCharts](https://truecharts.org/charts/stable/ghostfolio), [Umbrel](https://apps.umbrel.com/app/ghostfolio), and [Unraid](https://unraid.net/community/apps?q=ghostfolio).
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||||
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
||||||
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
||||||
import { permissions } from '@ghostfolio/common/permissions';
|
import { permissions } from '@ghostfolio/common/permissions';
|
||||||
@ -5,6 +6,8 @@ import type { RequestWithUser } from '@ghostfolio/common/types';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
|
Body,
|
||||||
|
Post,
|
||||||
Delete,
|
Delete,
|
||||||
HttpException,
|
HttpException,
|
||||||
Inject,
|
Inject,
|
||||||
@ -17,14 +20,44 @@ import { AccountBalance } from '@prisma/client';
|
|||||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
|
||||||
import { AccountBalanceService } from './account-balance.service';
|
import { AccountBalanceService } from './account-balance.service';
|
||||||
|
import { CreateAccountBalanceDto } from './create-account-balance.dto';
|
||||||
|
|
||||||
@Controller('account-balance')
|
@Controller('account-balance')
|
||||||
export class AccountBalanceController {
|
export class AccountBalanceController {
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly accountBalanceService: AccountBalanceService,
|
private readonly accountBalanceService: AccountBalanceService,
|
||||||
|
private readonly accountService: AccountService,
|
||||||
@Inject(REQUEST) private readonly request: RequestWithUser
|
@Inject(REQUEST) private readonly request: RequestWithUser
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@HasPermission(permissions.createAccountBalance)
|
||||||
|
@Post()
|
||||||
|
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
||||||
|
public async createAccountBalance(
|
||||||
|
@Body() data: CreateAccountBalanceDto
|
||||||
|
): Promise<AccountBalance> {
|
||||||
|
const account = await this.accountService.account({
|
||||||
|
id_userId: {
|
||||||
|
id: data.accountId,
|
||||||
|
userId: this.request.user.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
|
StatusCodes.FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.accountBalanceService.createOrUpdateAccountBalance({
|
||||||
|
accountId: account.id,
|
||||||
|
balance: data.balance,
|
||||||
|
date: data.date,
|
||||||
|
userId: account.userId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@HasPermission(permissions.deleteAccountBalance)
|
@HasPermission(permissions.deleteAccountBalance)
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
||||||
@ -32,10 +65,11 @@ export class AccountBalanceController {
|
|||||||
@Param('id') id: string
|
@Param('id') id: string
|
||||||
): Promise<AccountBalance> {
|
): Promise<AccountBalance> {
|
||||||
const accountBalance = await this.accountBalanceService.accountBalance({
|
const accountBalance = await this.accountBalanceService.accountBalance({
|
||||||
id
|
id,
|
||||||
|
userId: this.request.user.id
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!accountBalance || accountBalance.userId !== this.request.user.id) {
|
if (!accountBalance) {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
getReasonPhrase(StatusCodes.FORBIDDEN),
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
StatusCodes.FORBIDDEN
|
StatusCodes.FORBIDDEN
|
||||||
@ -43,7 +77,8 @@ export class AccountBalanceController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return this.accountBalanceService.deleteAccountBalance({
|
return this.accountBalanceService.deleteAccountBalance({
|
||||||
id
|
id: accountBalance.id,
|
||||||
|
userId: accountBalance.userId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||||
|
|
||||||
@ -10,6 +11,6 @@ import { AccountBalanceService } from './account-balance.service';
|
|||||||
controllers: [AccountBalanceController],
|
controllers: [AccountBalanceController],
|
||||||
exports: [AccountBalanceService],
|
exports: [AccountBalanceService],
|
||||||
imports: [ExchangeRateDataModule, PrismaModule],
|
imports: [ExchangeRateDataModule, PrismaModule],
|
||||||
providers: [AccountBalanceService]
|
providers: [AccountBalanceService, AccountService]
|
||||||
})
|
})
|
||||||
export class AccountBalanceModule {}
|
export class AccountBalanceModule {}
|
||||||
|
@ -1,14 +1,21 @@
|
|||||||
|
import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||||
|
import { resetHours } from '@ghostfolio/common/helper';
|
||||||
import { AccountBalancesResponse, Filter } from '@ghostfolio/common/interfaces';
|
import { AccountBalancesResponse, Filter } from '@ghostfolio/common/interfaces';
|
||||||
import { UserWithSettings } from '@ghostfolio/common/types';
|
import { UserWithSettings } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
import { AccountBalance, Prisma } from '@prisma/client';
|
import { AccountBalance, Prisma } from '@prisma/client';
|
||||||
|
import { parseISO } from 'date-fns';
|
||||||
|
|
||||||
|
import { CreateAccountBalanceDto } from './create-account-balance.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AccountBalanceService {
|
export class AccountBalanceService {
|
||||||
public constructor(
|
public constructor(
|
||||||
|
private readonly eventEmitter: EventEmitter2,
|
||||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||||
private readonly prismaService: PrismaService
|
private readonly prismaService: PrismaService
|
||||||
) {}
|
) {}
|
||||||
@ -24,20 +31,63 @@ export class AccountBalanceService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createAccountBalance(
|
public async createOrUpdateAccountBalance({
|
||||||
data: Prisma.AccountBalanceCreateInput
|
accountId,
|
||||||
): Promise<AccountBalance> {
|
balance,
|
||||||
return this.prismaService.accountBalance.create({
|
date,
|
||||||
data
|
userId
|
||||||
|
}: CreateAccountBalanceDto & {
|
||||||
|
userId: string;
|
||||||
|
}): Promise<AccountBalance> {
|
||||||
|
const accountBalance = await this.prismaService.accountBalance.upsert({
|
||||||
|
create: {
|
||||||
|
Account: {
|
||||||
|
connect: {
|
||||||
|
id_userId: {
|
||||||
|
userId,
|
||||||
|
id: accountId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
date: resetHours(parseISO(date)),
|
||||||
|
value: balance
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
value: balance
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
accountId_date: {
|
||||||
|
accountId,
|
||||||
|
date: resetHours(parseISO(date))
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.eventEmitter.emit(
|
||||||
|
PortfolioChangedEvent.getName(),
|
||||||
|
new PortfolioChangedEvent({
|
||||||
|
userId
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return accountBalance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteAccountBalance(
|
public async deleteAccountBalance(
|
||||||
where: Prisma.AccountBalanceWhereUniqueInput
|
where: Prisma.AccountBalanceWhereUniqueInput
|
||||||
): Promise<AccountBalance> {
|
): Promise<AccountBalance> {
|
||||||
return this.prismaService.accountBalance.delete({
|
const accountBalance = await this.prismaService.accountBalance.delete({
|
||||||
where
|
where
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.eventEmitter.emit(
|
||||||
|
PortfolioChangedEvent.getName(),
|
||||||
|
new PortfolioChangedEvent({
|
||||||
|
userId: <string>where.userId
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return accountBalance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAccountBalances({
|
public async getAccountBalances({
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
import { IsISO8601, IsNumber, IsUUID } from 'class-validator';
|
||||||
|
|
||||||
|
export class CreateAccountBalanceDto {
|
||||||
|
@IsUUID()
|
||||||
|
accountId: string;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
balance: number;
|
||||||
|
|
||||||
|
@IsISO8601()
|
||||||
|
date: string;
|
||||||
|
}
|
@ -1,11 +1,15 @@
|
|||||||
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
|
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
|
||||||
|
import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||||
|
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||||
import { Filter } from '@ghostfolio/common/interfaces';
|
import { Filter } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
import { Account, Order, Platform, Prisma } from '@prisma/client';
|
import { Account, Order, Platform, Prisma } from '@prisma/client';
|
||||||
import { Big } from 'big.js';
|
import { Big } from 'big.js';
|
||||||
|
import { format } from 'date-fns';
|
||||||
import { groupBy } from 'lodash';
|
import { groupBy } from 'lodash';
|
||||||
|
|
||||||
import { CashDetails } from './interfaces/cash-details.interface';
|
import { CashDetails } from './interfaces/cash-details.interface';
|
||||||
@ -14,6 +18,7 @@ import { CashDetails } from './interfaces/cash-details.interface';
|
|||||||
export class AccountService {
|
export class AccountService {
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly accountBalanceService: AccountBalanceService,
|
private readonly accountBalanceService: AccountBalanceService,
|
||||||
|
private readonly eventEmitter: EventEmitter2,
|
||||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||||
private readonly prismaService: PrismaService
|
private readonly prismaService: PrismaService
|
||||||
) {}
|
) {}
|
||||||
@ -85,17 +90,20 @@ export class AccountService {
|
|||||||
data
|
data
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.prismaService.accountBalance.create({
|
await this.accountBalanceService.createOrUpdateAccountBalance({
|
||||||
data: {
|
accountId: account.id,
|
||||||
Account: {
|
balance: data.balance,
|
||||||
connect: {
|
date: format(new Date(), DATE_FORMAT),
|
||||||
id_userId: { id: account.id, userId: aUserId }
|
userId: aUserId
|
||||||
}
|
|
||||||
},
|
|
||||||
value: data.balance
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.eventEmitter.emit(
|
||||||
|
PortfolioChangedEvent.getName(),
|
||||||
|
new PortfolioChangedEvent({
|
||||||
|
userId: account.userId
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return account;
|
return account;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,9 +111,18 @@ export class AccountService {
|
|||||||
where: Prisma.AccountWhereUniqueInput,
|
where: Prisma.AccountWhereUniqueInput,
|
||||||
aUserId: string
|
aUserId: string
|
||||||
): Promise<Account> {
|
): Promise<Account> {
|
||||||
return this.prismaService.account.delete({
|
const account = await this.prismaService.account.delete({
|
||||||
where
|
where
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.eventEmitter.emit(
|
||||||
|
PortfolioChangedEvent.getName(),
|
||||||
|
new PortfolioChangedEvent({
|
||||||
|
userId: account.userId
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return account;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAccounts(aUserId: string): Promise<Account[]> {
|
public async getAccounts(aUserId: string): Promise<Account[]> {
|
||||||
@ -196,21 +213,26 @@ export class AccountService {
|
|||||||
): Promise<Account> {
|
): Promise<Account> {
|
||||||
const { data, where } = params;
|
const { data, where } = params;
|
||||||
|
|
||||||
await this.prismaService.accountBalance.create({
|
await this.accountBalanceService.createOrUpdateAccountBalance({
|
||||||
data: {
|
accountId: <string>data.id,
|
||||||
Account: {
|
balance: <number>data.balance,
|
||||||
connect: {
|
date: format(new Date(), DATE_FORMAT),
|
||||||
id_userId: where.id_userId
|
userId: aUserId
|
||||||
}
|
|
||||||
},
|
|
||||||
value: <number>data.balance
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.prismaService.account.update({
|
const account = await this.prismaService.account.update({
|
||||||
data,
|
data,
|
||||||
where
|
where
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.eventEmitter.emit(
|
||||||
|
PortfolioChangedEvent.getName(),
|
||||||
|
new PortfolioChangedEvent({
|
||||||
|
userId: account.userId
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return account;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateAccountBalance({
|
public async updateAccountBalance({
|
||||||
@ -242,17 +264,11 @@ export class AccountService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (amountInCurrencyOfAccount) {
|
if (amountInCurrencyOfAccount) {
|
||||||
await this.accountBalanceService.createAccountBalance({
|
await this.accountBalanceService.createOrUpdateAccountBalance({
|
||||||
date,
|
accountId,
|
||||||
Account: {
|
userId,
|
||||||
connect: {
|
balance: new Big(balance).plus(amountInCurrencyOfAccount).toNumber(),
|
||||||
id_userId: {
|
date: date.toISOString()
|
||||||
userId,
|
|
||||||
id: accountId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
value: new Big(balance).plus(amountInCurrencyOfAccount).toNumber()
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -416,7 +416,7 @@ export class AdminService {
|
|||||||
dataSource,
|
dataSource,
|
||||||
marketDataItemCount,
|
marketDataItemCount,
|
||||||
symbol,
|
symbol,
|
||||||
assetClass: 'CASH',
|
assetClass: AssetClass.LIQUIDITY,
|
||||||
countriesCount: 0,
|
countriesCount: 0,
|
||||||
currency: symbol.replace(DEFAULT_CURRENCY, ''),
|
currency: symbol.replace(DEFAULT_CURRENCY, ''),
|
||||||
id: undefined,
|
id: undefined,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { EventsModule } from '@ghostfolio/api/events/events.module';
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||||
import { CronService } from '@ghostfolio/api/services/cron.service';
|
import { CronService } from '@ghostfolio/api/services/cron.service';
|
||||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
||||||
@ -14,6 +15,7 @@ import {
|
|||||||
import { BullModule } from '@nestjs/bull';
|
import { BullModule } from '@nestjs/bull';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||||
import { ScheduleModule } from '@nestjs/schedule';
|
import { ScheduleModule } from '@nestjs/schedule';
|
||||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||||
import { StatusCodes } from 'http-status-codes';
|
import { StatusCodes } from 'http-status-codes';
|
||||||
@ -44,6 +46,7 @@ import { TagModule } from './tag/tag.module';
|
|||||||
import { UserModule } from './user/user.module';
|
import { UserModule } from './user/user.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
controllers: [AppController],
|
||||||
imports: [
|
imports: [
|
||||||
AdminModule,
|
AdminModule,
|
||||||
AccessModule,
|
AccessModule,
|
||||||
@ -64,6 +67,8 @@ import { UserModule } from './user/user.module';
|
|||||||
ConfigurationModule,
|
ConfigurationModule,
|
||||||
DataGatheringModule,
|
DataGatheringModule,
|
||||||
DataProviderModule,
|
DataProviderModule,
|
||||||
|
EventEmitterModule.forRoot(),
|
||||||
|
EventsModule,
|
||||||
ExchangeRateModule,
|
ExchangeRateModule,
|
||||||
ExchangeRateDataModule,
|
ExchangeRateDataModule,
|
||||||
ExportModule,
|
ExportModule,
|
||||||
@ -109,7 +114,6 @@ import { UserModule } from './user/user.module';
|
|||||||
TwitterBotModule,
|
TwitterBotModule,
|
||||||
UserModule
|
UserModule
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
|
||||||
providers: [CronService]
|
providers: [CronService]
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
@ -416,6 +416,11 @@ export class ImportService {
|
|||||||
User: { connect: { id: user.id } },
|
User: { connect: { id: user.id } },
|
||||||
userId: user.id
|
userId: user.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (order.SymbolProfile?.symbol) {
|
||||||
|
// Update symbol that may have been assigned in createOrder()
|
||||||
|
assetProfile.symbol = order.SymbolProfile.symbol;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = new Big(quantity).mul(unitPrice).toNumber();
|
const value = new Big(quantity).mul(unitPrice).toNumber();
|
||||||
|
@ -60,15 +60,15 @@ export class OrderController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
|
@HasPermission(permissions.deleteOrder)
|
||||||
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
||||||
public async deleteOrder(@Param('id') id: string): Promise<OrderModel> {
|
public async deleteOrder(@Param('id') id: string): Promise<OrderModel> {
|
||||||
const order = await this.orderService.order({ id });
|
const order = await this.orderService.order({
|
||||||
|
id,
|
||||||
|
userId: this.request.user.id
|
||||||
|
});
|
||||||
|
|
||||||
if (
|
if (!order) {
|
||||||
!hasPermission(this.request.user.permissions, permissions.deleteOrder) ||
|
|
||||||
!order ||
|
|
||||||
order.userId !== this.request.user.id
|
|
||||||
) {
|
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
getReasonPhrase(StatusCodes.FORBIDDEN),
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
StatusCodes.FORBIDDEN
|
StatusCodes.FORBIDDEN
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||||
|
import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event';
|
||||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service';
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||||
@ -13,6 +14,7 @@ import { Filter, UniqueAsset } from '@ghostfolio/common/interfaces';
|
|||||||
import { OrderWithAccount } from '@ghostfolio/common/types';
|
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
import {
|
import {
|
||||||
AssetClass,
|
AssetClass,
|
||||||
AssetSubClass,
|
AssetSubClass,
|
||||||
@ -27,7 +29,6 @@ import { endOfToday, isAfter } from 'date-fns';
|
|||||||
import { groupBy, uniqBy } from 'lodash';
|
import { groupBy, uniqBy } from 'lodash';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import { CreateOrderDto } from './create-order.dto';
|
|
||||||
import { Activities } from './interfaces/activities.interface';
|
import { Activities } from './interfaces/activities.interface';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -35,6 +36,7 @@ export class OrderService {
|
|||||||
public constructor(
|
public constructor(
|
||||||
private readonly accountService: AccountService,
|
private readonly accountService: AccountService,
|
||||||
private readonly dataGatheringService: DataGatheringService,
|
private readonly dataGatheringService: DataGatheringService,
|
||||||
|
private readonly eventEmitter: EventEmitter2,
|
||||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||||
private readonly prismaService: PrismaService,
|
private readonly prismaService: PrismaService,
|
||||||
private readonly symbolProfileService: SymbolProfileService
|
private readonly symbolProfileService: SymbolProfileService
|
||||||
@ -138,7 +140,8 @@ export class OrderService {
|
|||||||
return { id };
|
return { id };
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
include: { SymbolProfile: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (updateAccountBalance === true) {
|
if (updateAccountBalance === true) {
|
||||||
@ -160,6 +163,13 @@ export class OrderService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.eventEmitter.emit(
|
||||||
|
PortfolioChangedEvent.getName(),
|
||||||
|
new PortfolioChangedEvent({
|
||||||
|
userId: order.userId
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return order;
|
return order;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,6 +184,13 @@ export class OrderService {
|
|||||||
await this.symbolProfileService.deleteById(order.symbolProfileId);
|
await this.symbolProfileService.deleteById(order.symbolProfileId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.eventEmitter.emit(
|
||||||
|
PortfolioChangedEvent.getName(),
|
||||||
|
new PortfolioChangedEvent({
|
||||||
|
userId: order.userId
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return order;
|
return order;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,6 +199,13 @@ export class OrderService {
|
|||||||
where
|
where
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.eventEmitter.emit(
|
||||||
|
PortfolioChangedEvent.getName(),
|
||||||
|
new PortfolioChangedEvent({
|
||||||
|
userId: <string>where.userId
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -455,7 +479,7 @@ export class OrderService {
|
|||||||
where
|
where
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.prismaService.order.update({
|
const order = await this.prismaService.order.update({
|
||||||
data: {
|
data: {
|
||||||
...data,
|
...data,
|
||||||
isDraft,
|
isDraft,
|
||||||
@ -467,6 +491,15 @@ export class OrderService {
|
|||||||
},
|
},
|
||||||
where
|
where
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.eventEmitter.emit(
|
||||||
|
PortfolioChangedEvent.getName(),
|
||||||
|
new PortfolioChangedEvent({
|
||||||
|
userId: order.userId
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return order;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async orders(params: {
|
private async orders(params: {
|
||||||
|
@ -1,15 +1,11 @@
|
|||||||
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
|
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
|
||||||
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
|
import { SymbolMetrics, UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
import {
|
import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models';
|
||||||
SymbolMetrics,
|
|
||||||
TimelinePosition,
|
|
||||||
UniqueAsset
|
|
||||||
} from '@ghostfolio/common/interfaces';
|
|
||||||
|
|
||||||
export class MWRPortfolioCalculator extends PortfolioCalculator {
|
export class MWRPortfolioCalculator extends PortfolioCalculator {
|
||||||
protected calculateOverallPerformance(
|
protected calculateOverallPerformance(
|
||||||
positions: TimelinePosition[]
|
positions: TimelinePosition[]
|
||||||
): CurrentPositions {
|
): PortfolioSnapshot {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,3 +24,7 @@ export const symbolProfileDummyData = {
|
|||||||
sectors: [],
|
sectors: [],
|
||||||
updatedAt: undefined
|
updatedAt: undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const userDummyData = {
|
||||||
|
id: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
|
||||||
|
};
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
|
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
|
import { HistoricalDataItem } from '@ghostfolio/common/interfaces';
|
||||||
|
import { DateRange, UserWithSettings } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
@ -16,33 +20,55 @@ export enum PerformanceCalculationType {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class PortfolioCalculatorFactory {
|
export class PortfolioCalculatorFactory {
|
||||||
public constructor(
|
public constructor(
|
||||||
|
private readonly configurationService: ConfigurationService,
|
||||||
private readonly currentRateService: CurrentRateService,
|
private readonly currentRateService: CurrentRateService,
|
||||||
private readonly exchangeRateDataService: ExchangeRateDataService
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||||
|
private readonly redisCacheService: RedisCacheService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public createCalculator({
|
public createCalculator({
|
||||||
|
accountBalanceItems = [],
|
||||||
activities,
|
activities,
|
||||||
calculationType,
|
calculationType,
|
||||||
currency
|
currency,
|
||||||
|
dateRange = 'max',
|
||||||
|
isExperimentalFeatures = false,
|
||||||
|
userId
|
||||||
}: {
|
}: {
|
||||||
|
accountBalanceItems?: HistoricalDataItem[];
|
||||||
activities: Activity[];
|
activities: Activity[];
|
||||||
calculationType: PerformanceCalculationType;
|
calculationType: PerformanceCalculationType;
|
||||||
currency: string;
|
currency: string;
|
||||||
|
dateRange?: DateRange;
|
||||||
|
isExperimentalFeatures?: boolean;
|
||||||
|
userId: string;
|
||||||
}): PortfolioCalculator {
|
}): PortfolioCalculator {
|
||||||
switch (calculationType) {
|
switch (calculationType) {
|
||||||
case PerformanceCalculationType.MWR:
|
case PerformanceCalculationType.MWR:
|
||||||
return new MWRPortfolioCalculator({
|
return new MWRPortfolioCalculator({
|
||||||
|
accountBalanceItems,
|
||||||
activities,
|
activities,
|
||||||
currency,
|
currency,
|
||||||
|
dateRange,
|
||||||
|
isExperimentalFeatures,
|
||||||
|
userId,
|
||||||
|
configurationService: this.configurationService,
|
||||||
currentRateService: this.currentRateService,
|
currentRateService: this.currentRateService,
|
||||||
exchangeRateDataService: this.exchangeRateDataService
|
exchangeRateDataService: this.exchangeRateDataService,
|
||||||
|
redisCacheService: this.redisCacheService
|
||||||
});
|
});
|
||||||
case PerformanceCalculationType.TWR:
|
case PerformanceCalculationType.TWR:
|
||||||
return new TWRPortfolioCalculator({
|
return new TWRPortfolioCalculator({
|
||||||
|
accountBalanceItems,
|
||||||
activities,
|
activities,
|
||||||
currency,
|
currency,
|
||||||
currentRateService: this.currentRateService,
|
currentRateService: this.currentRateService,
|
||||||
exchangeRateDataService: this.exchangeRateDataService
|
dateRange,
|
||||||
|
isExperimentalFeatures,
|
||||||
|
userId,
|
||||||
|
configurationService: this.configurationService,
|
||||||
|
exchangeRateDataService: this.exchangeRateDataService,
|
||||||
|
redisCacheService: this.redisCacheService
|
||||||
});
|
});
|
||||||
default:
|
default:
|
||||||
throw new Error('Invalid calculation type');
|
throw new Error('Invalid calculation type');
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,8 @@
|
|||||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
import {
|
import {
|
||||||
activityDummyData,
|
activityDummyData,
|
||||||
symbolProfileDummyData
|
symbolProfileDummyData,
|
||||||
|
userDummyData
|
||||||
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
|
||||||
import {
|
import {
|
||||||
PortfolioCalculatorFactory,
|
PortfolioCalculatorFactory,
|
||||||
@ -9,6 +10,9 @@ import {
|
|||||||
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
||||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
||||||
|
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
||||||
|
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
|
||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { parseDate } from '@ghostfolio/common/helper';
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
|
|
||||||
@ -23,12 +27,25 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
RedisCacheService: jest.fn().mockImplementation(() => {
|
||||||
|
return RedisCacheServiceMock;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
describe('PortfolioCalculator', () => {
|
describe('PortfolioCalculator', () => {
|
||||||
|
let configurationService: ConfigurationService;
|
||||||
let currentRateService: CurrentRateService;
|
let currentRateService: CurrentRateService;
|
||||||
let exchangeRateDataService: ExchangeRateDataService;
|
let exchangeRateDataService: ExchangeRateDataService;
|
||||||
let factory: PortfolioCalculatorFactory;
|
let factory: PortfolioCalculatorFactory;
|
||||||
|
let redisCacheService: RedisCacheService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
configurationService = new ConfigurationService();
|
||||||
|
|
||||||
currentRateService = new CurrentRateService(null, null, null, null);
|
currentRateService = new CurrentRateService(null, null, null, null);
|
||||||
|
|
||||||
exchangeRateDataService = new ExchangeRateDataService(
|
exchangeRateDataService = new ExchangeRateDataService(
|
||||||
@ -38,14 +55,22 @@ describe('PortfolioCalculator', () => {
|
|||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
redisCacheService = new RedisCacheService(null, null);
|
||||||
|
|
||||||
factory = new PortfolioCalculatorFactory(
|
factory = new PortfolioCalculatorFactory(
|
||||||
|
configurationService,
|
||||||
currentRateService,
|
currentRateService,
|
||||||
exchangeRateDataService
|
exchangeRateDataService,
|
||||||
|
redisCacheService
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('get current positions', () => {
|
describe('get current positions', () => {
|
||||||
it.only('with BALN.SW buy and sell in two activities', async () => {
|
it.only('with BALN.SW buy and sell in two activities', async () => {
|
||||||
|
const spy = jest
|
||||||
|
.spyOn(Date, 'now')
|
||||||
|
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
||||||
|
|
||||||
const activities: Activity[] = [
|
const activities: Activity[] = [
|
||||||
{
|
{
|
||||||
...activityDummyData,
|
...activityDummyData,
|
||||||
@ -97,18 +122,15 @@ describe('PortfolioCalculator', () => {
|
|||||||
const portfolioCalculator = factory.createCalculator({
|
const portfolioCalculator = factory.createCalculator({
|
||||||
activities,
|
activities,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'CHF'
|
currency: 'CHF',
|
||||||
|
userId: userDummyData.id
|
||||||
});
|
});
|
||||||
|
|
||||||
const spy = jest
|
|
||||||
.spyOn(Date, 'now')
|
|
||||||
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
|
||||||
|
|
||||||
const chartData = await portfolioCalculator.getChartData({
|
const chartData = await portfolioCalculator.getChartData({
|
||||||
start: parseDate('2021-11-22')
|
start: parseDate('2021-11-22')
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
|
||||||
parseDate('2021-11-22')
|
parseDate('2021-11-22')
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -121,7 +143,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
|
|
||||||
spy.mockRestore();
|
spy.mockRestore();
|
||||||
|
|
||||||
expect(currentPositions).toEqual({
|
expect(portfolioSnapshot).toEqual({
|
||||||
currentValueInBaseCurrency: new Big('0'),
|
currentValueInBaseCurrency: new Big('0'),
|
||||||
errors: [],
|
errors: [],
|
||||||
grossPerformance: new Big('-12.6'),
|
grossPerformance: new Big('-12.6'),
|
||||||
@ -173,8 +195,12 @@ describe('PortfolioCalculator', () => {
|
|||||||
valueInBaseCurrency: new Big('0')
|
valueInBaseCurrency: new Big('0')
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
totalFeesWithCurrencyEffect: new Big('3.2'),
|
||||||
|
totalInterestWithCurrencyEffect: new Big('0'),
|
||||||
totalInvestment: new Big('0'),
|
totalInvestment: new Big('0'),
|
||||||
totalInvestmentWithCurrencyEffect: new Big('0')
|
totalInvestmentWithCurrencyEffect: new Big('0'),
|
||||||
|
totalLiabilitiesWithCurrencyEffect: new Big('0'),
|
||||||
|
totalValuablesWithCurrencyEffect: new Big('0')
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(investments).toEqual([
|
expect(investments).toEqual([
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
import {
|
import {
|
||||||
activityDummyData,
|
activityDummyData,
|
||||||
symbolProfileDummyData
|
symbolProfileDummyData,
|
||||||
|
userDummyData
|
||||||
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
|
||||||
import {
|
import {
|
||||||
PerformanceCalculationType,
|
PerformanceCalculationType,
|
||||||
@ -9,6 +10,9 @@ import {
|
|||||||
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
||||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
||||||
|
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
||||||
|
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
|
||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { parseDate } from '@ghostfolio/common/helper';
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
|
|
||||||
@ -23,12 +27,25 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
RedisCacheService: jest.fn().mockImplementation(() => {
|
||||||
|
return RedisCacheServiceMock;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
describe('PortfolioCalculator', () => {
|
describe('PortfolioCalculator', () => {
|
||||||
|
let configurationService: ConfigurationService;
|
||||||
let currentRateService: CurrentRateService;
|
let currentRateService: CurrentRateService;
|
||||||
let exchangeRateDataService: ExchangeRateDataService;
|
let exchangeRateDataService: ExchangeRateDataService;
|
||||||
let factory: PortfolioCalculatorFactory;
|
let factory: PortfolioCalculatorFactory;
|
||||||
|
let redisCacheService: RedisCacheService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
configurationService = new ConfigurationService();
|
||||||
|
|
||||||
currentRateService = new CurrentRateService(null, null, null, null);
|
currentRateService = new CurrentRateService(null, null, null, null);
|
||||||
|
|
||||||
exchangeRateDataService = new ExchangeRateDataService(
|
exchangeRateDataService = new ExchangeRateDataService(
|
||||||
@ -38,14 +55,22 @@ describe('PortfolioCalculator', () => {
|
|||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
redisCacheService = new RedisCacheService(null, null);
|
||||||
|
|
||||||
factory = new PortfolioCalculatorFactory(
|
factory = new PortfolioCalculatorFactory(
|
||||||
|
configurationService,
|
||||||
currentRateService,
|
currentRateService,
|
||||||
exchangeRateDataService
|
exchangeRateDataService,
|
||||||
|
redisCacheService
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('get current positions', () => {
|
describe('get current positions', () => {
|
||||||
it.only('with BALN.SW buy and sell', async () => {
|
it.only('with BALN.SW buy and sell', async () => {
|
||||||
|
const spy = jest
|
||||||
|
.spyOn(Date, 'now')
|
||||||
|
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
||||||
|
|
||||||
const activities: Activity[] = [
|
const activities: Activity[] = [
|
||||||
{
|
{
|
||||||
...activityDummyData,
|
...activityDummyData,
|
||||||
@ -82,18 +107,15 @@ describe('PortfolioCalculator', () => {
|
|||||||
const portfolioCalculator = factory.createCalculator({
|
const portfolioCalculator = factory.createCalculator({
|
||||||
activities,
|
activities,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'CHF'
|
currency: 'CHF',
|
||||||
|
userId: userDummyData.id
|
||||||
});
|
});
|
||||||
|
|
||||||
const spy = jest
|
|
||||||
.spyOn(Date, 'now')
|
|
||||||
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
|
||||||
|
|
||||||
const chartData = await portfolioCalculator.getChartData({
|
const chartData = await portfolioCalculator.getChartData({
|
||||||
start: parseDate('2021-11-22')
|
start: parseDate('2021-11-22')
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
|
||||||
parseDate('2021-11-22')
|
parseDate('2021-11-22')
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -106,7 +128,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
|
|
||||||
spy.mockRestore();
|
spy.mockRestore();
|
||||||
|
|
||||||
expect(currentPositions).toEqual({
|
expect(portfolioSnapshot).toEqual({
|
||||||
currentValueInBaseCurrency: new Big('0'),
|
currentValueInBaseCurrency: new Big('0'),
|
||||||
errors: [],
|
errors: [],
|
||||||
grossPerformance: new Big('-12.6'),
|
grossPerformance: new Big('-12.6'),
|
||||||
@ -156,8 +178,12 @@ describe('PortfolioCalculator', () => {
|
|||||||
valueInBaseCurrency: new Big('0')
|
valueInBaseCurrency: new Big('0')
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
totalFeesWithCurrencyEffect: new Big('3.2'),
|
||||||
|
totalInterestWithCurrencyEffect: new Big('0'),
|
||||||
totalInvestment: new Big('0'),
|
totalInvestment: new Big('0'),
|
||||||
totalInvestmentWithCurrencyEffect: new Big('0')
|
totalInvestmentWithCurrencyEffect: new Big('0'),
|
||||||
|
totalLiabilitiesWithCurrencyEffect: new Big('0'),
|
||||||
|
totalValuablesWithCurrencyEffect: new Big('0')
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(investments).toEqual([
|
expect(investments).toEqual([
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
import {
|
import {
|
||||||
activityDummyData,
|
activityDummyData,
|
||||||
symbolProfileDummyData
|
symbolProfileDummyData,
|
||||||
|
userDummyData
|
||||||
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
|
||||||
import {
|
import {
|
||||||
PortfolioCalculatorFactory,
|
PortfolioCalculatorFactory,
|
||||||
@ -9,6 +10,9 @@ import {
|
|||||||
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
||||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
||||||
|
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
||||||
|
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
|
||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { parseDate } from '@ghostfolio/common/helper';
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
|
|
||||||
@ -23,12 +27,25 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
RedisCacheService: jest.fn().mockImplementation(() => {
|
||||||
|
return RedisCacheServiceMock;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
describe('PortfolioCalculator', () => {
|
describe('PortfolioCalculator', () => {
|
||||||
|
let configurationService: ConfigurationService;
|
||||||
let currentRateService: CurrentRateService;
|
let currentRateService: CurrentRateService;
|
||||||
let exchangeRateDataService: ExchangeRateDataService;
|
let exchangeRateDataService: ExchangeRateDataService;
|
||||||
let factory: PortfolioCalculatorFactory;
|
let factory: PortfolioCalculatorFactory;
|
||||||
|
let redisCacheService: RedisCacheService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
configurationService = new ConfigurationService();
|
||||||
|
|
||||||
currentRateService = new CurrentRateService(null, null, null, null);
|
currentRateService = new CurrentRateService(null, null, null, null);
|
||||||
|
|
||||||
exchangeRateDataService = new ExchangeRateDataService(
|
exchangeRateDataService = new ExchangeRateDataService(
|
||||||
@ -38,14 +55,22 @@ describe('PortfolioCalculator', () => {
|
|||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
redisCacheService = new RedisCacheService(null, null);
|
||||||
|
|
||||||
factory = new PortfolioCalculatorFactory(
|
factory = new PortfolioCalculatorFactory(
|
||||||
|
configurationService,
|
||||||
currentRateService,
|
currentRateService,
|
||||||
exchangeRateDataService
|
exchangeRateDataService,
|
||||||
|
redisCacheService
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('get current positions', () => {
|
describe('get current positions', () => {
|
||||||
it.only('with BALN.SW buy', async () => {
|
it.only('with BALN.SW buy', async () => {
|
||||||
|
const spy = jest
|
||||||
|
.spyOn(Date, 'now')
|
||||||
|
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
||||||
|
|
||||||
const activities: Activity[] = [
|
const activities: Activity[] = [
|
||||||
{
|
{
|
||||||
...activityDummyData,
|
...activityDummyData,
|
||||||
@ -67,18 +92,15 @@ describe('PortfolioCalculator', () => {
|
|||||||
const portfolioCalculator = factory.createCalculator({
|
const portfolioCalculator = factory.createCalculator({
|
||||||
activities,
|
activities,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'CHF'
|
currency: 'CHF',
|
||||||
|
userId: userDummyData.id
|
||||||
});
|
});
|
||||||
|
|
||||||
const spy = jest
|
|
||||||
.spyOn(Date, 'now')
|
|
||||||
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
|
||||||
|
|
||||||
const chartData = await portfolioCalculator.getChartData({
|
const chartData = await portfolioCalculator.getChartData({
|
||||||
start: parseDate('2021-11-30')
|
start: parseDate('2021-11-30')
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
|
||||||
parseDate('2021-11-30')
|
parseDate('2021-11-30')
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -91,7 +113,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
|
|
||||||
spy.mockRestore();
|
spy.mockRestore();
|
||||||
|
|
||||||
expect(currentPositions).toEqual({
|
expect(portfolioSnapshot).toEqual({
|
||||||
currentValueInBaseCurrency: new Big('297.8'),
|
currentValueInBaseCurrency: new Big('297.8'),
|
||||||
errors: [],
|
errors: [],
|
||||||
grossPerformance: new Big('24.6'),
|
grossPerformance: new Big('24.6'),
|
||||||
@ -141,8 +163,12 @@ describe('PortfolioCalculator', () => {
|
|||||||
valueInBaseCurrency: new Big('297.8')
|
valueInBaseCurrency: new Big('297.8')
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
totalFeesWithCurrencyEffect: new Big('1.55'),
|
||||||
|
totalInterestWithCurrencyEffect: new Big('0'),
|
||||||
totalInvestment: new Big('273.2'),
|
totalInvestment: new Big('273.2'),
|
||||||
totalInvestmentWithCurrencyEffect: new Big('273.2')
|
totalInvestmentWithCurrencyEffect: new Big('273.2'),
|
||||||
|
totalLiabilitiesWithCurrencyEffect: new Big('0'),
|
||||||
|
totalValuablesWithCurrencyEffect: new Big('0')
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(investments).toEqual([
|
expect(investments).toEqual([
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
import {
|
import {
|
||||||
activityDummyData,
|
activityDummyData,
|
||||||
symbolProfileDummyData
|
symbolProfileDummyData,
|
||||||
|
userDummyData
|
||||||
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
|
||||||
import {
|
import {
|
||||||
PortfolioCalculatorFactory,
|
PortfolioCalculatorFactory,
|
||||||
@ -9,6 +10,9 @@ import {
|
|||||||
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
||||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
||||||
|
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
||||||
|
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
|
||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock';
|
import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock';
|
||||||
import { parseDate } from '@ghostfolio/common/helper';
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
@ -24,6 +28,15 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
RedisCacheService: jest.fn().mockImplementation(() => {
|
||||||
|
return RedisCacheServiceMock;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
jest.mock(
|
jest.mock(
|
||||||
'@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service',
|
'@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service',
|
||||||
() => {
|
() => {
|
||||||
@ -37,11 +50,15 @@ jest.mock(
|
|||||||
);
|
);
|
||||||
|
|
||||||
describe('PortfolioCalculator', () => {
|
describe('PortfolioCalculator', () => {
|
||||||
|
let configurationService: ConfigurationService;
|
||||||
let currentRateService: CurrentRateService;
|
let currentRateService: CurrentRateService;
|
||||||
let exchangeRateDataService: ExchangeRateDataService;
|
let exchangeRateDataService: ExchangeRateDataService;
|
||||||
let factory: PortfolioCalculatorFactory;
|
let factory: PortfolioCalculatorFactory;
|
||||||
|
let redisCacheService: RedisCacheService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
configurationService = new ConfigurationService();
|
||||||
|
|
||||||
currentRateService = new CurrentRateService(null, null, null, null);
|
currentRateService = new CurrentRateService(null, null, null, null);
|
||||||
|
|
||||||
exchangeRateDataService = new ExchangeRateDataService(
|
exchangeRateDataService = new ExchangeRateDataService(
|
||||||
@ -51,14 +68,22 @@ describe('PortfolioCalculator', () => {
|
|||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
redisCacheService = new RedisCacheService(null, null);
|
||||||
|
|
||||||
factory = new PortfolioCalculatorFactory(
|
factory = new PortfolioCalculatorFactory(
|
||||||
|
configurationService,
|
||||||
currentRateService,
|
currentRateService,
|
||||||
exchangeRateDataService
|
exchangeRateDataService,
|
||||||
|
redisCacheService
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('get current positions', () => {
|
describe('get current positions', () => {
|
||||||
it.only('with BTCUSD buy and sell partially', async () => {
|
it.only('with BTCUSD buy and sell partially', async () => {
|
||||||
|
const spy = jest
|
||||||
|
.spyOn(Date, 'now')
|
||||||
|
.mockImplementation(() => parseDate('2018-01-01').getTime());
|
||||||
|
|
||||||
const activities: Activity[] = [
|
const activities: Activity[] = [
|
||||||
{
|
{
|
||||||
...activityDummyData,
|
...activityDummyData,
|
||||||
@ -95,18 +120,15 @@ describe('PortfolioCalculator', () => {
|
|||||||
const portfolioCalculator = factory.createCalculator({
|
const portfolioCalculator = factory.createCalculator({
|
||||||
activities,
|
activities,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'CHF'
|
currency: 'CHF',
|
||||||
|
userId: userDummyData.id
|
||||||
});
|
});
|
||||||
|
|
||||||
const spy = jest
|
|
||||||
.spyOn(Date, 'now')
|
|
||||||
.mockImplementation(() => parseDate('2018-01-01').getTime());
|
|
||||||
|
|
||||||
const chartData = await portfolioCalculator.getChartData({
|
const chartData = await portfolioCalculator.getChartData({
|
||||||
start: parseDate('2015-01-01')
|
start: parseDate('2015-01-01')
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
|
||||||
parseDate('2015-01-01')
|
parseDate('2015-01-01')
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -119,7 +141,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
|
|
||||||
spy.mockRestore();
|
spy.mockRestore();
|
||||||
|
|
||||||
expect(currentPositions).toEqual({
|
expect(portfolioSnapshot).toEqual({
|
||||||
currentValueInBaseCurrency: new Big('13298.425356'),
|
currentValueInBaseCurrency: new Big('13298.425356'),
|
||||||
errors: [],
|
errors: [],
|
||||||
grossPerformance: new Big('27172.74'),
|
grossPerformance: new Big('27172.74'),
|
||||||
@ -175,8 +197,12 @@ describe('PortfolioCalculator', () => {
|
|||||||
valueInBaseCurrency: new Big('13298.425356')
|
valueInBaseCurrency: new Big('13298.425356')
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
totalFeesWithCurrencyEffect: new Big('0'),
|
||||||
|
totalInterestWithCurrencyEffect: new Big('0'),
|
||||||
totalInvestment: new Big('320.43'),
|
totalInvestment: new Big('320.43'),
|
||||||
totalInvestmentWithCurrencyEffect: new Big('318.542667299999967957')
|
totalInvestmentWithCurrencyEffect: new Big('318.542667299999967957'),
|
||||||
|
totalLiabilitiesWithCurrencyEffect: new Big('0'),
|
||||||
|
totalValuablesWithCurrencyEffect: new Big('0')
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(investments).toEqual([
|
expect(investments).toEqual([
|
||||||
|
@ -0,0 +1,156 @@
|
|||||||
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
|
import {
|
||||||
|
activityDummyData,
|
||||||
|
symbolProfileDummyData,
|
||||||
|
userDummyData
|
||||||
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
|
||||||
|
import {
|
||||||
|
PortfolioCalculatorFactory,
|
||||||
|
PerformanceCalculationType
|
||||||
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
||||||
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
|
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
||||||
|
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
||||||
|
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
|
||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
|
|
||||||
|
import { Big } from 'big.js';
|
||||||
|
|
||||||
|
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
CurrentRateService: jest.fn().mockImplementation(() => {
|
||||||
|
return CurrentRateServiceMock;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
RedisCacheService: jest.fn().mockImplementation(() => {
|
||||||
|
return RedisCacheServiceMock;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PortfolioCalculator', () => {
|
||||||
|
let configurationService: ConfigurationService;
|
||||||
|
let currentRateService: CurrentRateService;
|
||||||
|
let exchangeRateDataService: ExchangeRateDataService;
|
||||||
|
let factory: PortfolioCalculatorFactory;
|
||||||
|
let redisCacheService: RedisCacheService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
configurationService = new ConfigurationService();
|
||||||
|
|
||||||
|
currentRateService = new CurrentRateService(null, null, null, null);
|
||||||
|
|
||||||
|
exchangeRateDataService = new ExchangeRateDataService(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
redisCacheService = new RedisCacheService(null, null);
|
||||||
|
|
||||||
|
factory = new PortfolioCalculatorFactory(
|
||||||
|
configurationService,
|
||||||
|
currentRateService,
|
||||||
|
exchangeRateDataService,
|
||||||
|
redisCacheService
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('compute portfolio snapshot', () => {
|
||||||
|
it.only('with fee activity', async () => {
|
||||||
|
const spy = jest
|
||||||
|
.spyOn(Date, 'now')
|
||||||
|
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
||||||
|
|
||||||
|
const activities: Activity[] = [
|
||||||
|
{
|
||||||
|
...activityDummyData,
|
||||||
|
date: new Date('2021-09-01'),
|
||||||
|
fee: 49,
|
||||||
|
quantity: 0,
|
||||||
|
SymbolProfile: {
|
||||||
|
...symbolProfileDummyData,
|
||||||
|
currency: 'USD',
|
||||||
|
dataSource: 'MANUAL',
|
||||||
|
name: 'Account Opening Fee',
|
||||||
|
symbol: '2c463fb3-af07-486e-adb0-8301b3d72141'
|
||||||
|
},
|
||||||
|
type: 'FEE',
|
||||||
|
unitPrice: 0
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const portfolioCalculator = factory.createCalculator({
|
||||||
|
activities,
|
||||||
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
|
currency: 'USD',
|
||||||
|
userId: userDummyData.id
|
||||||
|
});
|
||||||
|
|
||||||
|
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
|
||||||
|
parseDate('2021-11-30')
|
||||||
|
);
|
||||||
|
|
||||||
|
spy.mockRestore();
|
||||||
|
|
||||||
|
expect(portfolioSnapshot).toEqual({
|
||||||
|
currentValueInBaseCurrency: new Big('0'),
|
||||||
|
errors: [],
|
||||||
|
grossPerformance: new Big('0'),
|
||||||
|
grossPerformancePercentage: new Big('0'),
|
||||||
|
grossPerformancePercentageWithCurrencyEffect: new Big('0'),
|
||||||
|
grossPerformanceWithCurrencyEffect: new Big('0'),
|
||||||
|
hasErrors: true,
|
||||||
|
netPerformance: new Big('0'),
|
||||||
|
netPerformancePercentage: new Big('0'),
|
||||||
|
netPerformancePercentageWithCurrencyEffect: new Big('0'),
|
||||||
|
netPerformanceWithCurrencyEffect: new Big('0'),
|
||||||
|
positions: [
|
||||||
|
{
|
||||||
|
averagePrice: new Big('0'),
|
||||||
|
currency: 'USD',
|
||||||
|
dataSource: 'MANUAL',
|
||||||
|
dividend: new Big('0'),
|
||||||
|
dividendInBaseCurrency: new Big('0'),
|
||||||
|
fee: new Big('49'),
|
||||||
|
firstBuyDate: '2021-09-01',
|
||||||
|
grossPerformance: null,
|
||||||
|
grossPerformancePercentage: null,
|
||||||
|
grossPerformancePercentageWithCurrencyEffect: null,
|
||||||
|
grossPerformanceWithCurrencyEffect: null,
|
||||||
|
investment: new Big('0'),
|
||||||
|
investmentWithCurrencyEffect: new Big('0'),
|
||||||
|
marketPrice: null,
|
||||||
|
marketPriceInBaseCurrency: 0,
|
||||||
|
netPerformance: null,
|
||||||
|
netPerformancePercentage: null,
|
||||||
|
netPerformancePercentageWithCurrencyEffect: null,
|
||||||
|
netPerformanceWithCurrencyEffect: null,
|
||||||
|
quantity: new Big('0'),
|
||||||
|
symbol: '2c463fb3-af07-486e-adb0-8301b3d72141',
|
||||||
|
tags: [],
|
||||||
|
timeWeightedInvestment: new Big('0'),
|
||||||
|
timeWeightedInvestmentWithCurrencyEffect: new Big('0'),
|
||||||
|
transactionCount: 1,
|
||||||
|
valueInBaseCurrency: new Big('0')
|
||||||
|
}
|
||||||
|
],
|
||||||
|
totalFeesWithCurrencyEffect: new Big('49'),
|
||||||
|
totalInterestWithCurrencyEffect: new Big('0'),
|
||||||
|
totalInvestment: new Big('0'),
|
||||||
|
totalInvestmentWithCurrencyEffect: new Big('0'),
|
||||||
|
totalLiabilitiesWithCurrencyEffect: new Big('0'),
|
||||||
|
totalValuablesWithCurrencyEffect: new Big('0')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,7 +1,8 @@
|
|||||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
import {
|
import {
|
||||||
activityDummyData,
|
activityDummyData,
|
||||||
symbolProfileDummyData
|
symbolProfileDummyData,
|
||||||
|
userDummyData
|
||||||
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
|
||||||
import {
|
import {
|
||||||
PortfolioCalculatorFactory,
|
PortfolioCalculatorFactory,
|
||||||
@ -9,6 +10,9 @@ import {
|
|||||||
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
||||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
||||||
|
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
||||||
|
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
|
||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock';
|
import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock';
|
||||||
import { parseDate } from '@ghostfolio/common/helper';
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
@ -24,6 +28,15 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
RedisCacheService: jest.fn().mockImplementation(() => {
|
||||||
|
return RedisCacheServiceMock;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
jest.mock(
|
jest.mock(
|
||||||
'@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service',
|
'@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service',
|
||||||
() => {
|
() => {
|
||||||
@ -37,11 +50,15 @@ jest.mock(
|
|||||||
);
|
);
|
||||||
|
|
||||||
describe('PortfolioCalculator', () => {
|
describe('PortfolioCalculator', () => {
|
||||||
|
let configurationService: ConfigurationService;
|
||||||
let currentRateService: CurrentRateService;
|
let currentRateService: CurrentRateService;
|
||||||
let exchangeRateDataService: ExchangeRateDataService;
|
let exchangeRateDataService: ExchangeRateDataService;
|
||||||
let factory: PortfolioCalculatorFactory;
|
let factory: PortfolioCalculatorFactory;
|
||||||
|
let redisCacheService: RedisCacheService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
configurationService = new ConfigurationService();
|
||||||
|
|
||||||
currentRateService = new CurrentRateService(null, null, null, null);
|
currentRateService = new CurrentRateService(null, null, null, null);
|
||||||
|
|
||||||
exchangeRateDataService = new ExchangeRateDataService(
|
exchangeRateDataService = new ExchangeRateDataService(
|
||||||
@ -51,14 +68,22 @@ describe('PortfolioCalculator', () => {
|
|||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
redisCacheService = new RedisCacheService(null, null);
|
||||||
|
|
||||||
factory = new PortfolioCalculatorFactory(
|
factory = new PortfolioCalculatorFactory(
|
||||||
|
configurationService,
|
||||||
currentRateService,
|
currentRateService,
|
||||||
exchangeRateDataService
|
exchangeRateDataService,
|
||||||
|
redisCacheService
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('get current positions', () => {
|
describe('get current positions', () => {
|
||||||
it.only('with GOOGL buy', async () => {
|
it.only('with GOOGL buy', async () => {
|
||||||
|
const spy = jest
|
||||||
|
.spyOn(Date, 'now')
|
||||||
|
.mockImplementation(() => parseDate('2023-07-10').getTime());
|
||||||
|
|
||||||
const activities: Activity[] = [
|
const activities: Activity[] = [
|
||||||
{
|
{
|
||||||
...activityDummyData,
|
...activityDummyData,
|
||||||
@ -80,18 +105,15 @@ describe('PortfolioCalculator', () => {
|
|||||||
const portfolioCalculator = factory.createCalculator({
|
const portfolioCalculator = factory.createCalculator({
|
||||||
activities,
|
activities,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'CHF'
|
currency: 'CHF',
|
||||||
|
userId: userDummyData.id
|
||||||
});
|
});
|
||||||
|
|
||||||
const spy = jest
|
|
||||||
.spyOn(Date, 'now')
|
|
||||||
.mockImplementation(() => parseDate('2023-07-10').getTime());
|
|
||||||
|
|
||||||
const chartData = await portfolioCalculator.getChartData({
|
const chartData = await portfolioCalculator.getChartData({
|
||||||
start: parseDate('2023-01-03')
|
start: parseDate('2023-01-03')
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
|
||||||
parseDate('2023-01-03')
|
parseDate('2023-01-03')
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -104,7 +126,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
|
|
||||||
spy.mockRestore();
|
spy.mockRestore();
|
||||||
|
|
||||||
expect(currentPositions).toEqual({
|
expect(portfolioSnapshot).toEqual({
|
||||||
currentValueInBaseCurrency: new Big('103.10483'),
|
currentValueInBaseCurrency: new Big('103.10483'),
|
||||||
errors: [],
|
errors: [],
|
||||||
grossPerformance: new Big('27.33'),
|
grossPerformance: new Big('27.33'),
|
||||||
@ -154,8 +176,12 @@ describe('PortfolioCalculator', () => {
|
|||||||
valueInBaseCurrency: new Big('103.10483')
|
valueInBaseCurrency: new Big('103.10483')
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
totalFeesWithCurrencyEffect: new Big('1'),
|
||||||
|
totalInterestWithCurrencyEffect: new Big('0'),
|
||||||
totalInvestment: new Big('89.12'),
|
totalInvestment: new Big('89.12'),
|
||||||
totalInvestmentWithCurrencyEffect: new Big('82.329056')
|
totalInvestmentWithCurrencyEffect: new Big('82.329056'),
|
||||||
|
totalLiabilitiesWithCurrencyEffect: new Big('0'),
|
||||||
|
totalValuablesWithCurrencyEffect: new Big('0')
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(investments).toEqual([
|
expect(investments).toEqual([
|
||||||
|
@ -0,0 +1,156 @@
|
|||||||
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
|
import {
|
||||||
|
activityDummyData,
|
||||||
|
symbolProfileDummyData,
|
||||||
|
userDummyData
|
||||||
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
|
||||||
|
import {
|
||||||
|
PortfolioCalculatorFactory,
|
||||||
|
PerformanceCalculationType
|
||||||
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
||||||
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
|
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
||||||
|
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
||||||
|
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
|
||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
|
|
||||||
|
import { Big } from 'big.js';
|
||||||
|
|
||||||
|
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
CurrentRateService: jest.fn().mockImplementation(() => {
|
||||||
|
return CurrentRateServiceMock;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
RedisCacheService: jest.fn().mockImplementation(() => {
|
||||||
|
return RedisCacheServiceMock;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PortfolioCalculator', () => {
|
||||||
|
let configurationService: ConfigurationService;
|
||||||
|
let currentRateService: CurrentRateService;
|
||||||
|
let exchangeRateDataService: ExchangeRateDataService;
|
||||||
|
let factory: PortfolioCalculatorFactory;
|
||||||
|
let redisCacheService: RedisCacheService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
configurationService = new ConfigurationService();
|
||||||
|
|
||||||
|
currentRateService = new CurrentRateService(null, null, null, null);
|
||||||
|
|
||||||
|
exchangeRateDataService = new ExchangeRateDataService(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
redisCacheService = new RedisCacheService(null, null);
|
||||||
|
|
||||||
|
factory = new PortfolioCalculatorFactory(
|
||||||
|
configurationService,
|
||||||
|
currentRateService,
|
||||||
|
exchangeRateDataService,
|
||||||
|
redisCacheService
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('compute portfolio snapshot', () => {
|
||||||
|
it.only('with item activity', async () => {
|
||||||
|
const spy = jest
|
||||||
|
.spyOn(Date, 'now')
|
||||||
|
.mockImplementation(() => parseDate('2022-01-31').getTime());
|
||||||
|
|
||||||
|
const activities: Activity[] = [
|
||||||
|
{
|
||||||
|
...activityDummyData,
|
||||||
|
date: new Date('2022-01-01'),
|
||||||
|
fee: 0,
|
||||||
|
quantity: 1,
|
||||||
|
SymbolProfile: {
|
||||||
|
...symbolProfileDummyData,
|
||||||
|
currency: 'USD',
|
||||||
|
dataSource: 'MANUAL',
|
||||||
|
name: 'Penthouse Apartment',
|
||||||
|
symbol: 'dac95060-d4f2-4653-a253-2c45e6fb5cde'
|
||||||
|
},
|
||||||
|
type: 'ITEM',
|
||||||
|
unitPrice: 500000
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const portfolioCalculator = factory.createCalculator({
|
||||||
|
activities,
|
||||||
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
|
currency: 'USD',
|
||||||
|
userId: userDummyData.id
|
||||||
|
});
|
||||||
|
|
||||||
|
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
|
||||||
|
parseDate('2022-01-01')
|
||||||
|
);
|
||||||
|
|
||||||
|
spy.mockRestore();
|
||||||
|
|
||||||
|
expect(portfolioSnapshot).toEqual({
|
||||||
|
currentValueInBaseCurrency: new Big('0'),
|
||||||
|
errors: [],
|
||||||
|
grossPerformance: new Big('0'),
|
||||||
|
grossPerformancePercentage: new Big('0'),
|
||||||
|
grossPerformancePercentageWithCurrencyEffect: new Big('0'),
|
||||||
|
grossPerformanceWithCurrencyEffect: new Big('0'),
|
||||||
|
hasErrors: true,
|
||||||
|
netPerformance: new Big('0'),
|
||||||
|
netPerformancePercentage: new Big('0'),
|
||||||
|
netPerformancePercentageWithCurrencyEffect: new Big('0'),
|
||||||
|
netPerformanceWithCurrencyEffect: new Big('0'),
|
||||||
|
positions: [
|
||||||
|
{
|
||||||
|
averagePrice: new Big('500000'),
|
||||||
|
currency: 'USD',
|
||||||
|
dataSource: 'MANUAL',
|
||||||
|
dividend: new Big('0'),
|
||||||
|
dividendInBaseCurrency: new Big('0'),
|
||||||
|
fee: new Big('0'),
|
||||||
|
firstBuyDate: '2022-01-01',
|
||||||
|
grossPerformance: null,
|
||||||
|
grossPerformancePercentage: null,
|
||||||
|
grossPerformancePercentageWithCurrencyEffect: null,
|
||||||
|
grossPerformanceWithCurrencyEffect: null,
|
||||||
|
investment: new Big('0'),
|
||||||
|
investmentWithCurrencyEffect: new Big('0'),
|
||||||
|
marketPrice: null,
|
||||||
|
marketPriceInBaseCurrency: 500000,
|
||||||
|
netPerformance: null,
|
||||||
|
netPerformancePercentage: null,
|
||||||
|
netPerformancePercentageWithCurrencyEffect: null,
|
||||||
|
netPerformanceWithCurrencyEffect: null,
|
||||||
|
quantity: new Big('0'),
|
||||||
|
symbol: 'dac95060-d4f2-4653-a253-2c45e6fb5cde',
|
||||||
|
tags: [],
|
||||||
|
timeWeightedInvestment: new Big('0'),
|
||||||
|
timeWeightedInvestmentWithCurrencyEffect: new Big('0'),
|
||||||
|
transactionCount: 1,
|
||||||
|
valueInBaseCurrency: new Big('0')
|
||||||
|
}
|
||||||
|
],
|
||||||
|
totalFeesWithCurrencyEffect: new Big('0'),
|
||||||
|
totalInterestWithCurrencyEffect: new Big('0'),
|
||||||
|
totalInvestment: new Big('0'),
|
||||||
|
totalInvestmentWithCurrencyEffect: new Big('0'),
|
||||||
|
totalLiabilitiesWithCurrencyEffect: new Big('0'),
|
||||||
|
totalValuablesWithCurrencyEffect: new Big('0')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,156 @@
|
|||||||
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
|
import {
|
||||||
|
activityDummyData,
|
||||||
|
symbolProfileDummyData,
|
||||||
|
userDummyData
|
||||||
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
|
||||||
|
import {
|
||||||
|
PortfolioCalculatorFactory,
|
||||||
|
PerformanceCalculationType
|
||||||
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
||||||
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
|
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
||||||
|
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
||||||
|
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
|
||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
|
|
||||||
|
import { Big } from 'big.js';
|
||||||
|
|
||||||
|
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
CurrentRateService: jest.fn().mockImplementation(() => {
|
||||||
|
return CurrentRateServiceMock;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
RedisCacheService: jest.fn().mockImplementation(() => {
|
||||||
|
return RedisCacheServiceMock;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PortfolioCalculator', () => {
|
||||||
|
let configurationService: ConfigurationService;
|
||||||
|
let currentRateService: CurrentRateService;
|
||||||
|
let exchangeRateDataService: ExchangeRateDataService;
|
||||||
|
let factory: PortfolioCalculatorFactory;
|
||||||
|
let redisCacheService: RedisCacheService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
configurationService = new ConfigurationService();
|
||||||
|
|
||||||
|
currentRateService = new CurrentRateService(null, null, null, null);
|
||||||
|
|
||||||
|
exchangeRateDataService = new ExchangeRateDataService(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
redisCacheService = new RedisCacheService(null, null);
|
||||||
|
|
||||||
|
factory = new PortfolioCalculatorFactory(
|
||||||
|
configurationService,
|
||||||
|
currentRateService,
|
||||||
|
exchangeRateDataService,
|
||||||
|
redisCacheService
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('compute portfolio snapshot', () => {
|
||||||
|
it.only('with liability activity', async () => {
|
||||||
|
const spy = jest
|
||||||
|
.spyOn(Date, 'now')
|
||||||
|
.mockImplementation(() => parseDate('2022-01-31').getTime());
|
||||||
|
|
||||||
|
const activities: Activity[] = [
|
||||||
|
{
|
||||||
|
...activityDummyData,
|
||||||
|
date: new Date('2022-01-01'),
|
||||||
|
fee: 0,
|
||||||
|
quantity: 1,
|
||||||
|
SymbolProfile: {
|
||||||
|
...symbolProfileDummyData,
|
||||||
|
currency: 'USD',
|
||||||
|
dataSource: 'MANUAL',
|
||||||
|
name: 'Loan',
|
||||||
|
symbol: '55196015-1365-4560-aa60-8751ae6d18f8'
|
||||||
|
},
|
||||||
|
type: 'LIABILITY',
|
||||||
|
unitPrice: 3000
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const portfolioCalculator = factory.createCalculator({
|
||||||
|
activities,
|
||||||
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
|
currency: 'USD',
|
||||||
|
userId: userDummyData.id
|
||||||
|
});
|
||||||
|
|
||||||
|
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
|
||||||
|
parseDate('2022-01-01')
|
||||||
|
);
|
||||||
|
|
||||||
|
spy.mockRestore();
|
||||||
|
|
||||||
|
expect(portfolioSnapshot).toEqual({
|
||||||
|
currentValueInBaseCurrency: new Big('0'),
|
||||||
|
errors: [],
|
||||||
|
grossPerformance: new Big('0'),
|
||||||
|
grossPerformancePercentage: new Big('0'),
|
||||||
|
grossPerformancePercentageWithCurrencyEffect: new Big('0'),
|
||||||
|
grossPerformanceWithCurrencyEffect: new Big('0'),
|
||||||
|
hasErrors: true,
|
||||||
|
netPerformance: new Big('0'),
|
||||||
|
netPerformancePercentage: new Big('0'),
|
||||||
|
netPerformancePercentageWithCurrencyEffect: new Big('0'),
|
||||||
|
netPerformanceWithCurrencyEffect: new Big('0'),
|
||||||
|
positions: [
|
||||||
|
{
|
||||||
|
averagePrice: new Big('3000'),
|
||||||
|
currency: 'USD',
|
||||||
|
dataSource: 'MANUAL',
|
||||||
|
dividend: new Big('0'),
|
||||||
|
dividendInBaseCurrency: new Big('0'),
|
||||||
|
fee: new Big('0'),
|
||||||
|
firstBuyDate: '2022-01-01',
|
||||||
|
grossPerformance: null,
|
||||||
|
grossPerformancePercentage: null,
|
||||||
|
grossPerformancePercentageWithCurrencyEffect: null,
|
||||||
|
grossPerformanceWithCurrencyEffect: null,
|
||||||
|
investment: new Big('0'),
|
||||||
|
investmentWithCurrencyEffect: new Big('0'),
|
||||||
|
marketPrice: null,
|
||||||
|
marketPriceInBaseCurrency: 3000,
|
||||||
|
netPerformance: null,
|
||||||
|
netPerformancePercentage: null,
|
||||||
|
netPerformancePercentageWithCurrencyEffect: null,
|
||||||
|
netPerformanceWithCurrencyEffect: null,
|
||||||
|
quantity: new Big('0'),
|
||||||
|
symbol: '55196015-1365-4560-aa60-8751ae6d18f8',
|
||||||
|
tags: [],
|
||||||
|
timeWeightedInvestment: new Big('0'),
|
||||||
|
timeWeightedInvestmentWithCurrencyEffect: new Big('0'),
|
||||||
|
transactionCount: 1,
|
||||||
|
valueInBaseCurrency: new Big('0')
|
||||||
|
}
|
||||||
|
],
|
||||||
|
totalFeesWithCurrencyEffect: new Big('0'),
|
||||||
|
totalInterestWithCurrencyEffect: new Big('0'),
|
||||||
|
totalInvestment: new Big('0'),
|
||||||
|
totalInvestmentWithCurrencyEffect: new Big('0'),
|
||||||
|
totalLiabilitiesWithCurrencyEffect: new Big('0'),
|
||||||
|
totalValuablesWithCurrencyEffect: new Big('0')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,7 +1,8 @@
|
|||||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
import {
|
import {
|
||||||
activityDummyData,
|
activityDummyData,
|
||||||
symbolProfileDummyData
|
symbolProfileDummyData,
|
||||||
|
userDummyData
|
||||||
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
|
||||||
import {
|
import {
|
||||||
PerformanceCalculationType,
|
PerformanceCalculationType,
|
||||||
@ -9,6 +10,9 @@ import {
|
|||||||
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
||||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
||||||
|
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
||||||
|
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
|
||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock';
|
import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock';
|
||||||
import { parseDate } from '@ghostfolio/common/helper';
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
@ -24,6 +28,15 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
RedisCacheService: jest.fn().mockImplementation(() => {
|
||||||
|
return RedisCacheServiceMock;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
jest.mock(
|
jest.mock(
|
||||||
'@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service',
|
'@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service',
|
||||||
() => {
|
() => {
|
||||||
@ -37,11 +50,15 @@ jest.mock(
|
|||||||
);
|
);
|
||||||
|
|
||||||
describe('PortfolioCalculator', () => {
|
describe('PortfolioCalculator', () => {
|
||||||
|
let configurationService: ConfigurationService;
|
||||||
let currentRateService: CurrentRateService;
|
let currentRateService: CurrentRateService;
|
||||||
let exchangeRateDataService: ExchangeRateDataService;
|
let exchangeRateDataService: ExchangeRateDataService;
|
||||||
let factory: PortfolioCalculatorFactory;
|
let factory: PortfolioCalculatorFactory;
|
||||||
|
let redisCacheService: RedisCacheService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
configurationService = new ConfigurationService();
|
||||||
|
|
||||||
currentRateService = new CurrentRateService(null, null, null, null);
|
currentRateService = new CurrentRateService(null, null, null, null);
|
||||||
|
|
||||||
exchangeRateDataService = new ExchangeRateDataService(
|
exchangeRateDataService = new ExchangeRateDataService(
|
||||||
@ -51,14 +68,22 @@ describe('PortfolioCalculator', () => {
|
|||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
redisCacheService = new RedisCacheService(null, null);
|
||||||
|
|
||||||
factory = new PortfolioCalculatorFactory(
|
factory = new PortfolioCalculatorFactory(
|
||||||
|
configurationService,
|
||||||
currentRateService,
|
currentRateService,
|
||||||
exchangeRateDataService
|
exchangeRateDataService,
|
||||||
|
redisCacheService
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('get current positions', () => {
|
describe('get current positions', () => {
|
||||||
it.only('with MSFT buy', async () => {
|
it.only('with MSFT buy', async () => {
|
||||||
|
const spy = jest
|
||||||
|
.spyOn(Date, 'now')
|
||||||
|
.mockImplementation(() => parseDate('2023-07-10').getTime());
|
||||||
|
|
||||||
const activities: Activity[] = [
|
const activities: Activity[] = [
|
||||||
{
|
{
|
||||||
...activityDummyData,
|
...activityDummyData,
|
||||||
@ -95,20 +120,17 @@ describe('PortfolioCalculator', () => {
|
|||||||
const portfolioCalculator = factory.createCalculator({
|
const portfolioCalculator = factory.createCalculator({
|
||||||
activities,
|
activities,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'USD'
|
currency: 'USD',
|
||||||
|
userId: userDummyData.id
|
||||||
});
|
});
|
||||||
|
|
||||||
const spy = jest
|
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
|
||||||
.spyOn(Date, 'now')
|
|
||||||
.mockImplementation(() => parseDate('2023-07-10').getTime());
|
|
||||||
|
|
||||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
|
||||||
parseDate('2023-07-10')
|
parseDate('2023-07-10')
|
||||||
);
|
);
|
||||||
|
|
||||||
spy.mockRestore();
|
spy.mockRestore();
|
||||||
|
|
||||||
expect(currentPositions).toMatchObject({
|
expect(portfolioSnapshot).toMatchObject({
|
||||||
errors: [],
|
errors: [],
|
||||||
hasErrors: false,
|
hasErrors: false,
|
||||||
positions: [
|
positions: [
|
||||||
@ -130,8 +152,12 @@ describe('PortfolioCalculator', () => {
|
|||||||
transactionCount: 2
|
transactionCount: 2
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
totalFeesWithCurrencyEffect: new Big('19'),
|
||||||
|
totalInterestWithCurrencyEffect: new Big('0'),
|
||||||
totalInvestment: new Big('298.58'),
|
totalInvestment: new Big('298.58'),
|
||||||
totalInvestmentWithCurrencyEffect: new Big('298.58')
|
totalInvestmentWithCurrencyEffect: new Big('298.58'),
|
||||||
|
totalLiabilitiesWithCurrencyEffect: new Big('0'),
|
||||||
|
totalValuablesWithCurrencyEffect: new Big('0')
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
|
import { userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
|
||||||
import {
|
import {
|
||||||
PerformanceCalculationType,
|
PerformanceCalculationType,
|
||||||
PortfolioCalculatorFactory
|
PortfolioCalculatorFactory
|
||||||
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
||||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
||||||
|
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
||||||
|
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
|
||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { parseDate } from '@ghostfolio/common/helper';
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
|
|
||||||
@ -19,12 +23,25 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
RedisCacheService: jest.fn().mockImplementation(() => {
|
||||||
|
return RedisCacheServiceMock;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
describe('PortfolioCalculator', () => {
|
describe('PortfolioCalculator', () => {
|
||||||
|
let configurationService: ConfigurationService;
|
||||||
let currentRateService: CurrentRateService;
|
let currentRateService: CurrentRateService;
|
||||||
let exchangeRateDataService: ExchangeRateDataService;
|
let exchangeRateDataService: ExchangeRateDataService;
|
||||||
let factory: PortfolioCalculatorFactory;
|
let factory: PortfolioCalculatorFactory;
|
||||||
|
let redisCacheService: RedisCacheService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
configurationService = new ConfigurationService();
|
||||||
|
|
||||||
currentRateService = new CurrentRateService(null, null, null, null);
|
currentRateService = new CurrentRateService(null, null, null, null);
|
||||||
|
|
||||||
exchangeRateDataService = new ExchangeRateDataService(
|
exchangeRateDataService = new ExchangeRateDataService(
|
||||||
@ -34,30 +51,35 @@ describe('PortfolioCalculator', () => {
|
|||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
redisCacheService = new RedisCacheService(null, null);
|
||||||
|
|
||||||
factory = new PortfolioCalculatorFactory(
|
factory = new PortfolioCalculatorFactory(
|
||||||
|
configurationService,
|
||||||
currentRateService,
|
currentRateService,
|
||||||
exchangeRateDataService
|
exchangeRateDataService,
|
||||||
|
redisCacheService
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('get current positions', () => {
|
describe('get current positions', () => {
|
||||||
it('with no orders', async () => {
|
it('with no orders', async () => {
|
||||||
const portfolioCalculator = factory.createCalculator({
|
|
||||||
activities: [],
|
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
|
||||||
currency: 'CHF'
|
|
||||||
});
|
|
||||||
|
|
||||||
const spy = jest
|
const spy = jest
|
||||||
.spyOn(Date, 'now')
|
.spyOn(Date, 'now')
|
||||||
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
||||||
|
|
||||||
|
const portfolioCalculator = factory.createCalculator({
|
||||||
|
activities: [],
|
||||||
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
|
currency: 'CHF',
|
||||||
|
userId: userDummyData.id
|
||||||
|
});
|
||||||
|
|
||||||
const start = subDays(new Date(Date.now()), 10);
|
const start = subDays(new Date(Date.now()), 10);
|
||||||
|
|
||||||
const chartData = await portfolioCalculator.getChartData({ start });
|
const chartData = await portfolioCalculator.getChartData({ start });
|
||||||
|
|
||||||
const currentPositions =
|
const portfolioSnapshot =
|
||||||
await portfolioCalculator.getCurrentPositions(start);
|
await portfolioCalculator.computeSnapshot(start);
|
||||||
|
|
||||||
const investments = portfolioCalculator.getInvestments();
|
const investments = portfolioCalculator.getInvestments();
|
||||||
|
|
||||||
@ -68,7 +90,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
|
|
||||||
spy.mockRestore();
|
spy.mockRestore();
|
||||||
|
|
||||||
expect(currentPositions).toEqual({
|
expect(portfolioSnapshot).toEqual({
|
||||||
currentValueInBaseCurrency: new Big(0),
|
currentValueInBaseCurrency: new Big(0),
|
||||||
grossPerformance: new Big(0),
|
grossPerformance: new Big(0),
|
||||||
grossPerformancePercentage: new Big(0),
|
grossPerformancePercentage: new Big(0),
|
||||||
@ -80,13 +102,22 @@ describe('PortfolioCalculator', () => {
|
|||||||
netPerformancePercentageWithCurrencyEffect: new Big(0),
|
netPerformancePercentageWithCurrencyEffect: new Big(0),
|
||||||
netPerformanceWithCurrencyEffect: new Big(0),
|
netPerformanceWithCurrencyEffect: new Big(0),
|
||||||
positions: [],
|
positions: [],
|
||||||
|
totalFeesWithCurrencyEffect: new Big('0'),
|
||||||
|
totalInterestWithCurrencyEffect: new Big('0'),
|
||||||
totalInvestment: new Big(0),
|
totalInvestment: new Big(0),
|
||||||
totalInvestmentWithCurrencyEffect: new Big(0)
|
totalInvestmentWithCurrencyEffect: new Big(0),
|
||||||
|
totalLiabilitiesWithCurrencyEffect: new Big('0'),
|
||||||
|
totalValuablesWithCurrencyEffect: new Big('0')
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(investments).toEqual([]);
|
expect(investments).toEqual([]);
|
||||||
|
|
||||||
expect(investmentsByMonth).toEqual([]);
|
expect(investmentsByMonth).toEqual([
|
||||||
|
{
|
||||||
|
date: '2021-12-01',
|
||||||
|
investment: 0
|
||||||
|
}
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
import {
|
import {
|
||||||
activityDummyData,
|
activityDummyData,
|
||||||
symbolProfileDummyData
|
symbolProfileDummyData,
|
||||||
|
userDummyData
|
||||||
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
|
||||||
import {
|
import {
|
||||||
PerformanceCalculationType,
|
PerformanceCalculationType,
|
||||||
@ -9,6 +10,9 @@ import {
|
|||||||
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
||||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
||||||
|
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
||||||
|
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
|
||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { parseDate } from '@ghostfolio/common/helper';
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
|
|
||||||
@ -23,12 +27,25 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
RedisCacheService: jest.fn().mockImplementation(() => {
|
||||||
|
return RedisCacheServiceMock;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
describe('PortfolioCalculator', () => {
|
describe('PortfolioCalculator', () => {
|
||||||
|
let configurationService: ConfigurationService;
|
||||||
let currentRateService: CurrentRateService;
|
let currentRateService: CurrentRateService;
|
||||||
let exchangeRateDataService: ExchangeRateDataService;
|
let exchangeRateDataService: ExchangeRateDataService;
|
||||||
let factory: PortfolioCalculatorFactory;
|
let factory: PortfolioCalculatorFactory;
|
||||||
|
let redisCacheService: RedisCacheService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
configurationService = new ConfigurationService();
|
||||||
|
|
||||||
currentRateService = new CurrentRateService(null, null, null, null);
|
currentRateService = new CurrentRateService(null, null, null, null);
|
||||||
|
|
||||||
exchangeRateDataService = new ExchangeRateDataService(
|
exchangeRateDataService = new ExchangeRateDataService(
|
||||||
@ -38,14 +55,22 @@ describe('PortfolioCalculator', () => {
|
|||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
redisCacheService = new RedisCacheService(null, null);
|
||||||
|
|
||||||
factory = new PortfolioCalculatorFactory(
|
factory = new PortfolioCalculatorFactory(
|
||||||
|
configurationService,
|
||||||
currentRateService,
|
currentRateService,
|
||||||
exchangeRateDataService
|
exchangeRateDataService,
|
||||||
|
redisCacheService
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('get current positions', () => {
|
describe('get current positions', () => {
|
||||||
it.only('with NOVN.SW buy and sell partially', async () => {
|
it.only('with NOVN.SW buy and sell partially', async () => {
|
||||||
|
const spy = jest
|
||||||
|
.spyOn(Date, 'now')
|
||||||
|
.mockImplementation(() => parseDate('2022-04-11').getTime());
|
||||||
|
|
||||||
const activities: Activity[] = [
|
const activities: Activity[] = [
|
||||||
{
|
{
|
||||||
...activityDummyData,
|
...activityDummyData,
|
||||||
@ -82,17 +107,15 @@ describe('PortfolioCalculator', () => {
|
|||||||
const portfolioCalculator = factory.createCalculator({
|
const portfolioCalculator = factory.createCalculator({
|
||||||
activities,
|
activities,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'CHF'
|
currency: 'CHF',
|
||||||
|
userId: userDummyData.id
|
||||||
});
|
});
|
||||||
const spy = jest
|
|
||||||
.spyOn(Date, 'now')
|
|
||||||
.mockImplementation(() => parseDate('2022-04-11').getTime());
|
|
||||||
|
|
||||||
const chartData = await portfolioCalculator.getChartData({
|
const chartData = await portfolioCalculator.getChartData({
|
||||||
start: parseDate('2022-03-07')
|
start: parseDate('2022-03-07')
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
|
||||||
parseDate('2022-03-07')
|
parseDate('2022-03-07')
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -105,7 +128,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
|
|
||||||
spy.mockRestore();
|
spy.mockRestore();
|
||||||
|
|
||||||
expect(currentPositions).toEqual({
|
expect(portfolioSnapshot).toEqual({
|
||||||
currentValueInBaseCurrency: new Big('87.8'),
|
currentValueInBaseCurrency: new Big('87.8'),
|
||||||
errors: [],
|
errors: [],
|
||||||
grossPerformance: new Big('21.93'),
|
grossPerformance: new Big('21.93'),
|
||||||
@ -157,8 +180,12 @@ describe('PortfolioCalculator', () => {
|
|||||||
valueInBaseCurrency: new Big('87.8')
|
valueInBaseCurrency: new Big('87.8')
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
totalFeesWithCurrencyEffect: new Big('4.25'),
|
||||||
|
totalInterestWithCurrencyEffect: new Big('0'),
|
||||||
totalInvestment: new Big('75.80'),
|
totalInvestment: new Big('75.80'),
|
||||||
totalInvestmentWithCurrencyEffect: new Big('75.80')
|
totalInvestmentWithCurrencyEffect: new Big('75.80'),
|
||||||
|
totalLiabilitiesWithCurrencyEffect: new Big('0'),
|
||||||
|
totalValuablesWithCurrencyEffect: new Big('0')
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(investments).toEqual([
|
expect(investments).toEqual([
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
import {
|
import {
|
||||||
activityDummyData,
|
activityDummyData,
|
||||||
symbolProfileDummyData
|
symbolProfileDummyData,
|
||||||
|
userDummyData
|
||||||
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
|
||||||
import {
|
import {
|
||||||
PerformanceCalculationType,
|
PerformanceCalculationType,
|
||||||
@ -9,6 +10,9 @@ import {
|
|||||||
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
||||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
|
||||||
|
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
||||||
|
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
|
||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { parseDate } from '@ghostfolio/common/helper';
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
|
|
||||||
@ -23,12 +27,25 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
RedisCacheService: jest.fn().mockImplementation(() => {
|
||||||
|
return RedisCacheServiceMock;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
describe('PortfolioCalculator', () => {
|
describe('PortfolioCalculator', () => {
|
||||||
|
let configurationService: ConfigurationService;
|
||||||
let currentRateService: CurrentRateService;
|
let currentRateService: CurrentRateService;
|
||||||
let exchangeRateDataService: ExchangeRateDataService;
|
let exchangeRateDataService: ExchangeRateDataService;
|
||||||
let factory: PortfolioCalculatorFactory;
|
let factory: PortfolioCalculatorFactory;
|
||||||
|
let redisCacheService: RedisCacheService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
configurationService = new ConfigurationService();
|
||||||
|
|
||||||
currentRateService = new CurrentRateService(null, null, null, null);
|
currentRateService = new CurrentRateService(null, null, null, null);
|
||||||
|
|
||||||
exchangeRateDataService = new ExchangeRateDataService(
|
exchangeRateDataService = new ExchangeRateDataService(
|
||||||
@ -38,14 +55,22 @@ describe('PortfolioCalculator', () => {
|
|||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
redisCacheService = new RedisCacheService(null, null);
|
||||||
|
|
||||||
factory = new PortfolioCalculatorFactory(
|
factory = new PortfolioCalculatorFactory(
|
||||||
|
configurationService,
|
||||||
currentRateService,
|
currentRateService,
|
||||||
exchangeRateDataService
|
exchangeRateDataService,
|
||||||
|
redisCacheService
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('get current positions', () => {
|
describe('get current positions', () => {
|
||||||
it.only('with NOVN.SW buy and sell', async () => {
|
it.only('with NOVN.SW buy and sell', async () => {
|
||||||
|
const spy = jest
|
||||||
|
.spyOn(Date, 'now')
|
||||||
|
.mockImplementation(() => parseDate('2022-04-11').getTime());
|
||||||
|
|
||||||
const activities: Activity[] = [
|
const activities: Activity[] = [
|
||||||
{
|
{
|
||||||
...activityDummyData,
|
...activityDummyData,
|
||||||
@ -82,18 +107,15 @@ describe('PortfolioCalculator', () => {
|
|||||||
const portfolioCalculator = factory.createCalculator({
|
const portfolioCalculator = factory.createCalculator({
|
||||||
activities,
|
activities,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'CHF'
|
currency: 'CHF',
|
||||||
|
userId: userDummyData.id
|
||||||
});
|
});
|
||||||
|
|
||||||
const spy = jest
|
|
||||||
.spyOn(Date, 'now')
|
|
||||||
.mockImplementation(() => parseDate('2022-04-11').getTime());
|
|
||||||
|
|
||||||
const chartData = await portfolioCalculator.getChartData({
|
const chartData = await portfolioCalculator.getChartData({
|
||||||
start: parseDate('2022-03-07')
|
start: parseDate('2022-03-07')
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
|
||||||
parseDate('2022-03-07')
|
parseDate('2022-03-07')
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -113,6 +135,8 @@ describe('PortfolioCalculator', () => {
|
|||||||
netPerformanceInPercentage: 0,
|
netPerformanceInPercentage: 0,
|
||||||
netPerformanceInPercentageWithCurrencyEffect: 0,
|
netPerformanceInPercentageWithCurrencyEffect: 0,
|
||||||
netPerformanceWithCurrencyEffect: 0,
|
netPerformanceWithCurrencyEffect: 0,
|
||||||
|
netWorth: 151.6,
|
||||||
|
totalAccountBalance: 0,
|
||||||
totalInvestment: 151.6,
|
totalInvestment: 151.6,
|
||||||
totalInvestmentValueWithCurrencyEffect: 151.6,
|
totalInvestmentValueWithCurrencyEffect: 151.6,
|
||||||
value: 151.6,
|
value: 151.6,
|
||||||
@ -126,13 +150,15 @@ describe('PortfolioCalculator', () => {
|
|||||||
netPerformanceInPercentage: 13.100263852242744,
|
netPerformanceInPercentage: 13.100263852242744,
|
||||||
netPerformanceInPercentageWithCurrencyEffect: 13.100263852242744,
|
netPerformanceInPercentageWithCurrencyEffect: 13.100263852242744,
|
||||||
netPerformanceWithCurrencyEffect: 19.86,
|
netPerformanceWithCurrencyEffect: 19.86,
|
||||||
|
netWorth: 0,
|
||||||
|
totalAccountBalance: 0,
|
||||||
totalInvestment: 0,
|
totalInvestment: 0,
|
||||||
totalInvestmentValueWithCurrencyEffect: 0,
|
totalInvestmentValueWithCurrencyEffect: 0,
|
||||||
value: 0,
|
value: 0,
|
||||||
valueWithCurrencyEffect: 0
|
valueWithCurrencyEffect: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(currentPositions).toEqual({
|
expect(portfolioSnapshot).toEqual({
|
||||||
currentValueInBaseCurrency: new Big('0'),
|
currentValueInBaseCurrency: new Big('0'),
|
||||||
errors: [],
|
errors: [],
|
||||||
grossPerformance: new Big('19.86'),
|
grossPerformance: new Big('19.86'),
|
||||||
@ -182,8 +208,12 @@ describe('PortfolioCalculator', () => {
|
|||||||
valueInBaseCurrency: new Big('0')
|
valueInBaseCurrency: new Big('0')
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
totalFeesWithCurrencyEffect: new Big('0'),
|
||||||
|
totalInterestWithCurrencyEffect: new Big('0'),
|
||||||
totalInvestment: new Big('0'),
|
totalInvestment: new Big('0'),
|
||||||
totalInvestmentWithCurrencyEffect: new Big('0')
|
totalInvestmentWithCurrencyEffect: new Big('0'),
|
||||||
|
totalLiabilitiesWithCurrencyEffect: new Big('0'),
|
||||||
|
totalValuablesWithCurrencyEffect: new Big('0')
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(investments).toEqual([
|
expect(investments).toEqual([
|
||||||
|
@ -1,13 +1,19 @@
|
|||||||
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
||||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
|
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
|
|
||||||
describe('PortfolioCalculator', () => {
|
describe('PortfolioCalculator', () => {
|
||||||
|
let configurationService: ConfigurationService;
|
||||||
let currentRateService: CurrentRateService;
|
let currentRateService: CurrentRateService;
|
||||||
let exchangeRateDataService: ExchangeRateDataService;
|
let exchangeRateDataService: ExchangeRateDataService;
|
||||||
let factory: PortfolioCalculatorFactory;
|
let factory: PortfolioCalculatorFactory;
|
||||||
|
let redisCacheService: RedisCacheService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
configurationService = new ConfigurationService();
|
||||||
|
|
||||||
currentRateService = new CurrentRateService(null, null, null, null);
|
currentRateService = new CurrentRateService(null, null, null, null);
|
||||||
|
|
||||||
exchangeRateDataService = new ExchangeRateDataService(
|
exchangeRateDataService = new ExchangeRateDataService(
|
||||||
@ -17,9 +23,13 @@ describe('PortfolioCalculator', () => {
|
|||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
redisCacheService = new RedisCacheService(null, null);
|
||||||
|
|
||||||
factory = new PortfolioCalculatorFactory(
|
factory = new PortfolioCalculatorFactory(
|
||||||
|
configurationService,
|
||||||
currentRateService,
|
currentRateService,
|
||||||
exchangeRateDataService
|
exchangeRateDataService,
|
||||||
|
redisCacheService
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,13 +1,9 @@
|
|||||||
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
|
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
|
||||||
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
|
|
||||||
import { PortfolioOrderItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order-item.interface';
|
import { PortfolioOrderItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order-item.interface';
|
||||||
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
|
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
|
||||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||||
import {
|
import { SymbolMetrics, UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
SymbolMetrics,
|
import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models';
|
||||||
TimelinePosition,
|
|
||||||
UniqueAsset
|
|
||||||
} from '@ghostfolio/common/interfaces';
|
|
||||||
|
|
||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
import { Big } from 'big.js';
|
import { Big } from 'big.js';
|
||||||
@ -23,19 +19,27 @@ import { cloneDeep, first, last, sortBy } from 'lodash';
|
|||||||
export class TWRPortfolioCalculator extends PortfolioCalculator {
|
export class TWRPortfolioCalculator extends PortfolioCalculator {
|
||||||
protected calculateOverallPerformance(
|
protected calculateOverallPerformance(
|
||||||
positions: TimelinePosition[]
|
positions: TimelinePosition[]
|
||||||
): CurrentPositions {
|
): PortfolioSnapshot {
|
||||||
let currentValueInBaseCurrency = new Big(0);
|
let currentValueInBaseCurrency = new Big(0);
|
||||||
let grossPerformance = new Big(0);
|
let grossPerformance = new Big(0);
|
||||||
let grossPerformanceWithCurrencyEffect = new Big(0);
|
let grossPerformanceWithCurrencyEffect = new Big(0);
|
||||||
let hasErrors = false;
|
let hasErrors = false;
|
||||||
let netPerformance = new Big(0);
|
let netPerformance = new Big(0);
|
||||||
let netPerformanceWithCurrencyEffect = new Big(0);
|
let netPerformanceWithCurrencyEffect = new Big(0);
|
||||||
|
let totalFeesWithCurrencyEffect = new Big(0);
|
||||||
|
let totalInterestWithCurrencyEffect = new Big(0);
|
||||||
let totalInvestment = new Big(0);
|
let totalInvestment = new Big(0);
|
||||||
let totalInvestmentWithCurrencyEffect = new Big(0);
|
let totalInvestmentWithCurrencyEffect = new Big(0);
|
||||||
let totalTimeWeightedInvestment = new Big(0);
|
let totalTimeWeightedInvestment = new Big(0);
|
||||||
let totalTimeWeightedInvestmentWithCurrencyEffect = new Big(0);
|
let totalTimeWeightedInvestmentWithCurrencyEffect = new Big(0);
|
||||||
|
|
||||||
for (const currentPosition of positions) {
|
for (const currentPosition of positions) {
|
||||||
|
if (currentPosition.fee) {
|
||||||
|
totalFeesWithCurrencyEffect = totalFeesWithCurrencyEffect.plus(
|
||||||
|
currentPosition.fee
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (currentPosition.valueInBaseCurrency) {
|
if (currentPosition.valueInBaseCurrency) {
|
||||||
currentValueInBaseCurrency = currentValueInBaseCurrency.plus(
|
currentValueInBaseCurrency = currentValueInBaseCurrency.plus(
|
||||||
currentPosition.valueInBaseCurrency
|
currentPosition.valueInBaseCurrency
|
||||||
@ -101,6 +105,9 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
|||||||
hasErrors,
|
hasErrors,
|
||||||
netPerformance,
|
netPerformance,
|
||||||
netPerformanceWithCurrencyEffect,
|
netPerformanceWithCurrencyEffect,
|
||||||
|
positions,
|
||||||
|
totalFeesWithCurrencyEffect,
|
||||||
|
totalInterestWithCurrencyEffect,
|
||||||
totalInvestment,
|
totalInvestment,
|
||||||
totalInvestmentWithCurrencyEffect,
|
totalInvestmentWithCurrencyEffect,
|
||||||
netPerformancePercentage: totalTimeWeightedInvestment.eq(0)
|
netPerformancePercentage: totalTimeWeightedInvestment.eq(0)
|
||||||
@ -121,7 +128,8 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
|||||||
: grossPerformanceWithCurrencyEffect.div(
|
: grossPerformanceWithCurrencyEffect.div(
|
||||||
totalTimeWeightedInvestmentWithCurrencyEffect
|
totalTimeWeightedInvestmentWithCurrencyEffect
|
||||||
),
|
),
|
||||||
positions
|
totalLiabilitiesWithCurrencyEffect: new Big(0),
|
||||||
|
totalValuablesWithCurrencyEffect: new Big(0)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,14 +184,21 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
|||||||
[date: string]: Big;
|
[date: string]: Big;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
|
let totalAccountBalanceInBaseCurrency = new Big(0);
|
||||||
let totalDividend = new Big(0);
|
let totalDividend = new Big(0);
|
||||||
let totalDividendInBaseCurrency = new Big(0);
|
let totalDividendInBaseCurrency = new Big(0);
|
||||||
|
let totalInterest = new Big(0);
|
||||||
|
let totalInterestInBaseCurrency = new Big(0);
|
||||||
let totalInvestment = new Big(0);
|
let totalInvestment = new Big(0);
|
||||||
let totalInvestmentFromBuyTransactions = new Big(0);
|
let totalInvestmentFromBuyTransactions = new Big(0);
|
||||||
let totalInvestmentFromBuyTransactionsWithCurrencyEffect = new Big(0);
|
let totalInvestmentFromBuyTransactionsWithCurrencyEffect = new Big(0);
|
||||||
let totalInvestmentWithCurrencyEffect = new Big(0);
|
let totalInvestmentWithCurrencyEffect = new Big(0);
|
||||||
|
let totalLiabilities = new Big(0);
|
||||||
|
let totalLiabilitiesInBaseCurrency = new Big(0);
|
||||||
let totalQuantityFromBuyTransactions = new Big(0);
|
let totalQuantityFromBuyTransactions = new Big(0);
|
||||||
let totalUnits = new Big(0);
|
let totalUnits = new Big(0);
|
||||||
|
let totalValuables = new Big(0);
|
||||||
|
let totalValuablesInBaseCurrency = new Big(0);
|
||||||
let valueAtStartDate: Big;
|
let valueAtStartDate: Big;
|
||||||
let valueAtStartDateWithCurrencyEffect: Big;
|
let valueAtStartDateWithCurrencyEffect: Big;
|
||||||
|
|
||||||
@ -198,6 +213,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
|||||||
return {
|
return {
|
||||||
currentValues: {},
|
currentValues: {},
|
||||||
currentValuesWithCurrencyEffect: {},
|
currentValuesWithCurrencyEffect: {},
|
||||||
|
feesWithCurrencyEffect: new Big(0),
|
||||||
grossPerformance: new Big(0),
|
grossPerformance: new Big(0),
|
||||||
grossPerformancePercentage: new Big(0),
|
grossPerformancePercentage: new Big(0),
|
||||||
grossPerformancePercentageWithCurrencyEffect: new Big(0),
|
grossPerformancePercentageWithCurrencyEffect: new Big(0),
|
||||||
@ -218,10 +234,17 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
|||||||
timeWeightedInvestmentValues: {},
|
timeWeightedInvestmentValues: {},
|
||||||
timeWeightedInvestmentValuesWithCurrencyEffect: {},
|
timeWeightedInvestmentValuesWithCurrencyEffect: {},
|
||||||
timeWeightedInvestmentWithCurrencyEffect: new Big(0),
|
timeWeightedInvestmentWithCurrencyEffect: new Big(0),
|
||||||
|
totalAccountBalanceInBaseCurrency: new Big(0),
|
||||||
totalDividend: new Big(0),
|
totalDividend: new Big(0),
|
||||||
totalDividendInBaseCurrency: new Big(0),
|
totalDividendInBaseCurrency: new Big(0),
|
||||||
|
totalInterest: new Big(0),
|
||||||
|
totalInterestInBaseCurrency: new Big(0),
|
||||||
totalInvestment: new Big(0),
|
totalInvestment: new Big(0),
|
||||||
totalInvestmentWithCurrencyEffect: new Big(0)
|
totalInvestmentWithCurrencyEffect: new Big(0),
|
||||||
|
totalLiabilities: new Big(0),
|
||||||
|
totalLiabilitiesInBaseCurrency: new Big(0),
|
||||||
|
totalValuables: new Big(0),
|
||||||
|
totalValuablesInBaseCurrency: new Big(0)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -240,6 +263,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
|||||||
return {
|
return {
|
||||||
currentValues: {},
|
currentValues: {},
|
||||||
currentValuesWithCurrencyEffect: {},
|
currentValuesWithCurrencyEffect: {},
|
||||||
|
feesWithCurrencyEffect: new Big(0),
|
||||||
grossPerformance: new Big(0),
|
grossPerformance: new Big(0),
|
||||||
grossPerformancePercentage: new Big(0),
|
grossPerformancePercentage: new Big(0),
|
||||||
grossPerformancePercentageWithCurrencyEffect: new Big(0),
|
grossPerformancePercentageWithCurrencyEffect: new Big(0),
|
||||||
@ -260,10 +284,17 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
|||||||
timeWeightedInvestmentValues: {},
|
timeWeightedInvestmentValues: {},
|
||||||
timeWeightedInvestmentValuesWithCurrencyEffect: {},
|
timeWeightedInvestmentValuesWithCurrencyEffect: {},
|
||||||
timeWeightedInvestmentWithCurrencyEffect: new Big(0),
|
timeWeightedInvestmentWithCurrencyEffect: new Big(0),
|
||||||
|
totalAccountBalanceInBaseCurrency: new Big(0),
|
||||||
totalDividend: new Big(0),
|
totalDividend: new Big(0),
|
||||||
totalDividendInBaseCurrency: new Big(0),
|
totalDividendInBaseCurrency: new Big(0),
|
||||||
|
totalInterest: new Big(0),
|
||||||
|
totalInterestInBaseCurrency: new Big(0),
|
||||||
totalInvestment: new Big(0),
|
totalInvestment: new Big(0),
|
||||||
totalInvestmentWithCurrencyEffect: new Big(0)
|
totalInvestmentWithCurrencyEffect: new Big(0),
|
||||||
|
totalLiabilities: new Big(0),
|
||||||
|
totalLiabilitiesInBaseCurrency: new Big(0),
|
||||||
|
totalValuables: new Big(0),
|
||||||
|
totalValuablesInBaseCurrency: new Big(0)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -302,8 +333,10 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
|||||||
if (isChartMode) {
|
if (isChartMode) {
|
||||||
const datesWithOrders = {};
|
const datesWithOrders = {};
|
||||||
|
|
||||||
for (const order of orders) {
|
for (const { date, type } of orders) {
|
||||||
datesWithOrders[order.date] = true;
|
if (['BUY', 'SELL'].includes(type)) {
|
||||||
|
datesWithOrders[date] = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
while (isBefore(day, end)) {
|
while (isBefore(day, end)) {
|
||||||
@ -364,11 +397,46 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
|||||||
if (PortfolioCalculator.ENABLE_LOGGING) {
|
if (PortfolioCalculator.ENABLE_LOGGING) {
|
||||||
console.log();
|
console.log();
|
||||||
console.log();
|
console.log();
|
||||||
console.log(i + 1, order.type, order.itemType);
|
console.log(
|
||||||
|
i + 1,
|
||||||
|
order.date,
|
||||||
|
order.type,
|
||||||
|
order.itemType ? `(${order.itemType})` : ''
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const exchangeRateAtOrderDate = exchangeRates[order.date];
|
const exchangeRateAtOrderDate = exchangeRates[order.date];
|
||||||
|
|
||||||
|
if (order.type === 'DIVIDEND') {
|
||||||
|
const dividend = order.quantity.mul(order.unitPrice);
|
||||||
|
|
||||||
|
totalDividend = totalDividend.plus(dividend);
|
||||||
|
totalDividendInBaseCurrency = totalDividendInBaseCurrency.plus(
|
||||||
|
dividend.mul(exchangeRateAtOrderDate ?? 1)
|
||||||
|
);
|
||||||
|
} else if (order.type === 'INTEREST') {
|
||||||
|
const interest = order.quantity.mul(order.unitPrice);
|
||||||
|
|
||||||
|
totalInterest = totalInterest.plus(interest);
|
||||||
|
totalInterestInBaseCurrency = totalInterestInBaseCurrency.plus(
|
||||||
|
interest.mul(exchangeRateAtOrderDate ?? 1)
|
||||||
|
);
|
||||||
|
} else if (order.type === 'ITEM') {
|
||||||
|
const valuables = order.quantity.mul(order.unitPrice);
|
||||||
|
|
||||||
|
totalValuables = totalValuables.plus(valuables);
|
||||||
|
totalValuablesInBaseCurrency = totalValuablesInBaseCurrency.plus(
|
||||||
|
valuables.mul(exchangeRateAtOrderDate ?? 1)
|
||||||
|
);
|
||||||
|
} else if (order.type === 'LIABILITY') {
|
||||||
|
const liabilities = order.quantity.mul(order.unitPrice);
|
||||||
|
|
||||||
|
totalLiabilities = totalLiabilities.plus(liabilities);
|
||||||
|
totalLiabilitiesInBaseCurrency = totalLiabilitiesInBaseCurrency.plus(
|
||||||
|
liabilities.mul(exchangeRateAtOrderDate ?? 1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (order.itemType === 'start') {
|
if (order.itemType === 'start') {
|
||||||
// Take the unit price of the order as the market price if there are no
|
// Take the unit price of the order as the market price if there are no
|
||||||
// orders of this symbol before the start date
|
// orders of this symbol before the start date
|
||||||
@ -451,13 +519,6 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (PortfolioCalculator.ENABLE_LOGGING) {
|
if (PortfolioCalculator.ENABLE_LOGGING) {
|
||||||
console.log('totalInvestment', totalInvestment.toNumber());
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
'totalInvestmentWithCurrencyEffect',
|
|
||||||
totalInvestmentWithCurrencyEffect.toNumber()
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('order.quantity', order.quantity.toNumber());
|
console.log('order.quantity', order.quantity.toNumber());
|
||||||
console.log('transactionInvestment', transactionInvestment.toNumber());
|
console.log('transactionInvestment', transactionInvestment.toNumber());
|
||||||
|
|
||||||
@ -504,15 +565,6 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
|||||||
|
|
||||||
totalUnits = totalUnits.plus(order.quantity.mul(getFactor(order.type)));
|
totalUnits = totalUnits.plus(order.quantity.mul(getFactor(order.type)));
|
||||||
|
|
||||||
if (order.type === 'DIVIDEND') {
|
|
||||||
const dividend = order.quantity.mul(order.unitPrice);
|
|
||||||
|
|
||||||
totalDividend = totalDividend.plus(dividend);
|
|
||||||
totalDividendInBaseCurrency = totalDividendInBaseCurrency.plus(
|
|
||||||
dividend.mul(exchangeRateAtOrderDate ?? 1)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const valueOfInvestment = totalUnits.mul(order.unitPriceInBaseCurrency);
|
const valueOfInvestment = totalUnits.mul(order.unitPriceInBaseCurrency);
|
||||||
|
|
||||||
const valueOfInvestmentWithCurrencyEffect = totalUnits.mul(
|
const valueOfInvestmentWithCurrencyEffect = totalUnits.mul(
|
||||||
@ -808,6 +860,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
|||||||
return {
|
return {
|
||||||
currentValues,
|
currentValues,
|
||||||
currentValuesWithCurrencyEffect,
|
currentValuesWithCurrencyEffect,
|
||||||
|
feesWithCurrencyEffect,
|
||||||
grossPerformancePercentage,
|
grossPerformancePercentage,
|
||||||
grossPerformancePercentageWithCurrencyEffect,
|
grossPerformancePercentageWithCurrencyEffect,
|
||||||
initialValue,
|
initialValue,
|
||||||
@ -821,10 +874,17 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
|||||||
netPerformanceValuesWithCurrencyEffect,
|
netPerformanceValuesWithCurrencyEffect,
|
||||||
timeWeightedInvestmentValues,
|
timeWeightedInvestmentValues,
|
||||||
timeWeightedInvestmentValuesWithCurrencyEffect,
|
timeWeightedInvestmentValuesWithCurrencyEffect,
|
||||||
|
totalAccountBalanceInBaseCurrency,
|
||||||
totalDividend,
|
totalDividend,
|
||||||
totalDividendInBaseCurrency,
|
totalDividendInBaseCurrency,
|
||||||
|
totalInterest,
|
||||||
|
totalInterestInBaseCurrency,
|
||||||
totalInvestment,
|
totalInvestment,
|
||||||
totalInvestmentWithCurrencyEffect,
|
totalInvestmentWithCurrencyEffect,
|
||||||
|
totalLiabilities,
|
||||||
|
totalLiabilitiesInBaseCurrency,
|
||||||
|
totalValuables,
|
||||||
|
totalValuablesInBaseCurrency,
|
||||||
grossPerformance: totalGrossPerformance,
|
grossPerformance: totalGrossPerformance,
|
||||||
grossPerformanceWithCurrencyEffect:
|
grossPerformanceWithCurrencyEffect:
|
||||||
totalGrossPerformanceWithCurrencyEffect,
|
totalGrossPerformanceWithCurrencyEffect,
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
import { ResponseError, TimelinePosition } from '@ghostfolio/common/interfaces';
|
|
||||||
|
|
||||||
import { Big } from 'big.js';
|
|
||||||
|
|
||||||
export interface CurrentPositions extends ResponseError {
|
|
||||||
currentValueInBaseCurrency: Big;
|
|
||||||
grossPerformance: Big;
|
|
||||||
grossPerformanceWithCurrencyEffect: Big;
|
|
||||||
grossPerformancePercentage: Big;
|
|
||||||
grossPerformancePercentageWithCurrencyEffect: Big;
|
|
||||||
netAnnualizedPerformance?: Big;
|
|
||||||
netAnnualizedPerformanceWithCurrencyEffect?: Big;
|
|
||||||
netPerformance: Big;
|
|
||||||
netPerformanceWithCurrencyEffect: Big;
|
|
||||||
netPerformancePercentage: Big;
|
|
||||||
netPerformancePercentageWithCurrencyEffect: Big;
|
|
||||||
positions: TimelinePosition[];
|
|
||||||
totalInvestment: Big;
|
|
||||||
totalInvestmentWithCurrencyEffect: Big;
|
|
||||||
}
|
|
@ -1,6 +1,12 @@
|
|||||||
|
import { Big } from 'big.js';
|
||||||
|
|
||||||
import { TransactionPointSymbol } from './transaction-point-symbol.interface';
|
import { TransactionPointSymbol } from './transaction-point-symbol.interface';
|
||||||
|
|
||||||
export interface TransactionPoint {
|
export interface TransactionPoint {
|
||||||
date: string;
|
date: string;
|
||||||
|
fees: Big;
|
||||||
|
interest: Big;
|
||||||
items: TransactionPointSymbol[];
|
items: TransactionPointSymbol[];
|
||||||
|
liabilities: Big;
|
||||||
|
valuables: Big;
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,7 @@ import {
|
|||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { AssetClass, AssetSubClass } from '@prisma/client';
|
||||||
import { Big } from 'big.js';
|
import { Big } from 'big.js';
|
||||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
|
||||||
@ -78,10 +79,8 @@ export class PortfolioController {
|
|||||||
@Query('assetClasses') filterByAssetClasses?: string,
|
@Query('assetClasses') filterByAssetClasses?: string,
|
||||||
@Query('range') dateRange: DateRange = 'max',
|
@Query('range') dateRange: DateRange = 'max',
|
||||||
@Query('tags') filterByTags?: string,
|
@Query('tags') filterByTags?: string,
|
||||||
@Query('withLiabilities') withLiabilitiesParam = 'false',
|
|
||||||
@Query('withMarkets') withMarketsParam = 'false'
|
@Query('withMarkets') withMarketsParam = 'false'
|
||||||
): Promise<PortfolioDetails & { hasError: boolean }> {
|
): Promise<PortfolioDetails & { hasError: boolean }> {
|
||||||
const withLiabilities = withLiabilitiesParam === 'true';
|
|
||||||
const withMarkets = withMarketsParam === 'true';
|
const withMarkets = withMarketsParam === 'true';
|
||||||
|
|
||||||
let hasDetails = true;
|
let hasDetails = true;
|
||||||
@ -107,7 +106,6 @@ export class PortfolioController {
|
|||||||
dateRange,
|
dateRange,
|
||||||
filters,
|
filters,
|
||||||
impersonationId,
|
impersonationId,
|
||||||
withLiabilities,
|
|
||||||
withMarkets,
|
withMarkets,
|
||||||
userId: this.request.user.id,
|
userId: this.request.user.id,
|
||||||
withSummary: true
|
withSummary: true
|
||||||
@ -131,14 +129,19 @@ export class PortfolioController {
|
|||||||
|
|
||||||
const totalValue = Object.values(holdings)
|
const totalValue = Object.values(holdings)
|
||||||
.filter(({ assetClass, assetSubClass }) => {
|
.filter(({ assetClass, assetSubClass }) => {
|
||||||
return assetClass !== 'CASH' && assetSubClass !== 'CASH';
|
return (
|
||||||
|
assetClass !== AssetClass.LIQUIDITY &&
|
||||||
|
assetSubClass !== AssetSubClass.CASH
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.map(({ valueInBaseCurrency }) => {
|
.map(({ valueInBaseCurrency }) => {
|
||||||
return valueInBaseCurrency;
|
return valueInBaseCurrency;
|
||||||
})
|
})
|
||||||
.reduce((a, b) => a + b, 0);
|
.reduce((a, b) => {
|
||||||
|
return a + b;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
|
for (const [, portfolioPosition] of Object.entries(holdings)) {
|
||||||
portfolioPosition.investment =
|
portfolioPosition.investment =
|
||||||
portfolioPosition.investment / totalInvestment;
|
portfolioPosition.investment / totalInvestment;
|
||||||
portfolioPosition.valueInPercentage =
|
portfolioPosition.valueInPercentage =
|
||||||
@ -188,11 +191,11 @@ export class PortfolioController {
|
|||||||
holdings[symbol] = {
|
holdings[symbol] = {
|
||||||
...portfolioPosition,
|
...portfolioPosition,
|
||||||
assetClass:
|
assetClass:
|
||||||
hasDetails || portfolioPosition.assetClass === 'CASH'
|
hasDetails || portfolioPosition.assetClass === AssetClass.LIQUIDITY
|
||||||
? portfolioPosition.assetClass
|
? portfolioPosition.assetClass
|
||||||
: undefined,
|
: undefined,
|
||||||
assetSubClass:
|
assetSubClass:
|
||||||
hasDetails || portfolioPosition.assetSubClass === 'CASH'
|
hasDetails || portfolioPosition.assetSubClass === AssetSubClass.CASH
|
||||||
? portfolioPosition.assetSubClass
|
? portfolioPosition.assetSubClass
|
||||||
: undefined,
|
: undefined,
|
||||||
countries: hasDetails ? portfolioPosition.countries : [],
|
countries: hasDetails ? portfolioPosition.countries : [],
|
||||||
@ -293,6 +296,7 @@ export class PortfolioController {
|
|||||||
@Query('assetClasses') filterByAssetClasses?: string,
|
@Query('assetClasses') filterByAssetClasses?: string,
|
||||||
@Query('holdingType') filterByHoldingType?: string,
|
@Query('holdingType') filterByHoldingType?: string,
|
||||||
@Query('query') filterBySearchQuery?: string,
|
@Query('query') filterBySearchQuery?: string,
|
||||||
|
@Query('range') dateRange: DateRange = 'max',
|
||||||
@Query('tags') filterByTags?: string
|
@Query('tags') filterByTags?: string
|
||||||
): Promise<PortfolioHoldingsResponse> {
|
): Promise<PortfolioHoldingsResponse> {
|
||||||
const filters = this.apiService.buildFiltersFromQueryParams({
|
const filters = this.apiService.buildFiltersFromQueryParams({
|
||||||
@ -304,6 +308,7 @@ export class PortfolioController {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { holdings } = await this.portfolioService.getDetails({
|
const { holdings } = await this.portfolioService.getDetails({
|
||||||
|
dateRange,
|
||||||
filters,
|
filters,
|
||||||
impersonationId,
|
impersonationId,
|
||||||
userId: this.request.user.id
|
userId: this.request.user.id
|
||||||
@ -389,11 +394,9 @@ export class PortfolioController {
|
|||||||
@Query('assetClasses') filterByAssetClasses?: string,
|
@Query('assetClasses') filterByAssetClasses?: string,
|
||||||
@Query('range') dateRange: DateRange = 'max',
|
@Query('range') dateRange: DateRange = 'max',
|
||||||
@Query('tags') filterByTags?: string,
|
@Query('tags') filterByTags?: string,
|
||||||
@Query('withExcludedAccounts') withExcludedAccountsParam = 'false',
|
@Query('withExcludedAccounts') withExcludedAccountsParam = 'false'
|
||||||
@Query('withItems') withItemsParam = 'false'
|
|
||||||
): Promise<PortfolioPerformanceResponse> {
|
): Promise<PortfolioPerformanceResponse> {
|
||||||
const withExcludedAccounts = withExcludedAccountsParam === 'true';
|
const withExcludedAccounts = withExcludedAccountsParam === 'true';
|
||||||
const withItems = withItemsParam === 'true';
|
|
||||||
|
|
||||||
const hasReadRestrictedAccessPermission =
|
const hasReadRestrictedAccessPermission =
|
||||||
this.userService.hasReadRestrictedAccessPermission({
|
this.userService.hasReadRestrictedAccessPermission({
|
||||||
@ -412,7 +415,6 @@ export class PortfolioController {
|
|||||||
filters,
|
filters,
|
||||||
impersonationId,
|
impersonationId,
|
||||||
withExcludedAccounts,
|
withExcludedAccounts,
|
||||||
withItems,
|
|
||||||
userId: this.request.user.id
|
userId: this.request.user.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import { AccessModule } from '@ghostfolio/api/app/access/access.module';
|
|||||||
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
|
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
|
||||||
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||||
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
|
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
|
||||||
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
||||||
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
|
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||||
@ -35,6 +36,7 @@ import { RulesService } from './rules.service';
|
|||||||
MarketDataModule,
|
MarketDataModule,
|
||||||
OrderModule,
|
OrderModule,
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
|
RedisCacheModule,
|
||||||
SymbolProfileModule,
|
SymbolProfileModule,
|
||||||
UserModule
|
UserModule
|
||||||
],
|
],
|
||||||
|
@ -23,17 +23,13 @@ import {
|
|||||||
EMERGENCY_FUND_TAG_ID,
|
EMERGENCY_FUND_TAG_ID,
|
||||||
UNKNOWN_KEY
|
UNKNOWN_KEY
|
||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
import {
|
import { DATE_FORMAT, getSum, parseDate } from '@ghostfolio/common/helper';
|
||||||
DATE_FORMAT,
|
|
||||||
getAllActivityTypes,
|
|
||||||
getSum,
|
|
||||||
parseDate
|
|
||||||
} from '@ghostfolio/common/helper';
|
|
||||||
import {
|
import {
|
||||||
Accounts,
|
Accounts,
|
||||||
EnhancedSymbolProfile,
|
EnhancedSymbolProfile,
|
||||||
Filter,
|
Filter,
|
||||||
HistoricalDataItem,
|
HistoricalDataItem,
|
||||||
|
InvestmentItem,
|
||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
PortfolioInvestments,
|
PortfolioInvestments,
|
||||||
PortfolioPerformanceResponse,
|
PortfolioPerformanceResponse,
|
||||||
@ -41,10 +37,9 @@ import {
|
|||||||
PortfolioReport,
|
PortfolioReport,
|
||||||
PortfolioSummary,
|
PortfolioSummary,
|
||||||
Position,
|
Position,
|
||||||
TimelinePosition,
|
|
||||||
UserSettings
|
UserSettings
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
import { TimelinePosition } from '@ghostfolio/common/models';
|
||||||
import type {
|
import type {
|
||||||
AccountWithValue,
|
AccountWithValue,
|
||||||
DateRange,
|
DateRange,
|
||||||
@ -59,18 +54,17 @@ import {
|
|||||||
Account,
|
Account,
|
||||||
Type as ActivityType,
|
Type as ActivityType,
|
||||||
AssetClass,
|
AssetClass,
|
||||||
|
AssetSubClass,
|
||||||
DataSource,
|
DataSource,
|
||||||
Order,
|
Order,
|
||||||
Platform,
|
Platform,
|
||||||
Prisma
|
Prisma
|
||||||
} from '@prisma/client';
|
} from '@prisma/client';
|
||||||
import { Big } from 'big.js';
|
import { Big } from 'big.js';
|
||||||
import { isUUID } from 'class-validator';
|
|
||||||
import {
|
import {
|
||||||
differenceInDays,
|
differenceInDays,
|
||||||
format,
|
format,
|
||||||
isAfter,
|
isAfter,
|
||||||
isBefore,
|
|
||||||
isSameMonth,
|
isSameMonth,
|
||||||
isSameYear,
|
isSameYear,
|
||||||
parseISO,
|
parseISO,
|
||||||
@ -78,6 +72,7 @@ import {
|
|||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import { isEmpty, isNumber, last, uniq, uniqBy } from 'lodash';
|
import { isEmpty, isNumber, last, uniq, uniqBy } from 'lodash';
|
||||||
|
|
||||||
|
import { PortfolioCalculator } from './calculator/portfolio-calculator';
|
||||||
import {
|
import {
|
||||||
PerformanceCalculationType,
|
PerformanceCalculationType,
|
||||||
PortfolioCalculatorFactory
|
PortfolioCalculatorFactory
|
||||||
@ -282,8 +277,11 @@ export class PortfolioService {
|
|||||||
|
|
||||||
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
||||||
activities,
|
activities,
|
||||||
|
userId,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: this.request.user.Settings.settings.baseCurrency
|
currency: this.request.user.Settings.settings.baseCurrency,
|
||||||
|
isExperimentalFeatures:
|
||||||
|
this.request.user.Settings.settings.isExperimentalFeatures
|
||||||
});
|
});
|
||||||
|
|
||||||
const items = await portfolioCalculator.getChart({
|
const items = await portfolioCalculator.getChart({
|
||||||
@ -328,7 +326,6 @@ export class PortfolioService {
|
|||||||
impersonationId,
|
impersonationId,
|
||||||
userId,
|
userId,
|
||||||
withExcludedAccounts = false,
|
withExcludedAccounts = false,
|
||||||
withLiabilities = false,
|
|
||||||
withMarkets = false,
|
withMarkets = false,
|
||||||
withSummary = false
|
withSummary = false
|
||||||
}: {
|
}: {
|
||||||
@ -337,7 +334,6 @@ export class PortfolioService {
|
|||||||
impersonationId: string;
|
impersonationId: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
withExcludedAccounts?: boolean;
|
withExcludedAccounts?: boolean;
|
||||||
withLiabilities?: boolean;
|
|
||||||
withMarkets?: boolean;
|
withMarkets?: boolean;
|
||||||
withSummary?: boolean;
|
withSummary?: boolean;
|
||||||
}): Promise<PortfolioDetails & { hasErrors: boolean }> {
|
}): Promise<PortfolioDetails & { hasErrors: boolean }> {
|
||||||
@ -349,19 +345,8 @@ export class PortfolioService {
|
|||||||
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
|
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
|
||||||
);
|
);
|
||||||
|
|
||||||
let types = getAllActivityTypes().filter((activityType) => {
|
|
||||||
return activityType !== 'FEE';
|
|
||||||
});
|
|
||||||
|
|
||||||
if (withLiabilities === false) {
|
|
||||||
types = types.filter((activityType) => {
|
|
||||||
return activityType !== 'LIABILITY';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { activities } = await this.orderService.getOrders({
|
const { activities } = await this.orderService.getOrders({
|
||||||
filters,
|
filters,
|
||||||
types,
|
|
||||||
userCurrency,
|
userCurrency,
|
||||||
userId,
|
userId,
|
||||||
withExcludedAccounts
|
withExcludedAccounts
|
||||||
@ -369,16 +354,16 @@ export class PortfolioService {
|
|||||||
|
|
||||||
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
||||||
activities,
|
activities,
|
||||||
|
dateRange,
|
||||||
|
userId,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: userCurrency
|
currency: userCurrency,
|
||||||
|
isExperimentalFeatures:
|
||||||
|
this.request.user?.Settings.settings.isExperimentalFeatures
|
||||||
});
|
});
|
||||||
|
|
||||||
const { startDate } = getInterval(
|
const { currentValueInBaseCurrency, hasErrors, positions } =
|
||||||
dateRange,
|
await portfolioCalculator.getSnapshot();
|
||||||
portfolioCalculator.getStartDate()
|
|
||||||
);
|
|
||||||
const currentPositions =
|
|
||||||
await portfolioCalculator.getCurrentPositions(startDate);
|
|
||||||
|
|
||||||
const cashDetails = await this.accountService.getCashDetails({
|
const cashDetails = await this.accountService.getCashDetails({
|
||||||
filters,
|
filters,
|
||||||
@ -388,10 +373,9 @@ export class PortfolioService {
|
|||||||
|
|
||||||
const holdings: PortfolioDetails['holdings'] = {};
|
const holdings: PortfolioDetails['holdings'] = {};
|
||||||
|
|
||||||
const totalValueInBaseCurrency =
|
const totalValueInBaseCurrency = currentValueInBaseCurrency.plus(
|
||||||
currentPositions.currentValueInBaseCurrency.plus(
|
cashDetails.balanceInBaseCurrency
|
||||||
cashDetails.balanceInBaseCurrency
|
);
|
||||||
);
|
|
||||||
|
|
||||||
const isFilteredByAccount =
|
const isFilteredByAccount =
|
||||||
filters?.some(({ type }) => {
|
filters?.some(({ type }) => {
|
||||||
@ -399,7 +383,7 @@ export class PortfolioService {
|
|||||||
}) ?? false;
|
}) ?? false;
|
||||||
|
|
||||||
const isFilteredByCash = filters?.some(({ id, type }) => {
|
const isFilteredByCash = filters?.some(({ id, type }) => {
|
||||||
return id === 'CASH' && type === 'ASSET_CLASS';
|
return id === AssetClass.LIQUIDITY && type === 'ASSET_CLASS';
|
||||||
});
|
});
|
||||||
|
|
||||||
const isFilteredByClosedHoldings =
|
const isFilteredByClosedHoldings =
|
||||||
@ -409,27 +393,25 @@ export class PortfolioService {
|
|||||||
|
|
||||||
let filteredValueInBaseCurrency = isFilteredByAccount
|
let filteredValueInBaseCurrency = isFilteredByAccount
|
||||||
? totalValueInBaseCurrency
|
? totalValueInBaseCurrency
|
||||||
: currentPositions.currentValueInBaseCurrency;
|
: currentValueInBaseCurrency;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
filters?.length === 0 ||
|
filters?.length === 0 ||
|
||||||
(filters?.length === 1 &&
|
(filters?.length === 1 &&
|
||||||
filters[0].type === 'ASSET_CLASS' &&
|
filters[0].id === AssetClass.LIQUIDITY &&
|
||||||
filters[0].id === 'CASH')
|
filters[0].type === 'ASSET_CLASS')
|
||||||
) {
|
) {
|
||||||
filteredValueInBaseCurrency = filteredValueInBaseCurrency.plus(
|
filteredValueInBaseCurrency = filteredValueInBaseCurrency.plus(
|
||||||
cashDetails.balanceInBaseCurrency
|
cashDetails.balanceInBaseCurrency
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataGatheringItems = currentPositions.positions.map(
|
const dataGatheringItems = positions.map(({ dataSource, symbol }) => {
|
||||||
({ dataSource, symbol }) => {
|
return {
|
||||||
return {
|
dataSource,
|
||||||
dataSource,
|
symbol
|
||||||
symbol
|
};
|
||||||
};
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
||||||
this.dataProviderService.getQuotes({ user, items: dataGatheringItems }),
|
this.dataProviderService.getQuotes({ user, items: dataGatheringItems }),
|
||||||
@ -442,7 +424,7 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const portfolioItemsNow: { [symbol: string]: TimelinePosition } = {};
|
const portfolioItemsNow: { [symbol: string]: TimelinePosition } = {};
|
||||||
for (const position of currentPositions.positions) {
|
for (const position of positions) {
|
||||||
portfolioItemsNow[position.symbol] = position;
|
portfolioItemsNow[position.symbol] = position;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -465,7 +447,7 @@ export class PortfolioService {
|
|||||||
tags,
|
tags,
|
||||||
transactionCount,
|
transactionCount,
|
||||||
valueInBaseCurrency
|
valueInBaseCurrency
|
||||||
} of currentPositions.positions) {
|
} of positions) {
|
||||||
if (isFilteredByClosedHoldings === true) {
|
if (isFilteredByClosedHoldings === true) {
|
||||||
if (!quantity.eq(0)) {
|
if (!quantity.eq(0)) {
|
||||||
// Ignore positions with a quantity
|
// Ignore positions with a quantity
|
||||||
@ -593,6 +575,7 @@ export class PortfolioService {
|
|||||||
filteredValueInBaseCurrency,
|
filteredValueInBaseCurrency,
|
||||||
holdings,
|
holdings,
|
||||||
impersonationId,
|
impersonationId,
|
||||||
|
portfolioCalculator,
|
||||||
userCurrency,
|
userCurrency,
|
||||||
userId,
|
userId,
|
||||||
balanceInBaseCurrency: cashDetails.balanceInBaseCurrency,
|
balanceInBaseCurrency: cashDetails.balanceInBaseCurrency,
|
||||||
@ -605,10 +588,10 @@ export class PortfolioService {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
accounts,
|
accounts,
|
||||||
|
hasErrors,
|
||||||
holdings,
|
holdings,
|
||||||
platforms,
|
platforms,
|
||||||
summary,
|
summary
|
||||||
hasErrors: currentPositions.hasErrors
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -671,20 +654,22 @@ export class PortfolioService {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
||||||
|
userId,
|
||||||
activities: orders.filter((order) => {
|
activities: orders.filter((order) => {
|
||||||
return ['BUY', 'DIVIDEND', 'ITEM', 'SELL'].includes(order.type);
|
return ['BUY', 'DIVIDEND', 'ITEM', 'SELL'].includes(order.type);
|
||||||
}),
|
}),
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: userCurrency
|
currency: userCurrency,
|
||||||
|
isExperimentalFeatures:
|
||||||
|
this.request.user.Settings.settings.isExperimentalFeatures
|
||||||
});
|
});
|
||||||
|
|
||||||
const portfolioStart = portfolioCalculator.getStartDate();
|
const portfolioStart = portfolioCalculator.getStartDate();
|
||||||
const transactionPoints = portfolioCalculator.getTransactionPoints();
|
const transactionPoints = portfolioCalculator.getTransactionPoints();
|
||||||
|
|
||||||
const currentPositions =
|
const { positions } = await portfolioCalculator.getSnapshot();
|
||||||
await portfolioCalculator.getCurrentPositions(portfolioStart);
|
|
||||||
|
|
||||||
const position = currentPositions.positions.find(({ symbol }) => {
|
const position = positions.find(({ symbol }) => {
|
||||||
return symbol === aSymbol;
|
return symbol === aSymbol;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -846,11 +831,19 @@ export class PortfolioService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (isEmpty(historicalData)) {
|
if (isEmpty(historicalData)) {
|
||||||
historicalData = await this.dataProviderService.getHistoricalRaw(
|
try {
|
||||||
[{ dataSource: DataSource.YAHOO, symbol: aSymbol }],
|
historicalData = await this.dataProviderService.getHistoricalRaw({
|
||||||
portfolioStart,
|
dataGatheringItems: [
|
||||||
new Date()
|
{ dataSource: DataSource.YAHOO, symbol: aSymbol }
|
||||||
);
|
],
|
||||||
|
from: portfolioStart,
|
||||||
|
to: new Date()
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
historicalData = {
|
||||||
|
[aSymbol]: {}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const historicalDataArray: HistoricalDataItem[] = [];
|
const historicalDataArray: HistoricalDataItem[] = [];
|
||||||
@ -916,13 +909,12 @@ export class PortfolioService {
|
|||||||
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
||||||
const user = await this.userService.user({ id: userId });
|
const user = await this.userService.user({ id: userId });
|
||||||
|
|
||||||
const { endDate, startDate } = getInterval(dateRange);
|
const { endDate } = getInterval(dateRange);
|
||||||
|
|
||||||
const { activities } = await this.orderService.getOrders({
|
const { activities } = await this.orderService.getOrders({
|
||||||
endDate,
|
endDate,
|
||||||
filters,
|
filters,
|
||||||
userId,
|
userId,
|
||||||
types: ['BUY', 'SELL'],
|
|
||||||
userCurrency: this.getUserCurrency()
|
userCurrency: this.getUserCurrency()
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -935,16 +927,17 @@ export class PortfolioService {
|
|||||||
|
|
||||||
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
||||||
activities,
|
activities,
|
||||||
|
dateRange,
|
||||||
|
userId,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: this.request.user.Settings.settings.baseCurrency
|
currency: this.request.user.Settings.settings.baseCurrency,
|
||||||
|
isExperimentalFeatures:
|
||||||
|
this.request.user.Settings.settings.isExperimentalFeatures
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
let { hasErrors, positions } = await portfolioCalculator.getSnapshot();
|
||||||
startDate,
|
|
||||||
endDate
|
|
||||||
);
|
|
||||||
|
|
||||||
let positions = currentPositions.positions.filter(({ quantity }) => {
|
positions = positions.filter(({ quantity }) => {
|
||||||
return !quantity.eq(0);
|
return !quantity.eq(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -983,7 +976,7 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hasErrors: currentPositions.hasErrors,
|
hasErrors,
|
||||||
positions: positions.map(
|
positions: positions.map(
|
||||||
({
|
({
|
||||||
averagePrice,
|
averagePrice,
|
||||||
@ -1050,15 +1043,13 @@ export class PortfolioService {
|
|||||||
filters,
|
filters,
|
||||||
impersonationId,
|
impersonationId,
|
||||||
userId,
|
userId,
|
||||||
withExcludedAccounts = false,
|
withExcludedAccounts = false
|
||||||
withItems = false
|
|
||||||
}: {
|
}: {
|
||||||
dateRange?: DateRange;
|
dateRange?: DateRange;
|
||||||
filters?: Filter[];
|
filters?: Filter[];
|
||||||
impersonationId: string;
|
impersonationId: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
withExcludedAccounts?: boolean;
|
withExcludedAccounts?: boolean;
|
||||||
withItems?: boolean;
|
|
||||||
}): Promise<PortfolioPerformanceResponse> {
|
}): Promise<PortfolioPerformanceResponse> {
|
||||||
userId = await this.getUserId(impersonationId, userId);
|
userId = await this.getUserId(impersonationId, userId);
|
||||||
const user = await this.userService.user({ id: userId });
|
const user = await this.userService.user({ id: userId });
|
||||||
@ -1077,11 +1068,16 @@ export class PortfolioService {
|
|||||||
) => {
|
) => {
|
||||||
const formattedDate = format(date, DATE_FORMAT);
|
const formattedDate = format(date, DATE_FORMAT);
|
||||||
|
|
||||||
// Store the item in the map, overwriting if the date already exists
|
if (map[formattedDate]) {
|
||||||
map[formattedDate] = {
|
// If the value exists, add the current value to the existing one
|
||||||
date: formattedDate,
|
map[formattedDate].value += valueInBaseCurrency;
|
||||||
value: valueInBaseCurrency
|
} else {
|
||||||
};
|
// Otherwise, initialize the value for that date
|
||||||
|
map[formattedDate] = {
|
||||||
|
date: formattedDate,
|
||||||
|
value: valueInBaseCurrency
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return map;
|
return map;
|
||||||
},
|
},
|
||||||
@ -1096,8 +1092,7 @@ export class PortfolioService {
|
|||||||
filters,
|
filters,
|
||||||
userCurrency,
|
userCurrency,
|
||||||
userId,
|
userId,
|
||||||
withExcludedAccounts,
|
withExcludedAccounts
|
||||||
types: withItems ? ['BUY', 'ITEM', 'SELL'] : ['BUY', 'SELL']
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (accountBalanceItems?.length <= 0 && activities?.length <= 0) {
|
if (accountBalanceItems?.length <= 0 && activities?.length <= 0) {
|
||||||
@ -1122,9 +1117,14 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
||||||
|
accountBalanceItems,
|
||||||
activities,
|
activities,
|
||||||
|
dateRange,
|
||||||
|
userId,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: userCurrency
|
currency: userCurrency,
|
||||||
|
isExperimentalFeatures:
|
||||||
|
this.request.user.Settings.settings.isExperimentalFeatures
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -1140,7 +1140,7 @@ export class PortfolioService {
|
|||||||
netPerformancePercentageWithCurrencyEffect,
|
netPerformancePercentageWithCurrencyEffect,
|
||||||
netPerformanceWithCurrencyEffect,
|
netPerformanceWithCurrencyEffect,
|
||||||
totalInvestment
|
totalInvestment
|
||||||
} = await portfolioCalculator.getCurrentPositions(startDate, endDate);
|
} = await portfolioCalculator.getSnapshot();
|
||||||
|
|
||||||
let currentNetPerformance = netPerformance;
|
let currentNetPerformance = netPerformance;
|
||||||
|
|
||||||
@ -1152,6 +1152,8 @@ export class PortfolioService {
|
|||||||
let currentNetPerformanceWithCurrencyEffect =
|
let currentNetPerformanceWithCurrencyEffect =
|
||||||
netPerformanceWithCurrencyEffect;
|
netPerformanceWithCurrencyEffect;
|
||||||
|
|
||||||
|
let currentNetWorth = 0;
|
||||||
|
|
||||||
const items = await portfolioCalculator.getChart({
|
const items = await portfolioCalculator.getChart({
|
||||||
dateRange
|
dateRange
|
||||||
});
|
});
|
||||||
@ -1174,35 +1176,14 @@ export class PortfolioService {
|
|||||||
currentNetPerformanceWithCurrencyEffect = new Big(
|
currentNetPerformanceWithCurrencyEffect = new Big(
|
||||||
itemOfToday.netPerformanceWithCurrencyEffect
|
itemOfToday.netPerformanceWithCurrencyEffect
|
||||||
);
|
);
|
||||||
|
|
||||||
|
currentNetWorth = itemOfToday.netWorth;
|
||||||
}
|
}
|
||||||
|
|
||||||
accountBalanceItems = accountBalanceItems.filter(({ date }) => {
|
|
||||||
return !isBefore(parseDate(date), startDate);
|
|
||||||
});
|
|
||||||
|
|
||||||
const accountBalanceItemOfToday = accountBalanceItems.find(({ date }) => {
|
|
||||||
return date === format(new Date(), DATE_FORMAT);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!accountBalanceItemOfToday) {
|
|
||||||
accountBalanceItems.push({
|
|
||||||
date: format(new Date(), DATE_FORMAT),
|
|
||||||
value: last(accountBalanceItems)?.value ?? 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const mergedHistoricalDataItems = this.mergeHistoricalDataItems(
|
|
||||||
accountBalanceItems,
|
|
||||||
items
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentHistoricalDataItem = last(mergedHistoricalDataItems);
|
|
||||||
const currentNetWorth = currentHistoricalDataItem?.netWorth ?? 0;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
errors,
|
errors,
|
||||||
hasErrors,
|
hasErrors,
|
||||||
chart: mergedHistoricalDataItems,
|
chart: items,
|
||||||
firstOrderDate: parseDate(items[0]?.date),
|
firstOrderDate: parseDate(items[0]?.date),
|
||||||
performance: {
|
performance: {
|
||||||
currentNetWorth,
|
currentNetWorth,
|
||||||
@ -1231,23 +1212,22 @@ export class PortfolioService {
|
|||||||
|
|
||||||
const { activities } = await this.orderService.getOrders({
|
const { activities } = await this.orderService.getOrders({
|
||||||
userCurrency,
|
userCurrency,
|
||||||
userId,
|
userId
|
||||||
types: ['BUY', 'SELL']
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
||||||
activities,
|
activities,
|
||||||
|
userId,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: this.request.user.Settings.settings.baseCurrency
|
currency: this.request.user.Settings.settings.baseCurrency,
|
||||||
|
isExperimentalFeatures:
|
||||||
|
this.request.user.Settings.settings.isExperimentalFeatures
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
let { totalFeesWithCurrencyEffect, positions, totalInvestment } =
|
||||||
portfolioCalculator.getStartDate()
|
await portfolioCalculator.getSnapshot();
|
||||||
);
|
|
||||||
|
|
||||||
const positions = currentPositions.positions.filter(
|
positions = positions.filter((item) => !item.quantity.eq(0));
|
||||||
(item) => !item.quantity.eq(0)
|
|
||||||
);
|
|
||||||
|
|
||||||
const portfolioItemsNow: { [symbol: string]: TimelinePosition } = {};
|
const portfolioItemsNow: { [symbol: string]: TimelinePosition } = {};
|
||||||
|
|
||||||
@ -1309,8 +1289,8 @@ export class PortfolioService {
|
|||||||
[
|
[
|
||||||
new FeeRatioInitialInvestment(
|
new FeeRatioInitialInvestment(
|
||||||
this.exchangeRateDataService,
|
this.exchangeRateDataService,
|
||||||
currentPositions.totalInvestment.toNumber(),
|
totalInvestment.toNumber(),
|
||||||
this.getFees({ activities, userCurrency }).toNumber()
|
totalFeesWithCurrencyEffect.toNumber()
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
userSettings
|
userSettings
|
||||||
@ -1454,30 +1434,6 @@ export class PortfolioService {
|
|||||||
return valueInBaseCurrencyOfEmergencyFundPositions.toNumber();
|
return valueInBaseCurrencyOfEmergencyFundPositions.toNumber();
|
||||||
}
|
}
|
||||||
|
|
||||||
private getFees({
|
|
||||||
activities,
|
|
||||||
userCurrency
|
|
||||||
}: {
|
|
||||||
activities: Activity[];
|
|
||||||
userCurrency: string;
|
|
||||||
}) {
|
|
||||||
return getSum(
|
|
||||||
activities
|
|
||||||
.filter(({ isDraft }) => {
|
|
||||||
return isDraft === false;
|
|
||||||
})
|
|
||||||
.map(({ fee, SymbolProfile }) => {
|
|
||||||
return new Big(
|
|
||||||
this.exchangeRateDataService.toCurrency(
|
|
||||||
fee,
|
|
||||||
SymbolProfile.currency,
|
|
||||||
userCurrency
|
|
||||||
)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getInitialCashPosition({
|
private getInitialCashPosition({
|
||||||
balance,
|
balance,
|
||||||
currency
|
currency
|
||||||
@ -1488,8 +1444,8 @@ export class PortfolioService {
|
|||||||
return {
|
return {
|
||||||
currency,
|
currency,
|
||||||
allocationInPercentage: 0,
|
allocationInPercentage: 0,
|
||||||
assetClass: AssetClass.CASH,
|
assetClass: AssetClass.LIQUIDITY,
|
||||||
assetSubClass: AssetClass.CASH,
|
assetSubClass: AssetSubClass.CASH,
|
||||||
countries: [],
|
countries: [],
|
||||||
dataSource: undefined,
|
dataSource: undefined,
|
||||||
dateOfFirstActivity: undefined,
|
dateOfFirstActivity: undefined,
|
||||||
@ -1623,6 +1579,7 @@ export class PortfolioService {
|
|||||||
filteredValueInBaseCurrency,
|
filteredValueInBaseCurrency,
|
||||||
holdings,
|
holdings,
|
||||||
impersonationId,
|
impersonationId,
|
||||||
|
portfolioCalculator,
|
||||||
userCurrency,
|
userCurrency,
|
||||||
userId
|
userId
|
||||||
}: {
|
}: {
|
||||||
@ -1631,6 +1588,7 @@ export class PortfolioService {
|
|||||||
filteredValueInBaseCurrency: Big;
|
filteredValueInBaseCurrency: Big;
|
||||||
holdings: PortfolioDetails['holdings'];
|
holdings: PortfolioDetails['holdings'];
|
||||||
impersonationId: string;
|
impersonationId: string;
|
||||||
|
portfolioCalculator: PortfolioCalculator;
|
||||||
userCurrency: string;
|
userCurrency: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
}): Promise<PortfolioSummary> {
|
}): Promise<PortfolioSummary> {
|
||||||
@ -1659,17 +1617,8 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const dividendInBaseCurrency = getSum(
|
const dividendInBaseCurrency =
|
||||||
(
|
await portfolioCalculator.getDividendInBaseCurrency();
|
||||||
await this.getDividends({
|
|
||||||
activities: activities.filter(({ type }) => {
|
|
||||||
return type === 'DIVIDEND';
|
|
||||||
})
|
|
||||||
})
|
|
||||||
).map(({ investment }) => {
|
|
||||||
return new Big(investment);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const emergencyFund = new Big(
|
const emergencyFund = new Big(
|
||||||
Math.max(
|
Math.max(
|
||||||
@ -1678,42 +1627,16 @@ export class PortfolioService {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const fees = this.getFees({ activities, userCurrency }).toNumber();
|
const fees = await portfolioCalculator.getFeesInBaseCurrency();
|
||||||
const firstOrderDate = activities[0]?.date;
|
|
||||||
|
|
||||||
const interest = this.getSumOfActivityType({
|
const firstOrderDate = portfolioCalculator.getStartDate();
|
||||||
activities,
|
|
||||||
userCurrency,
|
|
||||||
activityType: 'INTEREST'
|
|
||||||
}).toNumber();
|
|
||||||
|
|
||||||
const items = getSum(
|
const interest = await portfolioCalculator.getInterestInBaseCurrency();
|
||||||
Object.keys(holdings)
|
|
||||||
.filter((symbol) => {
|
|
||||||
return (
|
|
||||||
isUUID(symbol) &&
|
|
||||||
holdings[symbol].dataSource === 'MANUAL' &&
|
|
||||||
holdings[symbol].valueInBaseCurrency > 0
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.map((symbol) => {
|
|
||||||
return new Big(holdings[symbol].valueInBaseCurrency).abs();
|
|
||||||
})
|
|
||||||
).toNumber();
|
|
||||||
|
|
||||||
const liabilities = getSum(
|
const liabilities =
|
||||||
Object.keys(holdings)
|
await portfolioCalculator.getLiabilitiesInBaseCurrency();
|
||||||
.filter((symbol) => {
|
|
||||||
return (
|
const valuables = await portfolioCalculator.getValuablesInBaseCurrency();
|
||||||
isUUID(symbol) &&
|
|
||||||
holdings[symbol].dataSource === 'MANUAL' &&
|
|
||||||
holdings[symbol].valueInBaseCurrency < 0
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.map((symbol) => {
|
|
||||||
return new Big(holdings[symbol].valueInBaseCurrency).abs();
|
|
||||||
})
|
|
||||||
).toNumber();
|
|
||||||
|
|
||||||
const totalBuy = this.getSumOfActivityType({
|
const totalBuy = this.getSumOfActivityType({
|
||||||
userCurrency,
|
userCurrency,
|
||||||
@ -1763,7 +1686,7 @@ export class PortfolioService {
|
|||||||
|
|
||||||
const netWorth = new Big(balanceInBaseCurrency)
|
const netWorth = new Big(balanceInBaseCurrency)
|
||||||
.plus(performanceInformation.performance.currentValue)
|
.plus(performanceInformation.performance.currentValue)
|
||||||
.plus(items)
|
.plus(valuables)
|
||||||
.plus(excludedAccountsAndActivities)
|
.plus(excludedAccountsAndActivities)
|
||||||
.minus(liabilities)
|
.minus(liabilities)
|
||||||
.toNumber();
|
.toNumber();
|
||||||
@ -1791,11 +1714,7 @@ export class PortfolioService {
|
|||||||
annualizedPerformancePercentWithCurrencyEffect,
|
annualizedPerformancePercentWithCurrencyEffect,
|
||||||
cash,
|
cash,
|
||||||
excludedAccountsAndActivities,
|
excludedAccountsAndActivities,
|
||||||
fees,
|
|
||||||
firstOrderDate,
|
firstOrderDate,
|
||||||
interest,
|
|
||||||
items,
|
|
||||||
liabilities,
|
|
||||||
totalBuy,
|
totalBuy,
|
||||||
totalSell,
|
totalSell,
|
||||||
committedFunds: committedFunds.toNumber(),
|
committedFunds: committedFunds.toNumber(),
|
||||||
@ -1807,6 +1726,7 @@ export class PortfolioService {
|
|||||||
.toNumber(),
|
.toNumber(),
|
||||||
total: emergencyFund.toNumber()
|
total: emergencyFund.toNumber()
|
||||||
},
|
},
|
||||||
|
fees: fees.toNumber(),
|
||||||
filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(),
|
filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(),
|
||||||
filteredValueInPercentage: netWorth
|
filteredValueInPercentage: netWorth
|
||||||
? filteredValueInBaseCurrency.div(netWorth).toNumber()
|
? filteredValueInBaseCurrency.div(netWorth).toNumber()
|
||||||
@ -1814,6 +1734,9 @@ export class PortfolioService {
|
|||||||
fireWealth: new Big(performanceInformation.performance.currentValue)
|
fireWealth: new Big(performanceInformation.performance.currentValue)
|
||||||
.minus(emergencyFundPositionsValueInBaseCurrency)
|
.minus(emergencyFundPositionsValueInBaseCurrency)
|
||||||
.toNumber(),
|
.toNumber(),
|
||||||
|
interest: interest.toNumber(),
|
||||||
|
items: valuables.toNumber(),
|
||||||
|
liabilities: liabilities.toNumber(),
|
||||||
ordersCount: activities.filter(({ type }) => {
|
ordersCount: activities.filter(({ type }) => {
|
||||||
return type === 'BUY' || type === 'SELL';
|
return type === 'BUY' || type === 'SELL';
|
||||||
}).length,
|
}).length,
|
||||||
@ -1991,44 +1914,4 @@ export class PortfolioService {
|
|||||||
|
|
||||||
return { accounts, platforms };
|
return { accounts, platforms };
|
||||||
}
|
}
|
||||||
|
|
||||||
private mergeHistoricalDataItems(
|
|
||||||
accountBalanceItems: HistoricalDataItem[],
|
|
||||||
performanceChartItems: HistoricalDataItem[]
|
|
||||||
): HistoricalDataItem[] {
|
|
||||||
const historicalDataItemsMap: { [date: string]: HistoricalDataItem } = {};
|
|
||||||
let latestAccountBalance = 0;
|
|
||||||
|
|
||||||
for (const item of accountBalanceItems.concat(performanceChartItems)) {
|
|
||||||
const isAccountBalanceItem = accountBalanceItems.includes(item);
|
|
||||||
|
|
||||||
const totalAccountBalance = isAccountBalanceItem
|
|
||||||
? item.value
|
|
||||||
: latestAccountBalance;
|
|
||||||
|
|
||||||
if (isAccountBalanceItem && performanceChartItems.length > 0) {
|
|
||||||
latestAccountBalance = item.value;
|
|
||||||
} else {
|
|
||||||
historicalDataItemsMap[item.date] = {
|
|
||||||
...item,
|
|
||||||
totalAccountBalance,
|
|
||||||
netWorth:
|
|
||||||
(isAccountBalanceItem ? 0 : item.value) + totalAccountBalance
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to an array and sort by date in ascending order
|
|
||||||
const historicalDataItems = Object.keys(historicalDataItemsMap).map(
|
|
||||||
(date) => {
|
|
||||||
return historicalDataItemsMap[date];
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
historicalDataItems.sort((a, b) => {
|
|
||||||
return new Date(a.date).getTime() - new Date(b.date).getTime();
|
|
||||||
});
|
|
||||||
|
|
||||||
return historicalDataItems;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
13
apps/api/src/app/redis-cache/redis-cache.service.mock.ts
Normal file
13
apps/api/src/app/redis-cache/redis-cache.service.mock.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { RedisCacheService } from './redis-cache.service';
|
||||||
|
|
||||||
|
export const RedisCacheServiceMock = {
|
||||||
|
get: (key: string): Promise<string> => {
|
||||||
|
return Promise.resolve(null);
|
||||||
|
},
|
||||||
|
getPortfolioSnapshotKey: (userId: string): string => {
|
||||||
|
return `portfolio-snapshot-${userId}`;
|
||||||
|
},
|
||||||
|
set: (key: string, value: string, ttlInSeconds?: number): Promise<string> => {
|
||||||
|
return Promise.resolve(value);
|
||||||
|
}
|
||||||
|
};
|
@ -24,6 +24,10 @@ export class RedisCacheService {
|
|||||||
return this.cache.get(key);
|
return this.cache.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getPortfolioSnapshotKey(userId: string) {
|
||||||
|
return `portfolio-snapshot-${userId}`;
|
||||||
|
}
|
||||||
|
|
||||||
public getQuoteKey({ dataSource, symbol }: UniqueAsset) {
|
public getQuoteKey({ dataSource, symbol }: UniqueAsset) {
|
||||||
return `quote-${getAssetProfileIdentifier({ dataSource, symbol })}`;
|
return `quote-${getAssetProfileIdentifier({ dataSource, symbol })}`;
|
||||||
}
|
}
|
||||||
|
@ -74,11 +74,21 @@ export class SymbolService {
|
|||||||
date = new Date(),
|
date = new Date(),
|
||||||
symbol
|
symbol
|
||||||
}: IDataGatheringItem): Promise<IDataProviderHistoricalResponse> {
|
}: IDataGatheringItem): Promise<IDataProviderHistoricalResponse> {
|
||||||
const historicalData = await this.dataProviderService.getHistoricalRaw(
|
let historicalData: {
|
||||||
[{ dataSource, symbol }],
|
[symbol: string]: {
|
||||||
date,
|
[date: string]: IDataProviderHistoricalResponse;
|
||||||
date
|
};
|
||||||
);
|
} = {
|
||||||
|
[symbol]: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
historicalData = await this.dataProviderService.getHistoricalRaw({
|
||||||
|
dataGatheringItems: [{ dataSource, symbol }],
|
||||||
|
from: date,
|
||||||
|
to: date
|
||||||
|
});
|
||||||
|
} catch {}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
marketPrice:
|
marketPrice:
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
|
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
|
||||||
import { environment } from '@ghostfolio/api/environments/environment';
|
import { environment } from '@ghostfolio/api/environments/environment';
|
||||||
|
import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import { I18nService } from '@ghostfolio/api/services/i18n/i18n.service';
|
import { I18nService } from '@ghostfolio/api/services/i18n/i18n.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||||
@ -25,6 +26,7 @@ import {
|
|||||||
import { UserWithSettings } from '@ghostfolio/common/types';
|
import { UserWithSettings } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
import { Prisma, Role, User } from '@prisma/client';
|
import { Prisma, Role, User } from '@prisma/client';
|
||||||
import { differenceInDays } from 'date-fns';
|
import { differenceInDays } from 'date-fns';
|
||||||
import { sortBy, without } from 'lodash';
|
import { sortBy, without } from 'lodash';
|
||||||
@ -37,6 +39,7 @@ export class UserService {
|
|||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly configurationService: ConfigurationService,
|
private readonly configurationService: ConfigurationService,
|
||||||
|
private readonly eventEmitter: EventEmitter2,
|
||||||
private readonly prismaService: PrismaService,
|
private readonly prismaService: PrismaService,
|
||||||
private readonly propertyService: PropertyService,
|
private readonly propertyService: PropertyService,
|
||||||
private readonly subscriptionService: SubscriptionService,
|
private readonly subscriptionService: SubscriptionService,
|
||||||
@ -437,11 +440,9 @@ export class UserService {
|
|||||||
userId: string;
|
userId: string;
|
||||||
userSettings: UserSettings;
|
userSettings: UserSettings;
|
||||||
}) {
|
}) {
|
||||||
const settings = userSettings as unknown as Prisma.JsonObject;
|
const { settings } = await this.prismaService.settings.upsert({
|
||||||
|
|
||||||
await this.prismaService.settings.upsert({
|
|
||||||
create: {
|
create: {
|
||||||
settings,
|
settings: userSettings as unknown as Prisma.JsonObject,
|
||||||
User: {
|
User: {
|
||||||
connect: {
|
connect: {
|
||||||
id: userId
|
id: userId
|
||||||
@ -449,14 +450,21 @@ export class UserService {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
settings
|
settings: userSettings as unknown as Prisma.JsonObject
|
||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
userId
|
userId
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
this.eventEmitter.emit(
|
||||||
|
PortfolioChangedEvent.getName(),
|
||||||
|
new PortfolioChangedEvent({
|
||||||
|
userId
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getRandomString(length: number) {
|
private getRandomString(length: number) {
|
||||||
|
11
apps/api/src/events/events.module.ts
Normal file
11
apps/api/src/events/events.module.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
|
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { PortfolioChangedListener } from './portfolio-changed.listener';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [RedisCacheModule],
|
||||||
|
providers: [PortfolioChangedListener]
|
||||||
|
})
|
||||||
|
export class EventsModule {}
|
15
apps/api/src/events/portfolio-changed.event.ts
Normal file
15
apps/api/src/events/portfolio-changed.event.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export class PortfolioChangedEvent {
|
||||||
|
private userId: string;
|
||||||
|
|
||||||
|
public constructor({ userId }: { userId: string }) {
|
||||||
|
this.userId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getName() {
|
||||||
|
return 'portfolio.changed';
|
||||||
|
}
|
||||||
|
|
||||||
|
public getUserId() {
|
||||||
|
return this.userId;
|
||||||
|
}
|
||||||
|
}
|
23
apps/api/src/events/portfolio-changed.listener.ts
Normal file
23
apps/api/src/events/portfolio-changed.listener.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
||||||
|
|
||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
|
|
||||||
|
import { PortfolioChangedEvent } from './portfolio-changed.event';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PortfolioChangedListener {
|
||||||
|
public constructor(private readonly redisCacheService: RedisCacheService) {}
|
||||||
|
|
||||||
|
@OnEvent(PortfolioChangedEvent.getName())
|
||||||
|
handlePortfolioChangedEvent(event: PortfolioChangedEvent) {
|
||||||
|
Logger.log(
|
||||||
|
`Portfolio of user with id ${event.getUserId()} has changed`,
|
||||||
|
'PortfolioChangedListener'
|
||||||
|
);
|
||||||
|
|
||||||
|
this.redisCacheService.remove(
|
||||||
|
this.redisCacheService.getPortfolioSnapshotKey(event.getUserId())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -18,10 +18,8 @@ export function getFactor(activityType: ActivityType) {
|
|||||||
|
|
||||||
switch (activityType) {
|
switch (activityType) {
|
||||||
case 'BUY':
|
case 'BUY':
|
||||||
case 'ITEM':
|
|
||||||
factor = 1;
|
factor = 1;
|
||||||
break;
|
break;
|
||||||
case 'LIABILITY':
|
|
||||||
case 'SELL':
|
case 'SELL':
|
||||||
factor = -1;
|
factor = -1;
|
||||||
break;
|
break;
|
||||||
@ -37,36 +35,48 @@ export function getInterval(
|
|||||||
aDateRange: DateRange,
|
aDateRange: DateRange,
|
||||||
portfolioStart = new Date(0)
|
portfolioStart = new Date(0)
|
||||||
) {
|
) {
|
||||||
let endDate = endOfDay(new Date());
|
let endDate = endOfDay(new Date(Date.now()));
|
||||||
let startDate = portfolioStart;
|
let startDate = portfolioStart;
|
||||||
|
|
||||||
switch (aDateRange) {
|
switch (aDateRange) {
|
||||||
case '1d':
|
case '1d':
|
||||||
startDate = max([startDate, subDays(resetHours(new Date()), 1)]);
|
startDate = max([
|
||||||
|
startDate,
|
||||||
|
subDays(resetHours(new Date(Date.now())), 1)
|
||||||
|
]);
|
||||||
break;
|
break;
|
||||||
case 'mtd':
|
case 'mtd':
|
||||||
startDate = max([
|
startDate = max([
|
||||||
startDate,
|
startDate,
|
||||||
subDays(startOfMonth(resetHours(new Date())), 1)
|
subDays(startOfMonth(resetHours(new Date(Date.now()))), 1)
|
||||||
]);
|
]);
|
||||||
break;
|
break;
|
||||||
case 'wtd':
|
case 'wtd':
|
||||||
startDate = max([
|
startDate = max([
|
||||||
startDate,
|
startDate,
|
||||||
subDays(startOfWeek(resetHours(new Date()), { weekStartsOn: 1 }), 1)
|
subDays(
|
||||||
|
startOfWeek(resetHours(new Date(Date.now())), { weekStartsOn: 1 }),
|
||||||
|
1
|
||||||
|
)
|
||||||
]);
|
]);
|
||||||
break;
|
break;
|
||||||
case 'ytd':
|
case 'ytd':
|
||||||
startDate = max([
|
startDate = max([
|
||||||
startDate,
|
startDate,
|
||||||
subDays(startOfYear(resetHours(new Date())), 1)
|
subDays(startOfYear(resetHours(new Date(Date.now()))), 1)
|
||||||
]);
|
]);
|
||||||
break;
|
break;
|
||||||
case '1y':
|
case '1y':
|
||||||
startDate = max([startDate, subYears(resetHours(new Date()), 1)]);
|
startDate = max([
|
||||||
|
startDate,
|
||||||
|
subYears(resetHours(new Date(Date.now())), 1)
|
||||||
|
]);
|
||||||
break;
|
break;
|
||||||
case '5y':
|
case '5y':
|
||||||
startDate = max([startDate, subYears(resetHours(new Date()), 5)]);
|
startDate = max([
|
||||||
|
startDate,
|
||||||
|
subYears(resetHours(new Date(Date.now())), 5)
|
||||||
|
]);
|
||||||
break;
|
break;
|
||||||
case 'max':
|
case 'max':
|
||||||
break;
|
break;
|
||||||
|
@ -57,6 +57,7 @@ export class RedactValuesInResponseInterceptor<T>
|
|||||||
'quantity',
|
'quantity',
|
||||||
'symbolMapping',
|
'symbolMapping',
|
||||||
'totalBalanceInBaseCurrency',
|
'totalBalanceInBaseCurrency',
|
||||||
|
'totalValueInBaseCurrency',
|
||||||
'unitPrice',
|
'unitPrice',
|
||||||
'value',
|
'value',
|
||||||
'valueInBaseCurrency'
|
'valueInBaseCurrency'
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { groupBy } from '@ghostfolio/common/helper';
|
import { groupBy } from '@ghostfolio/common/helper';
|
||||||
import { TimelinePosition, UserSettings } from '@ghostfolio/common/interfaces';
|
import { UserSettings } from '@ghostfolio/common/interfaces';
|
||||||
|
import { TimelinePosition } from '@ghostfolio/common/models';
|
||||||
|
|
||||||
import { EvaluationResult } from './interfaces/evaluation-result.interface';
|
import { EvaluationResult } from './interfaces/evaluation-result.interface';
|
||||||
import { RuleInterface } from './interfaces/rule.interface';
|
import { RuleInterface } from './interfaces/rule.interface';
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
import { Rule } from '@ghostfolio/api/models/rule';
|
import { Rule } from '@ghostfolio/api/models/rule';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { TimelinePosition, UserSettings } from '@ghostfolio/common/interfaces';
|
import { UserSettings } from '@ghostfolio/common/interfaces';
|
||||||
|
import { TimelinePosition } from '@ghostfolio/common/models';
|
||||||
|
|
||||||
export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule<Settings> {
|
export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule<Settings> {
|
||||||
private positions: TimelinePosition[];
|
private positions: TimelinePosition[];
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
import { Rule } from '@ghostfolio/api/models/rule';
|
import { Rule } from '@ghostfolio/api/models/rule';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { TimelinePosition, UserSettings } from '@ghostfolio/common/interfaces';
|
import { UserSettings } from '@ghostfolio/common/interfaces';
|
||||||
|
import { TimelinePosition } from '@ghostfolio/common/models';
|
||||||
|
|
||||||
export class CurrencyClusterRiskCurrentInvestment extends Rule<Settings> {
|
export class CurrencyClusterRiskCurrentInvestment extends Rule<Settings> {
|
||||||
private positions: TimelinePosition[];
|
private positions: TimelinePosition[];
|
||||||
|
@ -37,7 +37,17 @@ export class DataGatheringProcessor {
|
|||||||
@Process({ concurrency: 1, name: GATHER_ASSET_PROFILE_PROCESS })
|
@Process({ concurrency: 1, name: GATHER_ASSET_PROFILE_PROCESS })
|
||||||
public async gatherAssetProfile(job: Job<UniqueAsset>) {
|
public async gatherAssetProfile(job: Job<UniqueAsset>) {
|
||||||
try {
|
try {
|
||||||
|
Logger.log(
|
||||||
|
`Asset profile data gathering has been started for ${job.data.symbol} (${job.data.dataSource})`,
|
||||||
|
`DataGatheringProcessor (${GATHER_ASSET_PROFILE_PROCESS})`
|
||||||
|
);
|
||||||
|
|
||||||
await this.dataGatheringService.gatherAssetProfiles([job.data]);
|
await this.dataGatheringService.gatherAssetProfiles([job.data]);
|
||||||
|
|
||||||
|
Logger.log(
|
||||||
|
`Asset profile data gathering has been completed for ${job.data.symbol} (${job.data.dataSource})`,
|
||||||
|
`DataGatheringProcessor (${GATHER_ASSET_PROFILE_PROCESS})`
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(
|
Logger.error(
|
||||||
error,
|
error,
|
||||||
@ -62,11 +72,11 @@ export class DataGatheringProcessor {
|
|||||||
`DataGatheringProcessor (${GATHER_HISTORICAL_MARKET_DATA_PROCESS})`
|
`DataGatheringProcessor (${GATHER_HISTORICAL_MARKET_DATA_PROCESS})`
|
||||||
);
|
);
|
||||||
|
|
||||||
const historicalData = await this.dataProviderService.getHistoricalRaw(
|
const historicalData = await this.dataProviderService.getHistoricalRaw({
|
||||||
[{ dataSource, symbol }],
|
dataGatheringItems: [{ dataSource, symbol }],
|
||||||
currentDate,
|
from: currentDate,
|
||||||
new Date()
|
to: new Date()
|
||||||
);
|
});
|
||||||
|
|
||||||
const data: Prisma.MarketDataUpdateInput[] = [];
|
const data: Prisma.MarketDataUpdateInput[] = [];
|
||||||
let lastMarketPrice: number;
|
let lastMarketPrice: number;
|
||||||
|
@ -104,11 +104,11 @@ export class DataGatheringService {
|
|||||||
symbol: string;
|
symbol: string;
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
const historicalData = await this.dataProviderService.getHistoricalRaw(
|
const historicalData = await this.dataProviderService.getHistoricalRaw({
|
||||||
[{ dataSource, symbol }],
|
dataGatheringItems: [{ dataSource, symbol }],
|
||||||
date,
|
from: date,
|
||||||
date
|
to: date
|
||||||
);
|
});
|
||||||
|
|
||||||
const marketPrice =
|
const marketPrice =
|
||||||
historicalData[symbol][format(date, DATE_FORMAT)].marketPrice;
|
historicalData[symbol][format(date, DATE_FORMAT)].marketPrice;
|
||||||
@ -230,17 +230,12 @@ export class DataGatheringService {
|
|||||||
error,
|
error,
|
||||||
'DataGatheringService'
|
'DataGatheringService'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (uniqueAssets.length === 1) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.log(
|
|
||||||
`Asset profile data gathering has been completed for ${uniqueAssets
|
|
||||||
.map(({ dataSource, symbol }) => {
|
|
||||||
return `${symbol} (${dataSource})`;
|
|
||||||
})
|
|
||||||
.join(',')}.`,
|
|
||||||
'DataGatheringService'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async gatherSymbols({
|
public async gatherSymbols({
|
||||||
|
@ -59,7 +59,7 @@ export class CoinGeckoService implements DataProviderInterface {
|
|||||||
}): Promise<Partial<SymbolProfile>> {
|
}): Promise<Partial<SymbolProfile>> {
|
||||||
const response: Partial<SymbolProfile> = {
|
const response: Partial<SymbolProfile> = {
|
||||||
symbol,
|
symbol,
|
||||||
assetClass: AssetClass.CASH,
|
assetClass: AssetClass.LIQUIDITY,
|
||||||
assetSubClass: AssetSubClass.CRYPTOCURRENCY,
|
assetSubClass: AssetSubClass.CRYPTOCURRENCY,
|
||||||
currency: DEFAULT_CURRENCY,
|
currency: DEFAULT_CURRENCY,
|
||||||
dataSource: this.getName()
|
dataSource: this.getName()
|
||||||
@ -243,7 +243,7 @@ export class CoinGeckoService implements DataProviderInterface {
|
|||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
symbol,
|
symbol,
|
||||||
assetClass: AssetClass.CASH,
|
assetClass: AssetClass.LIQUIDITY,
|
||||||
assetSubClass: AssetSubClass.CRYPTOCURRENCY,
|
assetSubClass: AssetSubClass.CRYPTOCURRENCY,
|
||||||
currency: DEFAULT_CURRENCY,
|
currency: DEFAULT_CURRENCY,
|
||||||
dataProviderInfo: this.getDataProviderInfo(),
|
dataProviderInfo: this.getDataProviderInfo(),
|
||||||
|
@ -266,7 +266,7 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
|
|||||||
|
|
||||||
switch (quoteType?.toLowerCase()) {
|
switch (quoteType?.toLowerCase()) {
|
||||||
case 'cryptocurrency':
|
case 'cryptocurrency':
|
||||||
assetClass = AssetClass.CASH;
|
assetClass = AssetClass.LIQUIDITY;
|
||||||
assetSubClass = AssetSubClass.CRYPTOCURRENCY;
|
assetSubClass = AssetSubClass.CRYPTOCURRENCY;
|
||||||
break;
|
break;
|
||||||
case 'equity':
|
case 'equity':
|
||||||
|
@ -233,15 +233,17 @@ export class DataProviderService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getHistoricalRaw(
|
public async getHistoricalRaw({
|
||||||
aDataGatheringItems: UniqueAsset[],
|
dataGatheringItems,
|
||||||
from: Date,
|
from,
|
||||||
to: Date
|
to
|
||||||
): Promise<{
|
}: {
|
||||||
|
dataGatheringItems: UniqueAsset[];
|
||||||
|
from: Date;
|
||||||
|
to: Date;
|
||||||
|
}): Promise<{
|
||||||
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
||||||
}> {
|
}> {
|
||||||
let dataGatheringItems = aDataGatheringItems;
|
|
||||||
|
|
||||||
for (const { currency, rootCurrency } of DERIVED_CURRENCIES) {
|
for (const { currency, rootCurrency } of DERIVED_CURRENCIES) {
|
||||||
if (
|
if (
|
||||||
this.hasCurrency({
|
this.hasCurrency({
|
||||||
@ -330,6 +332,8 @@ export class DataProviderService {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(error, 'DataProviderService');
|
Logger.error(error, 'DataProviderService');
|
||||||
|
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@ -395,7 +399,8 @@ export class DataProviderService {
|
|||||||
numberOfItemsInCache > 1 ? 's' : ''
|
numberOfItemsInCache > 1 ? 's' : ''
|
||||||
} from cache in ${((performance.now() - startTimeTotal) / 1000).toFixed(
|
} from cache in ${((performance.now() - startTimeTotal) / 1000).toFixed(
|
||||||
3
|
3
|
||||||
)} seconds`
|
)} seconds`,
|
||||||
|
'DataProviderService'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -501,7 +506,8 @@ export class DataProviderService {
|
|||||||
} from ${dataSource} in ${(
|
} from ${dataSource} in ${(
|
||||||
(performance.now() - startTimeDataSource) /
|
(performance.now() - startTimeDataSource) /
|
||||||
1000
|
1000
|
||||||
).toFixed(3)} seconds`
|
).toFixed(3)} seconds`,
|
||||||
|
'DataProviderService'
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -531,14 +537,15 @@ export class DataProviderService {
|
|||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
|
||||||
Logger.debug('------------------------------------------------');
|
Logger.debug('--------------------------------------------------------');
|
||||||
Logger.debug(
|
Logger.debug(
|
||||||
`Fetched ${items.length} quote${items.length > 1 ? 's' : ''} in ${(
|
`Fetched ${items.length} quote${items.length > 1 ? 's' : ''} in ${(
|
||||||
(performance.now() - startTimeTotal) /
|
(performance.now() - startTimeTotal) /
|
||||||
1000
|
1000
|
||||||
).toFixed(3)} seconds`
|
).toFixed(3)} seconds`,
|
||||||
|
'DataProviderService'
|
||||||
);
|
);
|
||||||
Logger.debug('================================================');
|
Logger.debug('========================================================');
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
@ -468,7 +468,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
|||||||
assetSubClass = AssetSubClass.STOCK;
|
assetSubClass = AssetSubClass.STOCK;
|
||||||
break;
|
break;
|
||||||
case 'currency':
|
case 'currency':
|
||||||
assetClass = AssetClass.CASH;
|
assetClass = AssetClass.LIQUIDITY;
|
||||||
|
|
||||||
if (Exchange?.toLowerCase() === 'cc') {
|
if (Exchange?.toLowerCase() === 'cc') {
|
||||||
assetSubClass = AssetSubClass.CRYPTOCURRENCY;
|
assetSubClass = AssetSubClass.CRYPTOCURRENCY;
|
||||||
|
@ -91,7 +91,7 @@ export class ManualService implements DataProviderInterface {
|
|||||||
headers = {},
|
headers = {},
|
||||||
selector,
|
selector,
|
||||||
url
|
url
|
||||||
} = symbolProfile.scraperConfiguration ?? {};
|
} = symbolProfile?.scraperConfiguration ?? {};
|
||||||
|
|
||||||
if (defaultMarketPrice) {
|
if (defaultMarketPrice) {
|
||||||
const historical: {
|
const historical: {
|
||||||
|
@ -449,13 +449,16 @@ export class ExchangeRateDataService {
|
|||||||
factors[format(date, DATE_FORMAT)] = factor;
|
factors[format(date, DATE_FORMAT)] = factor;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
Logger.error(
|
let errorMessage = `No exchange rate has been found for ${currencyFrom}${currencyTo} at ${format(
|
||||||
`No exchange rate has been found for ${currencyFrom}${currencyTo} at ${format(
|
date,
|
||||||
date,
|
DATE_FORMAT
|
||||||
DATE_FORMAT
|
)}. Please complement market data for ${DEFAULT_CURRENCY}${currencyFrom}`;
|
||||||
)}. Please complement market data for ${DEFAULT_CURRENCY}${currencyFrom} and ${DEFAULT_CURRENCY}${currencyTo}.`,
|
|
||||||
'ExchangeRateDataService'
|
if (DEFAULT_CURRENCY !== currencyTo) {
|
||||||
);
|
errorMessage = `${errorMessage} and ${DEFAULT_CURRENCY}${currencyTo}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.error(`${errorMessage}.`, 'ExchangeRateDataService');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { GfLogoModule } from '@ghostfolio/ui/logo';
|
import { GfLogoComponent } from '@ghostfolio/ui/logo';
|
||||||
|
|
||||||
import { Platform } from '@angular/cdk/platform';
|
import { Platform } from '@angular/cdk/platform';
|
||||||
import { HttpClientModule } from '@angular/common/http';
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
@ -43,7 +43,7 @@ export function NgxStripeFactory(): string {
|
|||||||
BrowserAnimationsModule,
|
BrowserAnimationsModule,
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
GfHeaderModule,
|
GfHeaderModule,
|
||||||
GfLogoModule,
|
GfLogoComponent,
|
||||||
GfSubscriptionInterstitialDialogModule,
|
GfSubscriptionInterstitialDialogModule,
|
||||||
HttpClientModule,
|
HttpClientModule,
|
||||||
MarkdownModule.forRoot(),
|
MarkdownModule.forRoot(),
|
||||||
|
@ -84,55 +84,10 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ngOnInit() {
|
public ngOnInit() {
|
||||||
this.dataService
|
this.fetchAccount();
|
||||||
.fetchAccount(this.data.accountId)
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe(
|
|
||||||
({
|
|
||||||
balance,
|
|
||||||
currency,
|
|
||||||
name,
|
|
||||||
Platform,
|
|
||||||
transactionCount,
|
|
||||||
value,
|
|
||||||
valueInBaseCurrency
|
|
||||||
}) => {
|
|
||||||
this.balance = balance;
|
|
||||||
this.currency = currency;
|
|
||||||
|
|
||||||
if (isNumber(balance) && isNumber(value)) {
|
|
||||||
this.equity = new Big(value).minus(balance).toNumber();
|
|
||||||
} else {
|
|
||||||
this.equity = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.name = name;
|
|
||||||
this.platformName = Platform?.name ?? '-';
|
|
||||||
this.transactionCount = transactionCount;
|
|
||||||
this.valueInBaseCurrency = valueInBaseCurrency;
|
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
this.dataService
|
|
||||||
.fetchPortfolioHoldings({
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
type: 'ACCOUNT',
|
|
||||||
id: this.data.accountId
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe(({ holdings }) => {
|
|
||||||
this.holdings = holdings;
|
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.fetchAccountBalances();
|
this.fetchAccountBalances();
|
||||||
this.fetchActivities();
|
this.fetchActivities();
|
||||||
|
this.fetchPortfolioHoldings();
|
||||||
this.fetchPortfolioPerformance();
|
this.fetchPortfolioPerformance();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,15 +95,35 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
|||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onAddAccountBalance({
|
||||||
|
balance,
|
||||||
|
date
|
||||||
|
}: {
|
||||||
|
balance: number;
|
||||||
|
date: Date;
|
||||||
|
}) {
|
||||||
|
this.dataService
|
||||||
|
.postAccountBalance({
|
||||||
|
balance,
|
||||||
|
date,
|
||||||
|
accountId: this.data.accountId
|
||||||
|
})
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.fetchAccount();
|
||||||
|
this.fetchAccountBalances();
|
||||||
|
this.fetchPortfolioPerformance();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public onDeleteAccountBalance(aId: string) {
|
public onDeleteAccountBalance(aId: string) {
|
||||||
this.dataService
|
this.dataService
|
||||||
.deleteAccountBalance(aId)
|
.deleteAccountBalance(aId)
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe({
|
.subscribe(() => {
|
||||||
next: () => {
|
this.fetchAccount();
|
||||||
this.fetchAccountBalances();
|
this.fetchAccountBalances();
|
||||||
this.fetchPortfolioPerformance();
|
this.fetchPortfolioPerformance();
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,6 +156,39 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
|||||||
this.fetchActivities();
|
this.fetchActivities();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fetchAccount() {
|
||||||
|
this.dataService
|
||||||
|
.fetchAccount(this.data.accountId)
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(
|
||||||
|
({
|
||||||
|
balance,
|
||||||
|
currency,
|
||||||
|
name,
|
||||||
|
Platform,
|
||||||
|
transactionCount,
|
||||||
|
value,
|
||||||
|
valueInBaseCurrency
|
||||||
|
}) => {
|
||||||
|
this.balance = balance;
|
||||||
|
this.currency = currency;
|
||||||
|
|
||||||
|
if (isNumber(balance) && isNumber(value)) {
|
||||||
|
this.equity = new Big(value).minus(balance).toNumber();
|
||||||
|
} else {
|
||||||
|
this.equity = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.name = name;
|
||||||
|
this.platformName = Platform?.name ?? '-';
|
||||||
|
this.transactionCount = transactionCount;
|
||||||
|
this.valueInBaseCurrency = valueInBaseCurrency;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private fetchAccountBalances() {
|
private fetchAccountBalances() {
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchAccountBalances(this.data.accountId)
|
.fetchAccountBalances(this.data.accountId)
|
||||||
@ -212,6 +220,24 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fetchPortfolioHoldings() {
|
||||||
|
this.dataService
|
||||||
|
.fetchPortfolioHoldings({
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
type: 'ACCOUNT',
|
||||||
|
id: this.data.accountId
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(({ holdings }) => {
|
||||||
|
this.holdings = holdings;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private fetchPortfolioPerformance() {
|
private fetchPortfolioPerformance() {
|
||||||
this.isLoadingChart = true;
|
this.isLoadingChart = true;
|
||||||
|
|
||||||
@ -233,11 +259,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
|||||||
({ date, netWorth, netWorthInPercentage }) => {
|
({ date, netWorth, netWorthInPercentage }) => {
|
||||||
return {
|
return {
|
||||||
date,
|
date,
|
||||||
value:
|
value: isNumber(netWorth) ? netWorth : netWorthInPercentage
|
||||||
this.data.hasImpersonationId ||
|
|
||||||
this.user.settings.isRestrictedView
|
|
||||||
? netWorthInPercentage
|
|
||||||
: netWorth
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -115,6 +115,7 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
<gf-account-balances
|
<gf-account-balances
|
||||||
[accountBalances]="accountBalances"
|
[accountBalances]="accountBalances"
|
||||||
|
[accountCurrency]="currency"
|
||||||
[accountId]="data.accountId"
|
[accountId]="data.accountId"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[showActions]="
|
[showActions]="
|
||||||
@ -122,6 +123,7 @@
|
|||||||
hasPermissionToDeleteAccountBalance &&
|
hasPermissionToDeleteAccountBalance &&
|
||||||
!user.settings.isRestrictedView
|
!user.settings.isRestrictedView
|
||||||
"
|
"
|
||||||
|
(accountBalanceCreated)="onAddAccountBalance($event)"
|
||||||
(accountBalanceDeleted)="onDeleteAccountBalance($event)"
|
(accountBalanceDeleted)="onDeleteAccountBalance($event)"
|
||||||
/>
|
/>
|
||||||
</mat-tab>
|
</mat-tab>
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
|
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
|
||||||
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
|
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
|
||||||
import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module';
|
import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module';
|
||||||
import { GfAccountBalancesModule } from '@ghostfolio/ui/account-balances/account-balances.module';
|
import { GfAccountBalancesComponent } from '@ghostfolio/ui/account-balances';
|
||||||
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
|
import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table';
|
||||||
import { GfHoldingsTableModule } from '@ghostfolio/ui/holdings-table/holdings-table.module';
|
import { GfHoldingsTableComponent } from '@ghostfolio/ui/holdings-table';
|
||||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
@ -19,13 +19,13 @@ import { AccountDetailDialog } from './account-detail-dialog.component';
|
|||||||
declarations: [AccountDetailDialog],
|
declarations: [AccountDetailDialog],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
GfAccountBalancesModule,
|
GfAccountBalancesComponent,
|
||||||
GfActivitiesTableModule,
|
GfActivitiesTableComponent,
|
||||||
GfDialogFooterModule,
|
GfDialogFooterModule,
|
||||||
GfDialogHeaderModule,
|
GfDialogHeaderModule,
|
||||||
GfHoldingsTableModule,
|
GfHoldingsTableComponent,
|
||||||
GfInvestmentChartModule,
|
GfInvestmentChartModule,
|
||||||
GfValueModule,
|
GfValueComponent,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatDialogModule,
|
MatDialogModule,
|
||||||
MatTabsModule,
|
MatTabsModule,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { GfAssetProfileIconComponent } from '@ghostfolio/client/components/asset-profile-icon/asset-profile-icon.component';
|
import { GfAssetProfileIconComponent } from '@ghostfolio/client/components/asset-profile-icon/asset-profile-icon.component';
|
||||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
@ -18,7 +18,7 @@ import { AccountsTableComponent } from './accounts-table.component';
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
GfAssetProfileIconComponent,
|
GfAssetProfileIconComponent,
|
||||||
GfValueModule,
|
GfValueComponent,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatMenuModule,
|
MatMenuModule,
|
||||||
MatSortModule,
|
MatSortModule,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
|
import { GfLineChartComponent } from '@ghostfolio/ui/line-chart';
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
@ -9,7 +9,7 @@ import { GfMarketDataDetailDialogModule } from './market-data-detail-dialog/mark
|
|||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [AdminMarketDataDetailComponent],
|
declarations: [AdminMarketDataDetailComponent],
|
||||||
exports: [AdminMarketDataDetailComponent],
|
exports: [AdminMarketDataDetailComponent],
|
||||||
imports: [CommonModule, GfLineChartModule, GfMarketDataDetailDialogModule],
|
imports: [CommonModule, GfLineChartComponent, GfMarketDataDetailDialogModule],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
export class GfAdminMarketDataDetailModule {}
|
export class GfAdminMarketDataDetailModule {}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||||
import { GfActivitiesFilterModule } from '@ghostfolio/ui/activities-filter/activities-filter.module';
|
import { GfActivitiesFilterComponent } from '@ghostfolio/ui/activities-filter';
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
@ -20,7 +20,7 @@ import { GfCreateAssetProfileDialogModule } from './create-asset-profile-dialog/
|
|||||||
declarations: [AdminMarketDataComponent],
|
declarations: [AdminMarketDataComponent],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
GfActivitiesFilterModule,
|
GfActivitiesFilterComponent,
|
||||||
GfAssetProfileDialogModule,
|
GfAssetProfileDialogModule,
|
||||||
GfCreateAssetProfileDialogModule,
|
GfCreateAssetProfileDialogModule,
|
||||||
GfSymbolModule,
|
GfSymbolModule,
|
||||||
|
@ -265,22 +265,22 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
|||||||
let symbolMapping = {};
|
let symbolMapping = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
countries = JSON.parse(this.assetProfileForm.controls['countries'].value);
|
countries = JSON.parse(this.assetProfileForm.get('countries').value);
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
scraperConfiguration = JSON.parse(
|
scraperConfiguration = JSON.parse(
|
||||||
this.assetProfileForm.controls['scraperConfiguration'].value
|
this.assetProfileForm.get('scraperConfiguration').value
|
||||||
);
|
);
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
sectors = JSON.parse(this.assetProfileForm.controls['sectors'].value);
|
sectors = JSON.parse(this.assetProfileForm.get('sectors').value);
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
symbolMapping = JSON.parse(
|
symbolMapping = JSON.parse(
|
||||||
this.assetProfileForm.controls['symbolMapping'].value
|
this.assetProfileForm.get('symbolMapping').value
|
||||||
);
|
);
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
@ -289,14 +289,14 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
|||||||
scraperConfiguration,
|
scraperConfiguration,
|
||||||
sectors,
|
sectors,
|
||||||
symbolMapping,
|
symbolMapping,
|
||||||
assetClass: this.assetProfileForm.controls['assetClass'].value,
|
assetClass: this.assetProfileForm.get('assetClass').value,
|
||||||
assetSubClass: this.assetProfileForm.controls['assetSubClass'].value,
|
assetSubClass: this.assetProfileForm.get('assetSubClass').value,
|
||||||
comment: this.assetProfileForm.controls['comment'].value ?? null,
|
comment: this.assetProfileForm.get('comment').value || null,
|
||||||
currency: (<Currency>(
|
currency: (<Currency>(
|
||||||
(<unknown>this.assetProfileForm.controls['currency'].value)
|
(<unknown>this.assetProfileForm.get('currency').value)
|
||||||
))?.value,
|
))?.value,
|
||||||
name: this.assetProfileForm.controls['name'].value,
|
name: this.assetProfileForm.get('name').value,
|
||||||
url: this.assetProfileForm.controls['url'].value
|
url: this.assetProfileForm.get('url').value || null
|
||||||
};
|
};
|
||||||
|
|
||||||
this.adminService
|
this.adminService
|
||||||
@ -314,8 +314,8 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
|||||||
this.adminService
|
this.adminService
|
||||||
.testMarketData({
|
.testMarketData({
|
||||||
dataSource: this.data.dataSource,
|
dataSource: this.data.dataSource,
|
||||||
scraperConfiguration:
|
scraperConfiguration: this.assetProfileForm.get('scraperConfiguration')
|
||||||
this.assetProfileForm.controls['scraperConfiguration'].value,
|
.value,
|
||||||
symbol: this.data.symbol
|
symbol: this.data.symbol
|
||||||
})
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
@ -331,9 +331,8 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
|||||||
' ' +
|
' ' +
|
||||||
price +
|
price +
|
||||||
' ' +
|
' ' +
|
||||||
(<Currency>(
|
(<Currency>(<unknown>this.assetProfileForm.get('currency').value))
|
||||||
(<unknown>this.assetProfileForm.controls['currency'].value)
|
?.value
|
||||||
))?.value
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -302,7 +302,7 @@
|
|||||||
mat-flat-button
|
mat-flat-button
|
||||||
type="button"
|
type="button"
|
||||||
[disabled]="
|
[disabled]="
|
||||||
assetProfileForm.controls['scraperConfiguration'].value === '{}'
|
assetProfileForm.get('scraperConfiguration').value === '{}'
|
||||||
"
|
"
|
||||||
(click)="onTestMarketData()"
|
(click)="onTestMarketData()"
|
||||||
>
|
>
|
||||||
@ -338,11 +338,11 @@
|
|||||||
<mat-form-field appearance="outline" class="w-100 without-hint">
|
<mat-form-field appearance="outline" class="w-100 without-hint">
|
||||||
<mat-label i18n>Url</mat-label>
|
<mat-label i18n>Url</mat-label>
|
||||||
<input formControlName="url" matInput type="text" />
|
<input formControlName="url" matInput type="text" />
|
||||||
@if (assetProfileForm.controls['url'].value) {
|
@if (assetProfileForm.get('url').value) {
|
||||||
<gf-asset-profile-icon
|
<gf-asset-profile-icon
|
||||||
class="mr-3"
|
class="mr-3"
|
||||||
matSuffix
|
matSuffix
|
||||||
[url]="assetProfileForm.controls['url'].value"
|
[url]="assetProfileForm.get('url').value"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { GfAdminMarketDataDetailModule } from '@ghostfolio/client/components/admin-market-data-detail/admin-market-data-detail.module';
|
import { GfAdminMarketDataDetailModule } from '@ghostfolio/client/components/admin-market-data-detail/admin-market-data-detail.module';
|
||||||
import { AdminMarketDataService } from '@ghostfolio/client/components/admin-market-data/admin-market-data.service';
|
import { AdminMarketDataService } from '@ghostfolio/client/components/admin-market-data/admin-market-data.service';
|
||||||
import { GfAssetProfileIconComponent } from '@ghostfolio/client/components/asset-profile-icon/asset-profile-icon.component';
|
import { GfAssetProfileIconComponent } from '@ghostfolio/client/components/asset-profile-icon/asset-profile-icon.component';
|
||||||
import { GfCurrencySelectorModule } from '@ghostfolio/ui/currency-selector/currency-selector.module';
|
import { GfCurrencySelectorComponent } from '@ghostfolio/ui/currency-selector';
|
||||||
import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module';
|
import { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart';
|
||||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||||
|
|
||||||
import { TextFieldModule } from '@angular/cdk/text-field';
|
import { TextFieldModule } from '@angular/cdk/text-field';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
@ -26,9 +26,9 @@ import { AssetProfileDialog } from './asset-profile-dialog.component';
|
|||||||
FormsModule,
|
FormsModule,
|
||||||
GfAdminMarketDataDetailModule,
|
GfAdminMarketDataDetailModule,
|
||||||
GfAssetProfileIconComponent,
|
GfAssetProfileIconComponent,
|
||||||
GfCurrencySelectorModule,
|
GfCurrencySelectorComponent,
|
||||||
GfPortfolioProportionChartModule,
|
GfPortfolioProportionChartComponent,
|
||||||
GfValueModule,
|
GfValueComponent,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatCheckboxModule,
|
MatCheckboxModule,
|
||||||
MatDialogModule,
|
MatDialogModule,
|
||||||
|
@ -59,14 +59,12 @@ export class CreateAssetProfileDialog implements OnInit, OnDestroy {
|
|||||||
this.mode === 'auto'
|
this.mode === 'auto'
|
||||||
? this.dialogRef.close({
|
? this.dialogRef.close({
|
||||||
dataSource:
|
dataSource:
|
||||||
this.createAssetProfileForm.controls['searchSymbol'].value
|
this.createAssetProfileForm.get('searchSymbol').value.dataSource,
|
||||||
.dataSource,
|
symbol: this.createAssetProfileForm.get('searchSymbol').value.symbol
|
||||||
symbol:
|
|
||||||
this.createAssetProfileForm.controls['searchSymbol'].value.symbol
|
|
||||||
})
|
})
|
||||||
: this.dialogRef.close({
|
: this.dialogRef.close({
|
||||||
dataSource: 'MANUAL',
|
dataSource: 'MANUAL',
|
||||||
symbol: this.createAssetProfileForm.controls['addSymbol'].value
|
symbol: this.createAssetProfileForm.get('addSymbol').value
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { GfSymbolAutocompleteModule } from '@ghostfolio/ui/symbol-autocomplete';
|
import { GfSymbolAutocompleteComponent } from '@ghostfolio/ui/symbol-autocomplete';
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
@ -16,7 +16,7 @@ import { CreateAssetProfileDialog } from './create-asset-profile-dialog.componen
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
GfSymbolAutocompleteModule,
|
GfSymbolAutocompleteComponent,
|
||||||
MatDialogModule,
|
MatDialogModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { CacheService } from '@ghostfolio/client/services/cache.service';
|
import { CacheService } from '@ghostfolio/client/services/cache.service';
|
||||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
@ -19,7 +19,7 @@ import { AdminOverviewComponent } from './admin-overview.component';
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
GfValueModule,
|
GfValueComponent,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
MatMenuModule,
|
MatMenuModule,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
|
||||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
@ -14,8 +14,8 @@ import { AdminUsersComponent } from './admin-users.component';
|
|||||||
exports: [],
|
exports: [],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
GfPremiumIndicatorModule,
|
GfPremiumIndicatorComponent,
|
||||||
GfValueModule,
|
GfValueComponent,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatMenuModule,
|
MatMenuModule,
|
||||||
MatTableModule
|
MatTableModule
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
@ -15,7 +15,7 @@ import { BenchmarkComparatorComponent } from './benchmark-comparator.component';
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
GfPremiumIndicatorModule,
|
GfPremiumIndicatorComponent,
|
||||||
MatSelectModule,
|
MatSelectModule,
|
||||||
NgxSkeletonLoaderModule,
|
NgxSkeletonLoaderModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
|
@ -12,7 +12,7 @@ import { UserService } from '@ghostfolio/client/services/user/user.service';
|
|||||||
import { Filter, InfoItem, User } from '@ghostfolio/common/interfaces';
|
import { Filter, InfoItem, User } from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { DateRange } from '@ghostfolio/common/types';
|
import { DateRange } from '@ghostfolio/common/types';
|
||||||
import { AssistantComponent } from '@ghostfolio/ui/assistant/assistant.component';
|
import { GfAssistantComponent } from '@ghostfolio/ui/assistant/assistant.component';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
@ -62,7 +62,7 @@ export class HeaderComponent implements OnChanges {
|
|||||||
|
|
||||||
@Output() signOut = new EventEmitter<void>();
|
@Output() signOut = new EventEmitter<void>();
|
||||||
|
|
||||||
@ViewChild('assistant') assistantElement: AssistantComponent;
|
@ViewChild('assistant') assistantElement: GfAssistantComponent;
|
||||||
@ViewChild('assistantTrigger') assistentMenuTriggerElement: MatMenuTrigger;
|
@ViewChild('assistantTrigger') assistentMenuTriggerElement: MatMenuTrigger;
|
||||||
|
|
||||||
public hasPermissionForSocialLogin: boolean;
|
public hasPermissionForSocialLogin: boolean;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { LoginWithAccessTokenDialogModule } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.module';
|
import { LoginWithAccessTokenDialogModule } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.module';
|
||||||
import { GfAssistantModule } from '@ghostfolio/ui/assistant';
|
import { GfAssistantComponent } from '@ghostfolio/ui/assistant';
|
||||||
import { GfLogoModule } from '@ghostfolio/ui/logo';
|
import { GfLogoComponent } from '@ghostfolio/ui/logo';
|
||||||
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
@ -17,9 +17,9 @@ import { HeaderComponent } from './header.component';
|
|||||||
exports: [HeaderComponent],
|
exports: [HeaderComponent],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
GfAssistantModule,
|
GfAssistantComponent,
|
||||||
GfLogoModule,
|
GfLogoComponent,
|
||||||
GfPremiumIndicatorModule,
|
GfPremiumIndicatorComponent,
|
||||||
LoginWithAccessTokenDialogModule,
|
LoginWithAccessTokenDialogModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatMenuModule,
|
MatMenuModule,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { GfFearAndGreedIndexModule } from '@ghostfolio/client/components/fear-and-greed-index/fear-and-greed-index.module';
|
import { GfFearAndGreedIndexModule } from '@ghostfolio/client/components/fear-and-greed-index/fear-and-greed-index.module';
|
||||||
import { GfBenchmarkModule } from '@ghostfolio/ui/benchmark/benchmark.module';
|
import { GfBenchmarkComponent } from '@ghostfolio/ui/benchmark';
|
||||||
import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
|
import { GfLineChartComponent } from '@ghostfolio/ui/line-chart';
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
@ -13,9 +13,9 @@ import { HomeMarketComponent } from './home-market.component';
|
|||||||
exports: [HomeMarketComponent],
|
exports: [HomeMarketComponent],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
GfBenchmarkModule,
|
GfBenchmarkComponent,
|
||||||
GfFearAndGreedIndexModule,
|
GfFearAndGreedIndexModule,
|
||||||
GfLineChartModule,
|
GfLineChartComponent,
|
||||||
NgxSkeletonLoaderModule
|
NgxSkeletonLoaderModule
|
||||||
],
|
],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { GfPortfolioPerformanceModule } from '@ghostfolio/client/components/portfolio-performance/portfolio-performance.module';
|
import { GfPortfolioPerformanceModule } from '@ghostfolio/client/components/portfolio-performance/portfolio-performance.module';
|
||||||
import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
|
import { GfLineChartComponent } from '@ghostfolio/ui/line-chart';
|
||||||
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
|
import { GfNoTransactionsInfoComponent } from '@ghostfolio/ui/no-transactions-info';
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
@ -13,8 +13,8 @@ import { HomeOverviewComponent } from './home-overview.component';
|
|||||||
declarations: [HomeOverviewComponent],
|
declarations: [HomeOverviewComponent],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
GfLineChartModule,
|
GfLineChartComponent,
|
||||||
GfNoTransactionsInfoModule,
|
GfNoTransactionsInfoComponent,
|
||||||
GfPortfolioPerformanceModule,
|
GfPortfolioPerformanceModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
RouterModule
|
RouterModule
|
||||||
|
@ -102,7 +102,7 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
|
|||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchPortfolioDetails({ withLiabilities: true })
|
.fetchPortfolioDetails()
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(({ summary }) => {
|
.subscribe(({ summary }) => {
|
||||||
this.summary = summary;
|
this.summary = summary;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
@ -9,7 +9,7 @@ import { PortfolioPerformanceComponent } from './portfolio-performance.component
|
|||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [PortfolioPerformanceComponent],
|
declarations: [PortfolioPerformanceComponent],
|
||||||
exports: [PortfolioPerformanceComponent],
|
exports: [PortfolioPerformanceComponent],
|
||||||
imports: [CommonModule, GfValueModule, NgxSkeletonLoaderModule],
|
imports: [CommonModule, GfValueComponent, NgxSkeletonLoaderModule],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
export class GfPortfolioPerformanceModule {}
|
export class GfPortfolioPerformanceModule {}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
@ -8,7 +8,7 @@ import { PortfolioSummaryComponent } from './portfolio-summary.component';
|
|||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [PortfolioSummaryComponent],
|
declarations: [PortfolioSummaryComponent],
|
||||||
exports: [PortfolioSummaryComponent],
|
exports: [PortfolioSummaryComponent],
|
||||||
imports: [CommonModule, GfValueModule],
|
imports: [CommonModule, GfValueComponent],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
export class GfPortfolioSummaryModule {}
|
export class GfPortfolioSummaryModule {}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { GfAccountsTableModule } from '@ghostfolio/client/components/accounts-table/accounts-table.module';
|
import { GfAccountsTableModule } from '@ghostfolio/client/components/accounts-table/accounts-table.module';
|
||||||
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
|
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
|
||||||
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
|
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
|
||||||
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
|
import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table';
|
||||||
import { GfDataProviderCreditsModule } from '@ghostfolio/ui/data-provider-credits/data-provider-credits.module';
|
import { GfDataProviderCreditsComponent } from '@ghostfolio/ui/data-provider-credits';
|
||||||
import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
|
import { GfLineChartComponent } from '@ghostfolio/ui/line-chart';
|
||||||
import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module';
|
import { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart';
|
||||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
@ -22,13 +22,13 @@ import { PositionDetailDialog } from './position-detail-dialog.component';
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
GfAccountsTableModule,
|
GfAccountsTableModule,
|
||||||
GfActivitiesTableModule,
|
GfActivitiesTableComponent,
|
||||||
GfDataProviderCreditsModule,
|
GfDataProviderCreditsComponent,
|
||||||
GfDialogFooterModule,
|
GfDialogFooterModule,
|
||||||
GfDialogHeaderModule,
|
GfDialogHeaderModule,
|
||||||
GfLineChartModule,
|
GfLineChartComponent,
|
||||||
GfPortfolioProportionChartModule,
|
GfPortfolioProportionChartComponent,
|
||||||
GfValueModule,
|
GfValueComponent,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatChipsModule,
|
MatChipsModule,
|
||||||
MatDialogModule,
|
MatDialogModule,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||||
import { GfTrendIndicatorModule } from '@ghostfolio/ui/trend-indicator';
|
import { GfTrendIndicatorComponent } from '@ghostfolio/ui/trend-indicator';
|
||||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
@ -18,8 +18,8 @@ import { PositionComponent } from './position.component';
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
GfPositionDetailDialogModule,
|
GfPositionDetailDialogModule,
|
||||||
GfSymbolModule,
|
GfSymbolModule,
|
||||||
GfTrendIndicatorModule,
|
GfTrendIndicatorComponent,
|
||||||
GfValueModule,
|
GfValueComponent,
|
||||||
MatDialogModule,
|
MatDialogModule,
|
||||||
NgxSkeletonLoaderModule,
|
NgxSkeletonLoaderModule,
|
||||||
RouterModule
|
RouterModule
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
|
import { GfNoTransactionsInfoComponent } from '@ghostfolio/ui/no-transactions-info';
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
@ -12,7 +12,7 @@ import { PositionsComponent } from './positions.component';
|
|||||||
exports: [PositionsComponent],
|
exports: [PositionsComponent],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
GfNoTransactionsInfoModule,
|
GfNoTransactionsInfoComponent,
|
||||||
GfPositionModule,
|
GfPositionModule,
|
||||||
MatButtonModule
|
MatButtonModule
|
||||||
],
|
],
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { GfRuleModule } from '@ghostfolio/client/components/rule/rule.module';
|
import { GfRuleModule } from '@ghostfolio/client/components/rule/rule.module';
|
||||||
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
|
import { GfNoTransactionsInfoComponent } from '@ghostfolio/ui/no-transactions-info';
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
@ -14,7 +14,7 @@ import { RulesComponent } from './rules.component';
|
|||||||
exports: [RulesComponent],
|
exports: [RulesComponent],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
GfNoTransactionsInfoModule,
|
GfNoTransactionsInfoComponent,
|
||||||
GfPositionModule,
|
GfPositionModule,
|
||||||
GfRuleModule,
|
GfRuleModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
@ -12,7 +12,7 @@ import { SubscriptionInterstitialDialog } from './subscription-interstitial-dial
|
|||||||
declarations: [SubscriptionInterstitialDialog],
|
declarations: [SubscriptionInterstitialDialog],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
GfPremiumIndicatorModule,
|
GfPremiumIndicatorComponent,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatDialogModule,
|
MatDialogModule,
|
||||||
RouterModule
|
RouterModule
|
||||||
|
@ -67,9 +67,9 @@ export class CreateOrUpdateAccessDialog implements OnDestroy {
|
|||||||
|
|
||||||
public onSubmit() {
|
public onSubmit() {
|
||||||
const access: CreateAccessDto = {
|
const access: CreateAccessDto = {
|
||||||
alias: this.accessForm.controls['alias'].value,
|
alias: this.accessForm.get('alias').value,
|
||||||
granteeUserId: this.accessForm.controls['userId'].value,
|
granteeUserId: this.accessForm.get('userId').value,
|
||||||
permissions: [this.accessForm.controls['permissions'].value]
|
permissions: [this.accessForm.get('permissions').value]
|
||||||
};
|
};
|
||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (accessForm.controls['type'].value === 'PRIVATE') {
|
@if (accessForm.get('type').value === 'PRIVATE') {
|
||||||
<div>
|
<div>
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
<mat-label i18n>Permission</mat-label>
|
<mat-label i18n>Permission</mat-label>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { GfPortfolioAccessTableModule } from '@ghostfolio/client/components/access-table/access-table.module';
|
import { GfPortfolioAccessTableModule } from '@ghostfolio/client/components/access-table/access-table.module';
|
||||||
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
@ -17,7 +17,7 @@ import { UserAccountAccessComponent } from './user-account-access.component';
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
GfCreateOrUpdateAccessDialogModule,
|
GfCreateOrUpdateAccessDialogModule,
|
||||||
GfPortfolioAccessTableModule,
|
GfPortfolioAccessTableModule,
|
||||||
GfPremiumIndicatorModule,
|
GfPremiumIndicatorComponent,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatDialogModule,
|
MatDialogModule,
|
||||||
RouterModule
|
RouterModule
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { GfMembershipCardModule } from '@ghostfolio/ui/membership-card';
|
import { GfMembershipCardComponent } from '@ghostfolio/ui/membership-card';
|
||||||
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
|
||||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
@ -15,9 +15,9 @@ import { UserAccountMembershipComponent } from './user-account-membership.compon
|
|||||||
exports: [UserAccountMembershipComponent],
|
exports: [UserAccountMembershipComponent],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
GfMembershipCardModule,
|
GfMembershipCardComponent,
|
||||||
GfPremiumIndicatorModule,
|
GfPremiumIndicatorComponent,
|
||||||
GfValueModule,
|
GfValueComponent,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
RouterModule
|
RouterModule
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
@ -18,7 +18,7 @@ import { UserAccountSettingsComponent } from './user-account-settings.component'
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
GfValueModule,
|
GfValueComponent,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
|
@ -63,9 +63,11 @@ export class HttpResponseInterceptor implements HttpInterceptor {
|
|||||||
undefined,
|
undefined,
|
||||||
{ duration: 6000 }
|
{ duration: 6000 }
|
||||||
);
|
);
|
||||||
} else if (!error.url.endsWith('auth/anonymous')) {
|
} else if (!error.url.includes('/auth')) {
|
||||||
this.snackBarRef = this.snackBar.open(
|
this.snackBarRef = this.snackBar.open(
|
||||||
$localize`This feature requires a subscription.`,
|
this.hasPermissionForSubscription
|
||||||
|
? $localize`This feature requires a subscription.`
|
||||||
|
: $localize`This action is not allowed.`,
|
||||||
this.hasPermissionForSubscription
|
this.hasPermissionForSubscription
|
||||||
? $localize`Upgrade Plan`
|
? $localize`Upgrade Plan`
|
||||||
: undefined,
|
: undefined,
|
||||||
|
@ -233,6 +233,8 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
|||||||
.afterClosed()
|
.afterClosed()
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
|
this.fetchAccounts();
|
||||||
|
|
||||||
this.router.navigate(['.'], { relativeTo: this.route });
|
this.router.navigate(['.'], { relativeTo: this.route });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
|
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
|
||||||
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
|
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
|
import { validateObjectForForm } from '@ghostfolio/client/util/form.util';
|
||||||
import { Currency } from '@ghostfolio/common/interfaces';
|
import { Currency } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -81,7 +82,7 @@ export class CreateOrUpdateAccountDialog implements OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public autoCompleteCheck() {
|
public autoCompleteCheck() {
|
||||||
const inputValue = this.accountForm.controls['platformId'].value;
|
const inputValue = this.accountForm.get('platformId').value;
|
||||||
|
|
||||||
if (typeof inputValue === 'string') {
|
if (typeof inputValue === 'string') {
|
||||||
const matchingEntry = this.platforms.find(({ name }) => {
|
const matchingEntry = this.platforms.find(({ name }) => {
|
||||||
@ -89,7 +90,7 @@ export class CreateOrUpdateAccountDialog implements OnDestroy {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (matchingEntry) {
|
if (matchingEntry) {
|
||||||
this.accountForm.controls['platformId'].setValue(matchingEntry);
|
this.accountForm.get('platformId').setValue(matchingEntry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -102,24 +103,40 @@ export class CreateOrUpdateAccountDialog implements OnDestroy {
|
|||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
public onSubmit() {
|
public async onSubmit() {
|
||||||
const account: CreateAccountDto | UpdateAccountDto = {
|
const account: CreateAccountDto | UpdateAccountDto = {
|
||||||
balance: this.accountForm.controls['balance'].value,
|
balance: this.accountForm.get('balance').value,
|
||||||
comment: this.accountForm.controls['comment'].value,
|
comment: this.accountForm.get('comment').value || null,
|
||||||
currency: this.accountForm.controls['currency'].value?.value,
|
currency: this.accountForm.get('currency').value?.value,
|
||||||
id: this.accountForm.controls['accountId'].value,
|
id: this.accountForm.get('accountId').value,
|
||||||
isExcluded: this.accountForm.controls['isExcluded'].value,
|
isExcluded: this.accountForm.get('isExcluded').value,
|
||||||
name: this.accountForm.controls['name'].value,
|
name: this.accountForm.get('name').value,
|
||||||
platformId: this.accountForm.controls['platformId'].value?.id ?? null
|
platformId: this.accountForm.get('platformId').value?.id || null
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.data.account.id) {
|
try {
|
||||||
(account as UpdateAccountDto).id = this.data.account.id;
|
if (this.data.account.id) {
|
||||||
} else {
|
(account as UpdateAccountDto).id = this.data.account.id;
|
||||||
delete (account as CreateAccountDto).id;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dialogRef.close({ account });
|
await validateObjectForForm({
|
||||||
|
classDto: UpdateAccountDto,
|
||||||
|
form: this.accountForm,
|
||||||
|
object: account
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
delete (account as CreateAccountDto).id;
|
||||||
|
|
||||||
|
await validateObjectForForm({
|
||||||
|
classDto: CreateAccountDto,
|
||||||
|
form: this.accountForm,
|
||||||
|
object: account
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dialogRef.close({ account });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
(keydown.enter)="$event.stopPropagation()"
|
(keydown.enter)="$event.stopPropagation()"
|
||||||
/>
|
/>
|
||||||
<span class="ml-2" matTextSuffix>{{
|
<span class="ml-2" matTextSuffix>{{
|
||||||
accountForm.controls['currency']?.value?.value
|
accountForm.get('currency')?.value?.value
|
||||||
}}</span>
|
}}</span>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { GfAssetProfileIconComponent } from '@ghostfolio/client/components/asset-profile-icon/asset-profile-icon.component';
|
import { GfAssetProfileIconComponent } from '@ghostfolio/client/components/asset-profile-icon/asset-profile-icon.component';
|
||||||
import { GfCurrencySelectorModule } from '@ghostfolio/ui/currency-selector/currency-selector.module';
|
import { GfCurrencySelectorComponent } from '@ghostfolio/ui/currency-selector';
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
@ -19,7 +19,7 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.c
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
GfAssetProfileIconComponent,
|
GfAssetProfileIconComponent,
|
||||||
GfCurrencySelectorModule,
|
GfCurrencySelectorComponent,
|
||||||
MatAutocompleteModule,
|
MatAutocompleteModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatCheckboxModule,
|
MatCheckboxModule,
|
||||||
|
@ -66,9 +66,9 @@ export class TransferBalanceDialog implements OnDestroy {
|
|||||||
|
|
||||||
public onSubmit() {
|
public onSubmit() {
|
||||||
const account: TransferBalanceDto = {
|
const account: TransferBalanceDto = {
|
||||||
accountIdFrom: this.transferBalanceForm.controls['fromAccount'].value,
|
accountIdFrom: this.transferBalanceForm.get('fromAccount').value,
|
||||||
accountIdTo: this.transferBalanceForm.controls['toAccount'].value,
|
accountIdTo: this.transferBalanceForm.get('toAccount').value,
|
||||||
balance: this.transferBalanceForm.controls['balance'].value
|
balance: this.transferBalanceForm.get('balance').value
|
||||||
};
|
};
|
||||||
|
|
||||||
this.dialogRef.close({ account });
|
this.dialogRef.close({ account });
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
|
||||||
|
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
@ -6,7 +6,7 @@ import { RouterModule } from '@angular/router';
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'page' },
|
host: { class: 'page' },
|
||||||
imports: [GfPremiumIndicatorModule, MatButtonModule, RouterModule],
|
imports: [GfPremiumIndicatorComponent, MatButtonModule, RouterModule],
|
||||||
selector: 'gf-black-friday-2022-page',
|
selector: 'gf-black-friday-2022-page',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
templateUrl: './black-friday-2022-page.html'
|
templateUrl: './black-friday-2022-page.html'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
|
||||||
|
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
@ -6,7 +6,7 @@ import { RouterModule } from '@angular/router';
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'page' },
|
host: { class: 'page' },
|
||||||
imports: [GfPremiumIndicatorModule, MatButtonModule, RouterModule],
|
imports: [GfPremiumIndicatorComponent, MatButtonModule, RouterModule],
|
||||||
selector: 'gf-black-week-2023-page',
|
selector: 'gf-black-week-2023-page',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
templateUrl: './black-week-2023-page.html'
|
templateUrl: './black-week-2023-page.html'
|
||||||
|
@ -30,7 +30,8 @@
|
|||||||
systems, including
|
systems, including
|
||||||
<a href="https://github.com/bigbeartechworld/big-bear-casaos"
|
<a href="https://github.com/bigbeartechworld/big-bear-casaos"
|
||||||
>CasaOS</a
|
>CasaOS</a
|
||||||
>, <a href="https://www.runtipi.io/docs/apps-available">Runtipi</a>,
|
>, Home Assistant,
|
||||||
|
<a href="https://www.runtipi.io/docs/apps-available">Runtipi</a>,
|
||||||
<a href="https://truecharts.org/charts/stable/ghostfolio"
|
<a href="https://truecharts.org/charts/stable/ghostfolio"
|
||||||
>TrueCharts</a
|
>TrueCharts</a
|
||||||
>, <a href="https://apps.umbrel.com/app/ghostfolio">Umbrel</a>, and
|
>, <a href="https://apps.umbrel.com/app/ghostfolio">Umbrel</a>, and
|
||||||
@ -117,6 +118,25 @@
|
|||||||
providers are considered experimental.</mat-card-content
|
providers are considered experimental.</mat-card-content
|
||||||
>
|
>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
<mat-card appearance="outlined" class="mb-3">
|
||||||
|
<mat-card-header>
|
||||||
|
<mat-card-title>How do I add a custom asset?</mat-card-title>
|
||||||
|
</mat-card-header>
|
||||||
|
<mat-card-content>
|
||||||
|
<p>
|
||||||
|
If you want to track an asset that is not available from any data
|
||||||
|
provider, you can create a custom asset as follows.
|
||||||
|
</p>
|
||||||
|
<ol>
|
||||||
|
<li>Go to the <i>Admin Control</i> panel</li>
|
||||||
|
<li>Go to the <i>Market Data</i> section</li>
|
||||||
|
<li>Create an asset profile</li>
|
||||||
|
<li>Select <i>Add Manually</i> and enter a unique symbol</li>
|
||||||
|
<li>Edit your asset profile</li>
|
||||||
|
<li>Add a new activity by searching for the symbol</li>
|
||||||
|
</ol>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
<mat-card appearance="outlined" class="mb-3">
|
<mat-card appearance="outlined" class="mb-3">
|
||||||
<mat-card-header>
|
<mat-card-header>
|
||||||
<mat-card-title>Which devices are supported?</mat-card-title>
|
<mat-card-title>Which devices are supported?</mat-card-title>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
@ -13,7 +13,7 @@ import { FeaturesPageComponent } from './features-page.component';
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
FeaturesPageRoutingModule,
|
FeaturesPageRoutingModule,
|
||||||
GfPremiumIndicatorModule,
|
GfPremiumIndicatorComponent,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatCardModule
|
MatCardModule
|
||||||
],
|
],
|
||||||
|
@ -2,12 +2,6 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col text-center">
|
<div class="col text-center">
|
||||||
<div>
|
<div>
|
||||||
<div class="badge badge-light badge-pill border mb-3 px-3 py-2">
|
|
||||||
<a href="../en/blog/2023/09/ghostfolio-2"
|
|
||||||
><span class="mr-1 text-uppercase" i18n>New</span>
|
|
||||||
<span class="font-weight-normal">Ghostfolio 2.0</span></a
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<h1 class="font-weight-bold intro" i18n>
|
<h1 class="font-weight-bold intro" i18n>
|
||||||
Manage your wealth like a boss
|
Manage your wealth like a boss
|
||||||
</h1>
|
</h1>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
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 { GfCarouselModule } from '@ghostfolio/ui/carousel';
|
import { GfCarouselComponent } from '@ghostfolio/ui/carousel';
|
||||||
import { GfLogoModule } from '@ghostfolio/ui/logo';
|
import { GfLogoComponent } from '@ghostfolio/ui/logo';
|
||||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
@ -16,9 +16,9 @@ import { LandingPageComponent } from './landing-page.component';
|
|||||||
declarations: [LandingPageComponent],
|
declarations: [LandingPageComponent],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
GfCarouselModule,
|
GfCarouselComponent,
|
||||||
GfLogoModule,
|
GfLogoComponent,
|
||||||
GfValueModule,
|
GfValueComponent,
|
||||||
GfWorldMapChartModule,
|
GfWorldMapChartModule,
|
||||||
LandingPageRoutingModule,
|
LandingPageRoutingModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
@ -9,7 +9,12 @@ import { OpenPageComponent } from './open-page.component';
|
|||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [OpenPageComponent],
|
declarations: [OpenPageComponent],
|
||||||
imports: [CommonModule, GfValueModule, MatCardModule, OpenPageRoutingModule],
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
GfValueComponent,
|
||||||
|
MatCardModule,
|
||||||
|
OpenPageRoutingModule
|
||||||
|
],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
export class OpenPageModule {}
|
export class OpenPageModule {}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user