Compare commits
36 Commits
Author | SHA1 | Date | |
---|---|---|---|
3330ae70b6 | |||
96a615dc5d | |||
98f44323da | |||
8adacd9760 | |||
908aba170d | |||
d599797a65 | |||
8ac1272a9d | |||
0a85a56c67 | |||
4ad5590838 | |||
5b8af68e71 | |||
80d043729d | |||
178166d86b | |||
37358fb480 | |||
616d601cf6 | |||
e88b889fdd | |||
f6cdc4ff47 | |||
818c40fc61 | |||
3589e72aea | |||
e68aa1fa68 | |||
bb76ace95d | |||
2bd9309827 | |||
f743c03e17 | |||
dfcf826b4f | |||
218efbb5bd | |||
ba3b4564cd | |||
3723a5c725 | |||
bf256ae50c | |||
c4b6273886 | |||
7f047362cc | |||
66900ffa24 | |||
33e05e949a | |||
94d2310217 | |||
b7d950f3f9 | |||
b7943889da | |||
84aa542560 | |||
4bd41ffa41 |
84
CHANGELOG.md
84
CHANGELOG.md
@ -5,6 +5,85 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## 1.37.0 - 13.08.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the calculated net worth to the portfolio summary tab on the home page
|
||||||
|
- Added the calculated time in market to the portfolio summary tab on the home page
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the usability of the tabs on the home page
|
||||||
|
- Restructured the portfolio summary tab on the home page
|
||||||
|
- Upgraded `angular-material-css-vars` from version `2.1.0` to `2.1.2`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the position detail chart if there are missing historical data around the first buy date
|
||||||
|
- Fixed the snack bar background color in dark mode
|
||||||
|
- Fixed the search functionality for symbols (filter for supported currencies)
|
||||||
|
|
||||||
|
## 1.36.0 - 09.08.2021
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the data gathering handling on server restart
|
||||||
|
- Respected the cash balance on the allocations page
|
||||||
|
- Eliminated the name from the scraper configuration
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed hidden cryptocurrency holdings
|
||||||
|
|
||||||
|
## 1.35.0 - 08.08.2021
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Hid the pagination of tabs
|
||||||
|
- Improved the classification of assets
|
||||||
|
- Improved the support for future transactions (drafts)
|
||||||
|
- Optimized the accounts table for mobile
|
||||||
|
- Upgraded `chart.js` from version `3.3.2` to `3.5.0`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Added a fallback if the exchange rate service has not been initialized correctly
|
||||||
|
|
||||||
|
### Todo
|
||||||
|
|
||||||
|
- Apply data migration (`yarn database:push`)
|
||||||
|
|
||||||
|
## 1.34.0 - 07.08.2021
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Restructured the page hierarchy
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue with the currency conversion of the market price in the position detail dialog
|
||||||
|
- Fixed the chart and missing data of positions from the past in the position detail dialog
|
||||||
|
|
||||||
|
## 1.33.0 - 05.08.2021
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue of a division by zero in the portfolio calculations
|
||||||
|
- Fixed an issue with the currency conversion in the position detail dialog
|
||||||
|
|
||||||
|
## 1.32.0 - 04.08.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the name to the position detail dialog when opened from the transactions table
|
||||||
|
- Added a screenshot to the blog posts
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the missing market state in the positions tab
|
||||||
|
- Fixed the chart of positions with differing currency from user
|
||||||
|
|
||||||
## 1.31.1 - 01.08.2021
|
## 1.31.1 - 01.08.2021
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
@ -19,8 +98,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Refactored the core engine for the portfolio calculations
|
- Rewritten the core engine for the portfolio calculations
|
||||||
- Improved the performance of the portfolio calculations
|
- Switched to [Time-Weighted Rate of Return](https://www.investopedia.com/terms/t/time-weightedror.asp) (TWR) for the performance calculation
|
||||||
|
- Improved the performance of the portfolio calculations
|
||||||
|
|
||||||
## 1.30.0 - 31.07.2021
|
## 1.30.0 - 31.07.2021
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
<strong>Open Source Wealth Management Software made for Humans</strong>
|
<strong>Open Source Wealth Management Software made for Humans</strong>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="https://ghostfol.io"><strong>Live Demo</strong></a>
|
<a href="https://ghostfol.io"><strong>Live Demo</strong></a> | <a href="https://ghostfol.io/pricing"><strong>Ghostfolio Premium</strong></a> | <a href="https://ghostfol.io/en/blog/2021/07/hello-ghostfolio"><strong>Blog</strong></a> | <a href="https://twitter.com/ghostfolio_"><strong>Twitter</strong></a>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="#contributing">
|
<a href="#contributing">
|
||||||
@ -26,6 +26,10 @@
|
|||||||
|
|
||||||
**Ghostfolio** is an open source wealth management software built with web technology. The application empowers busy people to keep track of their wealth like stocks, ETFs or cryptocurrencies and make solid, data-driven investment decisions.
|
**Ghostfolio** is an open source wealth management software built with web technology. The application empowers busy people to keep track of their wealth like stocks, ETFs or cryptocurrencies and make solid, data-driven investment decisions.
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="./apps/client/src/assets/images/screenshot.png" width="300">
|
||||||
|
</div>
|
||||||
|
|
||||||
## Ghostfolio Premium
|
## Ghostfolio Premium
|
||||||
|
|
||||||
Our official **[Ghostfolio Premium](https://ghostfol.io/pricing)** cloud offering is the easiest way to get started. Due to the time it saves, this will be the best option for most people. The revenue is used for covering the hosting costs.
|
Our official **[Ghostfolio Premium](https://ghostfol.io/pricing)** cloud offering is the easiest way to get started. Due to the time it saves, this will be the best option for most people. The revenue is used for covering the hosting costs.
|
||||||
|
@ -5,7 +5,7 @@ import { Prisma } from '@prisma/client';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AccessService {
|
export class AccessService {
|
||||||
public constructor(private prisma: PrismaService) {}
|
public constructor(private readonly prismaService: PrismaService) {}
|
||||||
|
|
||||||
public async accesses(params: {
|
public async accesses(params: {
|
||||||
include?: Prisma.AccessInclude;
|
include?: Prisma.AccessInclude;
|
||||||
@ -17,7 +17,7 @@ export class AccessService {
|
|||||||
}): Promise<AccessWithGranteeUser[]> {
|
}): Promise<AccessWithGranteeUser[]> {
|
||||||
const { include, skip, take, cursor, where, orderBy } = params;
|
const { include, skip, take, cursor, where, orderBy } = params;
|
||||||
|
|
||||||
return this.prisma.access.findMany({
|
return this.prismaService.access.findMany({
|
||||||
cursor,
|
cursor,
|
||||||
include,
|
include,
|
||||||
orderBy,
|
orderBy,
|
||||||
|
@ -3,21 +3,19 @@ import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Account, Currency, Order, Prisma } from '@prisma/client';
|
import { Account, Currency, Order, Prisma } from '@prisma/client';
|
||||||
|
|
||||||
import { RedisCacheService } from '../redis-cache/redis-cache.service';
|
|
||||||
import { CashDetails } from './interfaces/cash-details.interface';
|
import { CashDetails } from './interfaces/cash-details.interface';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AccountService {
|
export class AccountService {
|
||||||
public constructor(
|
public constructor(
|
||||||
private exchangeRateDataService: ExchangeRateDataService,
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||||
private readonly redisCacheService: RedisCacheService,
|
private readonly prismaService: PrismaService
|
||||||
private prisma: PrismaService
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async account(
|
public async account(
|
||||||
accountWhereUniqueInput: Prisma.AccountWhereUniqueInput
|
accountWhereUniqueInput: Prisma.AccountWhereUniqueInput
|
||||||
): Promise<Account | null> {
|
): Promise<Account | null> {
|
||||||
return this.prisma.account.findUnique({
|
return this.prismaService.account.findUnique({
|
||||||
where: accountWhereUniqueInput
|
where: accountWhereUniqueInput
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -30,7 +28,7 @@ export class AccountService {
|
|||||||
Order?: Order[];
|
Order?: Order[];
|
||||||
}
|
}
|
||||||
> {
|
> {
|
||||||
return this.prisma.account.findUnique({
|
return this.prismaService.account.findUnique({
|
||||||
include: accountInclude,
|
include: accountInclude,
|
||||||
where: accountWhereUniqueInput
|
where: accountWhereUniqueInput
|
||||||
});
|
});
|
||||||
@ -46,7 +44,7 @@ export class AccountService {
|
|||||||
}): Promise<Account[]> {
|
}): Promise<Account[]> {
|
||||||
const { include, skip, take, cursor, where, orderBy } = params;
|
const { include, skip, take, cursor, where, orderBy } = params;
|
||||||
|
|
||||||
return this.prisma.account.findMany({
|
return this.prismaService.account.findMany({
|
||||||
cursor,
|
cursor,
|
||||||
include,
|
include,
|
||||||
orderBy,
|
orderBy,
|
||||||
@ -60,7 +58,7 @@ export class AccountService {
|
|||||||
data: Prisma.AccountCreateInput,
|
data: Prisma.AccountCreateInput,
|
||||||
aUserId: string
|
aUserId: string
|
||||||
): Promise<Account> {
|
): Promise<Account> {
|
||||||
return this.prisma.account.create({
|
return this.prismaService.account.create({
|
||||||
data
|
data
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -69,9 +67,7 @@ export class AccountService {
|
|||||||
where: Prisma.AccountWhereUniqueInput,
|
where: Prisma.AccountWhereUniqueInput,
|
||||||
aUserId: string
|
aUserId: string
|
||||||
): Promise<Account> {
|
): Promise<Account> {
|
||||||
this.redisCacheService.remove(`${aUserId}.portfolio`);
|
return this.prismaService.account.delete({
|
||||||
|
|
||||||
return this.prisma.account.delete({
|
|
||||||
where
|
where
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -105,7 +101,7 @@ export class AccountService {
|
|||||||
aUserId: string
|
aUserId: string
|
||||||
): Promise<Account> {
|
): Promise<Account> {
|
||||||
const { data, where } = params;
|
const { data, where } = params;
|
||||||
return this.prisma.account.update({
|
return this.prismaService.account.update({
|
||||||
data,
|
data,
|
||||||
where
|
where
|
||||||
});
|
});
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { AdminData } from '@ghostfolio/common/interfaces';
|
import { AdminData } from '@ghostfolio/common/interfaces';
|
||||||
@ -7,8 +8,9 @@ import { Currency } from '@prisma/client';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class AdminService {
|
export class AdminService {
|
||||||
public constructor(
|
public constructor(
|
||||||
private exchangeRateDataService: ExchangeRateDataService,
|
private readonly dataGatheringService: DataGatheringService,
|
||||||
private prisma: PrismaService
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||||
|
private readonly prismaService: PrismaService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async get(): Promise<AdminData> {
|
public async get(): Promise<AdminData> {
|
||||||
@ -61,24 +63,22 @@ export class AdminService {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
lastDataGathering: await this.getLastDataGathering(),
|
lastDataGathering: await this.getLastDataGathering(),
|
||||||
transactionCount: await this.prisma.order.count(),
|
transactionCount: await this.prismaService.order.count(),
|
||||||
userCount: await this.prisma.user.count(),
|
userCount: await this.prismaService.user.count(),
|
||||||
users: await this.getUsersWithAnalytics()
|
users: await this.getUsersWithAnalytics()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getLastDataGathering() {
|
private async getLastDataGathering() {
|
||||||
const lastDataGathering = await this.prisma.property.findUnique({
|
const lastDataGathering =
|
||||||
where: { key: 'LAST_DATA_GATHERING' }
|
await this.dataGatheringService.getLastDataGathering();
|
||||||
});
|
|
||||||
|
|
||||||
if (lastDataGathering?.value) {
|
if (lastDataGathering) {
|
||||||
return new Date(lastDataGathering.value);
|
return lastDataGathering;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataGatheringInProgress = await this.prisma.property.findUnique({
|
const dataGatheringInProgress =
|
||||||
where: { key: 'LOCKED_DATA_GATHERING' }
|
await this.dataGatheringService.getIsInProgress();
|
||||||
});
|
|
||||||
|
|
||||||
if (dataGatheringInProgress) {
|
if (dataGatheringInProgress) {
|
||||||
return 'IN_PROGRESS';
|
return 'IN_PROGRESS';
|
||||||
@ -88,7 +88,7 @@ export class AdminService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getUsersWithAnalytics() {
|
private async getUsersWithAnalytics() {
|
||||||
return await this.prisma.user.findMany({
|
return await this.prismaService.user.findMany({
|
||||||
orderBy: {
|
orderBy: {
|
||||||
Analytics: {
|
Analytics: {
|
||||||
updatedAt: 'desc'
|
updatedAt: 'desc'
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||||
import { Controller } from '@nestjs/common';
|
import { Controller } from '@nestjs/common';
|
||||||
|
|
||||||
import { PrismaService } from '../services/prisma.service';
|
|
||||||
import { RedisCacheService } from './redis-cache/redis-cache.service';
|
import { RedisCacheService } from './redis-cache/redis-cache.service';
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
export class AppController {
|
export class AppController {
|
||||||
public constructor(
|
public constructor(
|
||||||
private prisma: PrismaService,
|
private readonly dataGatheringService: DataGatheringService,
|
||||||
private readonly redisCacheService: RedisCacheService
|
private readonly redisCacheService: RedisCacheService
|
||||||
) {
|
) {
|
||||||
this.initialize();
|
this.initialize();
|
||||||
@ -15,17 +15,12 @@ export class AppController {
|
|||||||
private async initialize() {
|
private async initialize() {
|
||||||
this.redisCacheService.reset();
|
this.redisCacheService.reset();
|
||||||
|
|
||||||
const isDataGatheringLocked = await this.prisma.property.findUnique({
|
const isDataGatheringInProgress =
|
||||||
where: { key: 'LOCKED_DATA_GATHERING' }
|
await this.dataGatheringService.getIsInProgress();
|
||||||
});
|
|
||||||
|
|
||||||
if (!isDataGatheringLocked) {
|
if (isDataGatheringInProgress) {
|
||||||
// Prepare for automatical data gather if not locked
|
// Prepare for automatical data gathering, if hung up in progress state
|
||||||
await this.prisma.property.deleteMany({
|
await this.dataGatheringService.reset();
|
||||||
where: {
|
|
||||||
OR: [{ key: 'LAST_DATA_GATHERING' }, { key: 'LOCKED_DATA_GATHERING' }]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,13 +7,13 @@ import { AuthDevice, Prisma } from '@prisma/client';
|
|||||||
export class AuthDeviceService {
|
export class AuthDeviceService {
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly configurationService: ConfigurationService,
|
private readonly configurationService: ConfigurationService,
|
||||||
private prisma: PrismaService
|
private readonly prismaService: PrismaService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async authDevice(
|
public async authDevice(
|
||||||
where: Prisma.AuthDeviceWhereUniqueInput
|
where: Prisma.AuthDeviceWhereUniqueInput
|
||||||
): Promise<AuthDevice | null> {
|
): Promise<AuthDevice | null> {
|
||||||
return this.prisma.authDevice.findUnique({
|
return this.prismaService.authDevice.findUnique({
|
||||||
where
|
where
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -26,7 +26,7 @@ export class AuthDeviceService {
|
|||||||
orderBy?: Prisma.AuthDeviceOrderByInput;
|
orderBy?: Prisma.AuthDeviceOrderByInput;
|
||||||
}): Promise<AuthDevice[]> {
|
}): Promise<AuthDevice[]> {
|
||||||
const { skip, take, cursor, where, orderBy } = params;
|
const { skip, take, cursor, where, orderBy } = params;
|
||||||
return this.prisma.authDevice.findMany({
|
return this.prismaService.authDevice.findMany({
|
||||||
skip,
|
skip,
|
||||||
take,
|
take,
|
||||||
cursor,
|
cursor,
|
||||||
@ -38,7 +38,7 @@ export class AuthDeviceService {
|
|||||||
public async createAuthDevice(
|
public async createAuthDevice(
|
||||||
data: Prisma.AuthDeviceCreateInput
|
data: Prisma.AuthDeviceCreateInput
|
||||||
): Promise<AuthDevice> {
|
): Promise<AuthDevice> {
|
||||||
return this.prisma.authDevice.create({
|
return this.prismaService.authDevice.create({
|
||||||
data
|
data
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -49,7 +49,7 @@ export class AuthDeviceService {
|
|||||||
}): Promise<AuthDevice> {
|
}): Promise<AuthDevice> {
|
||||||
const { data, where } = params;
|
const { data, where } = params;
|
||||||
|
|
||||||
return this.prisma.authDevice.update({
|
return this.prismaService.authDevice.update({
|
||||||
data,
|
data,
|
||||||
where
|
where
|
||||||
});
|
});
|
||||||
@ -58,7 +58,7 @@ export class AuthDeviceService {
|
|||||||
public async deleteAuthDevice(
|
public async deleteAuthDevice(
|
||||||
where: Prisma.AuthDeviceWhereUniqueInput
|
where: Prisma.AuthDeviceWhereUniqueInput
|
||||||
): Promise<AuthDevice> {
|
): Promise<AuthDevice> {
|
||||||
return this.prisma.authDevice.delete({
|
return this.prismaService.authDevice.delete({
|
||||||
where
|
where
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ import { UserService } from '../user/user.service';
|
|||||||
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||||
public constructor(
|
public constructor(
|
||||||
readonly configurationService: ConfigurationService,
|
readonly configurationService: ConfigurationService,
|
||||||
private prisma: PrismaService,
|
private readonly prismaService: PrismaService,
|
||||||
private readonly userService: UserService
|
private readonly userService: UserService
|
||||||
) {
|
) {
|
||||||
super({
|
super({
|
||||||
@ -24,7 +24,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
|||||||
const user = await this.userService.user({ id });
|
const user = await this.userService.user({ id });
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
await this.prisma.analytics.upsert({
|
await this.prismaService.analytics.upsert({
|
||||||
create: { User: { connect: { id: user.id } } },
|
create: { User: { connect: { id: user.id } } },
|
||||||
update: { activityCount: { increment: 1 }, updatedAt: new Date() },
|
update: { activityCount: { increment: 1 }, updatedAt: new Date() },
|
||||||
where: { userId: user.id }
|
where: { userId: user.id }
|
||||||
|
2
apps/api/src/app/cache/cache.controller.ts
vendored
2
apps/api/src/app/cache/cache.controller.ts
vendored
@ -21,6 +21,6 @@ export class CacheController {
|
|||||||
public async flushCache(): Promise<void> {
|
public async flushCache(): Promise<void> {
|
||||||
this.redisCacheService.reset();
|
this.redisCacheService.reset();
|
||||||
|
|
||||||
return this.cacheService.flush(this.request.user.id);
|
return this.cacheService.flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
19
apps/api/src/app/cache/cache.module.ts
vendored
19
apps/api/src/app/cache/cache.module.ts
vendored
@ -1,3 +1,10 @@
|
|||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||||
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
||||||
|
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
|
||||||
|
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
||||||
|
import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
|
||||||
|
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
@ -8,6 +15,16 @@ import { CacheService } from './cache.service';
|
|||||||
@Module({
|
@Module({
|
||||||
imports: [RedisCacheModule],
|
imports: [RedisCacheModule],
|
||||||
controllers: [CacheController],
|
controllers: [CacheController],
|
||||||
providers: [CacheService, PrismaService]
|
providers: [
|
||||||
|
AlphaVantageService,
|
||||||
|
CacheService,
|
||||||
|
ConfigurationService,
|
||||||
|
DataGatheringService,
|
||||||
|
DataProviderService,
|
||||||
|
GhostfolioScraperApiService,
|
||||||
|
PrismaService,
|
||||||
|
RakutenRapidApiService,
|
||||||
|
YahooFinanceService
|
||||||
|
]
|
||||||
})
|
})
|
||||||
export class CacheModule {}
|
export class CacheModule {}
|
||||||
|
14
apps/api/src/app/cache/cache.service.ts
vendored
14
apps/api/src/app/cache/cache.service.ts
vendored
@ -1,16 +1,14 @@
|
|||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CacheService {
|
export class CacheService {
|
||||||
public constructor(private prisma: PrismaService) {}
|
public constructor(
|
||||||
|
private readonly dataGaterhingService: DataGatheringService
|
||||||
|
) {}
|
||||||
|
|
||||||
public async flush(aUserId: string): Promise<void> {
|
public async flush(): Promise<void> {
|
||||||
await this.prisma.property.deleteMany({
|
await this.dataGaterhingService.reset();
|
||||||
where: {
|
|
||||||
OR: [{ key: 'LAST_DATA_GATHERING' }, { key: 'LOCKED_DATA_GATHERING' }]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ export interface TransactionPointSymbol {
|
|||||||
currency: Currency;
|
currency: Currency;
|
||||||
firstBuyDate: string;
|
firstBuyDate: string;
|
||||||
investment: Big;
|
investment: Big;
|
||||||
name: string;
|
|
||||||
quantity: Big;
|
quantity: Big;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
transactionCount: number;
|
transactionCount: number;
|
||||||
|
@ -7,7 +7,7 @@ import { DateQuery } from './interfaces/date-query.interface';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MarketDataService {
|
export class MarketDataService {
|
||||||
public constructor(private prisma: PrismaService) {}
|
public constructor(private readonly prismaService: PrismaService) {}
|
||||||
|
|
||||||
public async get({
|
public async get({
|
||||||
date,
|
date,
|
||||||
@ -16,7 +16,7 @@ export class MarketDataService {
|
|||||||
date: Date;
|
date: Date;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
}): Promise<MarketData> {
|
}): Promise<MarketData> {
|
||||||
return await this.prisma.marketData.findFirst({
|
return await this.prismaService.marketData.findFirst({
|
||||||
where: {
|
where: {
|
||||||
symbol,
|
symbol,
|
||||||
date: resetHours(date)
|
date: resetHours(date)
|
||||||
@ -31,7 +31,7 @@ export class MarketDataService {
|
|||||||
dateQuery: DateQuery;
|
dateQuery: DateQuery;
|
||||||
symbols: string[];
|
symbols: string[];
|
||||||
}): Promise<MarketData[]> {
|
}): Promise<MarketData[]> {
|
||||||
return await this.prisma.marketData.findMany({
|
return await this.prismaService.marketData.findMany({
|
||||||
orderBy: [
|
orderBy: [
|
||||||
{
|
{
|
||||||
date: 'asc'
|
date: 'asc'
|
||||||
|
@ -170,7 +170,6 @@ describe('PortfolioCalculator', () => {
|
|||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
investment: new Big('1443.8'),
|
investment: new Big('1443.8'),
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
|
||||||
quantity: new Big('10'),
|
quantity: new Big('10'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
@ -184,7 +183,6 @@ describe('PortfolioCalculator', () => {
|
|||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
investment: new Big('2923.7'),
|
investment: new Big('2923.7'),
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
|
||||||
quantity: new Big('20'),
|
quantity: new Big('20'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
transactionCount: 2
|
transactionCount: 2
|
||||||
@ -198,7 +196,6 @@ describe('PortfolioCalculator', () => {
|
|||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
investment: new Big('652.55'),
|
investment: new Big('652.55'),
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
|
||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
transactionCount: 3
|
transactionCount: 3
|
||||||
@ -212,7 +209,6 @@ describe('PortfolioCalculator', () => {
|
|||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
investment: new Big('6627.05'),
|
investment: new Big('6627.05'),
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
|
||||||
quantity: new Big('35'),
|
quantity: new Big('35'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
transactionCount: 5
|
transactionCount: 5
|
||||||
@ -226,7 +222,6 @@ describe('PortfolioCalculator', () => {
|
|||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
investment: new Big('8403.95'),
|
investment: new Big('8403.95'),
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
|
||||||
quantity: new Big('45'),
|
quantity: new Big('45'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
transactionCount: 6
|
transactionCount: 6
|
||||||
@ -262,7 +257,6 @@ describe('PortfolioCalculator', () => {
|
|||||||
date: '2019-02-01',
|
date: '2019-02-01',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
|
||||||
quantity: new Big('10'),
|
quantity: new Big('10'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('1443.8'),
|
investment: new Big('1443.8'),
|
||||||
@ -276,7 +270,6 @@ describe('PortfolioCalculator', () => {
|
|||||||
date: '2019-08-03',
|
date: '2019-08-03',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
|
||||||
quantity: new Big('20'),
|
quantity: new Big('20'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('2923.7'),
|
investment: new Big('2923.7'),
|
||||||
@ -290,7 +283,6 @@ describe('PortfolioCalculator', () => {
|
|||||||
date: '2019-09-01',
|
date: '2019-09-01',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
name: 'Amazon.com, Inc.',
|
|
||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
symbol: 'AMZN',
|
symbol: 'AMZN',
|
||||||
investment: new Big('10109.95'),
|
investment: new Big('10109.95'),
|
||||||
@ -299,7 +291,6 @@ describe('PortfolioCalculator', () => {
|
|||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
|
||||||
quantity: new Big('20'),
|
quantity: new Big('20'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('2923.7'),
|
investment: new Big('2923.7'),
|
||||||
@ -313,7 +304,6 @@ describe('PortfolioCalculator', () => {
|
|||||||
date: '2020-02-02',
|
date: '2020-02-02',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
name: 'Amazon.com, Inc.',
|
|
||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
symbol: 'AMZN',
|
symbol: 'AMZN',
|
||||||
investment: new Big('10109.95'),
|
investment: new Big('10109.95'),
|
||||||
@ -322,7 +312,6 @@ describe('PortfolioCalculator', () => {
|
|||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
|
||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('652.55'),
|
investment: new Big('652.55'),
|
||||||
@ -336,7 +325,6 @@ describe('PortfolioCalculator', () => {
|
|||||||
date: '2021-02-01',
|
date: '2021-02-01',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
name: 'Amazon.com, Inc.',
|
|
||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
symbol: 'AMZN',
|
symbol: 'AMZN',
|
||||||
investment: new Big('10109.95'),
|
investment: new Big('10109.95'),
|
||||||
@ -345,7 +333,6 @@ describe('PortfolioCalculator', () => {
|
|||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
|
||||||
quantity: new Big('15'),
|
quantity: new Big('15'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('2684.05'),
|
investment: new Big('2684.05'),
|
||||||
@ -359,7 +346,6 @@ describe('PortfolioCalculator', () => {
|
|||||||
date: '2021-08-01',
|
date: '2021-08-01',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
name: 'Amazon.com, Inc.',
|
|
||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
symbol: 'AMZN',
|
symbol: 'AMZN',
|
||||||
investment: new Big('10109.95'),
|
investment: new Big('10109.95'),
|
||||||
@ -368,7 +354,6 @@ describe('PortfolioCalculator', () => {
|
|||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
|
||||||
quantity: new Big('25'),
|
quantity: new Big('25'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('4460.95'),
|
investment: new Big('4460.95'),
|
||||||
@ -411,124 +396,9 @@ describe('PortfolioCalculator', () => {
|
|||||||
const portfolioItemsAtTransactionPoints =
|
const portfolioItemsAtTransactionPoints =
|
||||||
portfolioCalculator.getTransactionPoints();
|
portfolioCalculator.getTransactionPoints();
|
||||||
|
|
||||||
expect(portfolioItemsAtTransactionPoints).toEqual([
|
expect(portfolioItemsAtTransactionPoints).toEqual(
|
||||||
{
|
transactionPointsBuyAndSell
|
||||||
date: '2019-02-01',
|
);
|
||||||
items: [
|
|
||||||
{
|
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
|
||||||
quantity: new Big('10'),
|
|
||||||
symbol: 'VTI',
|
|
||||||
investment: new Big('1443.8'),
|
|
||||||
currency: Currency.USD,
|
|
||||||
firstBuyDate: '2019-02-01',
|
|
||||||
transactionCount: 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: '2019-08-03',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
|
||||||
quantity: new Big('20'),
|
|
||||||
symbol: 'VTI',
|
|
||||||
investment: new Big('2923.7'),
|
|
||||||
currency: Currency.USD,
|
|
||||||
firstBuyDate: '2019-02-01',
|
|
||||||
transactionCount: 2
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: '2019-09-01',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
name: 'Amazon.com, Inc.',
|
|
||||||
quantity: new Big('5'),
|
|
||||||
symbol: 'AMZN',
|
|
||||||
investment: new Big('10109.95'),
|
|
||||||
currency: Currency.USD,
|
|
||||||
firstBuyDate: '2019-09-01',
|
|
||||||
transactionCount: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
|
||||||
quantity: new Big('20'),
|
|
||||||
symbol: 'VTI',
|
|
||||||
investment: new Big('2923.7'),
|
|
||||||
currency: Currency.USD,
|
|
||||||
firstBuyDate: '2019-02-01',
|
|
||||||
transactionCount: 2
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: '2020-02-02',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
name: 'Amazon.com, Inc.',
|
|
||||||
quantity: new Big('5'),
|
|
||||||
symbol: 'AMZN',
|
|
||||||
investment: new Big('10109.95'),
|
|
||||||
currency: Currency.USD,
|
|
||||||
firstBuyDate: '2019-09-01',
|
|
||||||
transactionCount: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
|
||||||
quantity: new Big('5'),
|
|
||||||
symbol: 'VTI',
|
|
||||||
investment: new Big('652.55'),
|
|
||||||
currency: Currency.USD,
|
|
||||||
firstBuyDate: '2019-02-01',
|
|
||||||
transactionCount: 3
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: '2020-08-02',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
|
||||||
quantity: new Big('5'),
|
|
||||||
symbol: 'VTI',
|
|
||||||
investment: new Big('652.55'),
|
|
||||||
currency: Currency.USD,
|
|
||||||
firstBuyDate: '2019-02-01',
|
|
||||||
transactionCount: 3
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: '2021-02-01',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
|
||||||
quantity: new Big('15'),
|
|
||||||
symbol: 'VTI',
|
|
||||||
investment: new Big('2684.05'),
|
|
||||||
currency: Currency.USD,
|
|
||||||
firstBuyDate: '2019-02-01',
|
|
||||||
transactionCount: 4
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: '2021-08-01',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
|
||||||
quantity: new Big('25'),
|
|
||||||
symbol: 'VTI',
|
|
||||||
investment: new Big('4460.95'),
|
|
||||||
currency: Currency.USD,
|
|
||||||
firstBuyDate: '2019-02-01',
|
|
||||||
transactionCount: 5
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('with mixed symbols', () => {
|
it('with mixed symbols', () => {
|
||||||
@ -545,7 +415,6 @@ describe('PortfolioCalculator', () => {
|
|||||||
date: '2017-01-03',
|
date: '2017-01-03',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
name: 'Tesla, Inc.',
|
|
||||||
quantity: new Big('50'),
|
quantity: new Big('50'),
|
||||||
symbol: 'TSLA',
|
symbol: 'TSLA',
|
||||||
investment: new Big('2148.5'),
|
investment: new Big('2148.5'),
|
||||||
@ -559,7 +428,6 @@ describe('PortfolioCalculator', () => {
|
|||||||
date: '2017-07-01',
|
date: '2017-07-01',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
name: 'Bitcoin USD',
|
|
||||||
quantity: new Big('0.5614682'),
|
quantity: new Big('0.5614682'),
|
||||||
symbol: 'BTCUSD',
|
symbol: 'BTCUSD',
|
||||||
investment: new Big('1999.9999999999998659756'),
|
investment: new Big('1999.9999999999998659756'),
|
||||||
@ -568,7 +436,6 @@ describe('PortfolioCalculator', () => {
|
|||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Tesla, Inc.',
|
|
||||||
quantity: new Big('50'),
|
quantity: new Big('50'),
|
||||||
symbol: 'TSLA',
|
symbol: 'TSLA',
|
||||||
investment: new Big('2148.5'),
|
investment: new Big('2148.5'),
|
||||||
@ -582,7 +449,6 @@ describe('PortfolioCalculator', () => {
|
|||||||
date: '2018-09-01',
|
date: '2018-09-01',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
name: 'Amazon.com, Inc.',
|
|
||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
symbol: 'AMZN',
|
symbol: 'AMZN',
|
||||||
investment: new Big('10109.95'),
|
investment: new Big('10109.95'),
|
||||||
@ -591,7 +457,6 @@ describe('PortfolioCalculator', () => {
|
|||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Bitcoin USD',
|
|
||||||
quantity: new Big('0.5614682'),
|
quantity: new Big('0.5614682'),
|
||||||
symbol: 'BTCUSD',
|
symbol: 'BTCUSD',
|
||||||
investment: new Big('1999.9999999999998659756'),
|
investment: new Big('1999.9999999999998659756'),
|
||||||
@ -600,7 +465,6 @@ describe('PortfolioCalculator', () => {
|
|||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Tesla, Inc.',
|
|
||||||
quantity: new Big('50'),
|
quantity: new Big('50'),
|
||||||
symbol: 'TSLA',
|
symbol: 'TSLA',
|
||||||
investment: new Big('2148.5'),
|
investment: new Big('2148.5'),
|
||||||
@ -645,7 +509,6 @@ describe('PortfolioCalculator', () => {
|
|||||||
grossPerformancePercentage: new Big('-0.08595335390431712673'), // (657.62-719.46)/719.46=-0.08595335390431712673
|
grossPerformancePercentage: new Big('-0.08595335390431712673'), // (657.62-719.46)/719.46=-0.08595335390431712673
|
||||||
investment: new Big('719.46'),
|
investment: new Big('719.46'),
|
||||||
marketPrice: 657.62,
|
marketPrice: 657.62,
|
||||||
name: 'Tesla, Inc.',
|
|
||||||
quantity: new Big('1'),
|
quantity: new Big('1'),
|
||||||
symbol: 'TSLA',
|
symbol: 'TSLA',
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
@ -684,7 +547,6 @@ describe('PortfolioCalculator', () => {
|
|||||||
grossPerformancePercentage: new Big('-0.08595335390431712673'), // (657.62-719.46)/719.46=-0.08595335390431712673
|
grossPerformancePercentage: new Big('-0.08595335390431712673'), // (657.62-719.46)/719.46=-0.08595335390431712673
|
||||||
investment: new Big('719.46'),
|
investment: new Big('719.46'),
|
||||||
marketPrice: 657.62,
|
marketPrice: 657.62,
|
||||||
name: 'Tesla, Inc.',
|
|
||||||
quantity: new Big('1'),
|
quantity: new Big('1'),
|
||||||
symbol: 'TSLA',
|
symbol: 'TSLA',
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
@ -723,7 +585,6 @@ describe('PortfolioCalculator', () => {
|
|||||||
grossPerformancePercentage: new Big('-0.01356013560135601356'), // 657.62/666.66-1=-0.013560136
|
grossPerformancePercentage: new Big('-0.01356013560135601356'), // 657.62/666.66-1=-0.013560136
|
||||||
investment: new Big('719.46'),
|
investment: new Big('719.46'),
|
||||||
marketPrice: 657.62,
|
marketPrice: 657.62,
|
||||||
name: 'Tesla, Inc.',
|
|
||||||
quantity: new Big('1'),
|
quantity: new Big('1'),
|
||||||
symbol: 'TSLA',
|
symbol: 'TSLA',
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
@ -765,7 +626,6 @@ describe('PortfolioCalculator', () => {
|
|||||||
),
|
),
|
||||||
investment: new Big('4460.95'),
|
investment: new Big('4460.95'),
|
||||||
marketPrice: 194.86,
|
marketPrice: 194.86,
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
|
||||||
quantity: new Big('25'),
|
quantity: new Big('25'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
transactionCount: 5
|
transactionCount: 5
|
||||||
@ -774,6 +634,138 @@ describe('PortfolioCalculator', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('with buy and sell', async () => {
|
||||||
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
|
currentRateService,
|
||||||
|
Currency.USD
|
||||||
|
);
|
||||||
|
portfolioCalculator.setTransactionPoints(transactionPointsBuyAndSell);
|
||||||
|
|
||||||
|
const spy = jest
|
||||||
|
.spyOn(Date, 'now')
|
||||||
|
.mockImplementation(() => new Date(Date.UTC(2020, 9, 24)).getTime()); // 2020-10-24
|
||||||
|
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||||
|
parseDate('2019-01-01')
|
||||||
|
);
|
||||||
|
spy.mockRestore();
|
||||||
|
|
||||||
|
expect(currentPositions).toEqual({
|
||||||
|
hasErrors: false,
|
||||||
|
currentValue: new Big('4871.5'),
|
||||||
|
grossPerformance: new Big('240.4'),
|
||||||
|
grossPerformancePercentage: new Big('0.01104605615757711361'),
|
||||||
|
totalInvestment: new Big('4460.95'),
|
||||||
|
positions: [
|
||||||
|
{
|
||||||
|
averagePrice: new Big('0'),
|
||||||
|
currency: 'USD',
|
||||||
|
firstBuyDate: '2019-09-01',
|
||||||
|
grossPerformance: new Big('0'),
|
||||||
|
grossPerformancePercentage: new Big('0'),
|
||||||
|
investment: new Big('0'),
|
||||||
|
marketPrice: 2021.99,
|
||||||
|
quantity: new Big('0'),
|
||||||
|
symbol: 'AMZN',
|
||||||
|
transactionCount: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
averagePrice: new Big('178.438'),
|
||||||
|
currency: 'USD',
|
||||||
|
firstBuyDate: '2019-02-01',
|
||||||
|
grossPerformance: new Big('240.4'),
|
||||||
|
grossPerformancePercentage: new Big(
|
||||||
|
'0.08839407904876477101219019935616297754969945667391763908415656216989674494965785538864363782688167989866968512455219637257546280462751601552'
|
||||||
|
),
|
||||||
|
investment: new Big('4460.95'),
|
||||||
|
marketPrice: 194.86,
|
||||||
|
quantity: new Big('25'),
|
||||||
|
symbol: 'VTI',
|
||||||
|
transactionCount: 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with buy, sell, buy', async () => {
|
||||||
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
|
currentRateService,
|
||||||
|
Currency.USD
|
||||||
|
);
|
||||||
|
portfolioCalculator.setTransactionPoints([
|
||||||
|
{
|
||||||
|
date: '2019-09-01',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
quantity: new Big('5'),
|
||||||
|
symbol: 'VTI',
|
||||||
|
investment: new Big('805.9'),
|
||||||
|
currency: Currency.USD,
|
||||||
|
firstBuyDate: '2019-09-01',
|
||||||
|
transactionCount: 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: '2020-08-02',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
quantity: new Big('0'),
|
||||||
|
symbol: 'VTI',
|
||||||
|
investment: new Big('0'),
|
||||||
|
currency: Currency.USD,
|
||||||
|
firstBuyDate: '2019-09-01',
|
||||||
|
transactionCount: 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: '2021-02-01',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
quantity: new Big('5'),
|
||||||
|
symbol: 'VTI',
|
||||||
|
investment: new Big('1013.9'),
|
||||||
|
currency: Currency.USD,
|
||||||
|
firstBuyDate: '2019-09-01',
|
||||||
|
transactionCount: 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const spy = jest
|
||||||
|
.spyOn(Date, 'now')
|
||||||
|
.mockImplementation(() => new Date(Date.UTC(2021, 7, 1)).getTime()); // 2021-08-01
|
||||||
|
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||||
|
parseDate('2019-02-01')
|
||||||
|
);
|
||||||
|
spy.mockRestore();
|
||||||
|
|
||||||
|
expect(currentPositions).toEqual({
|
||||||
|
hasErrors: false,
|
||||||
|
currentValue: new Big('1086.7'),
|
||||||
|
grossPerformance: new Big('207.6'),
|
||||||
|
grossPerformancePercentage: new Big('0.2516103956224511062'),
|
||||||
|
totalInvestment: new Big('1013.9'),
|
||||||
|
positions: [
|
||||||
|
{
|
||||||
|
averagePrice: new Big('202.78'),
|
||||||
|
currency: 'USD',
|
||||||
|
firstBuyDate: '2019-09-01',
|
||||||
|
grossPerformance: new Big('207.6'),
|
||||||
|
grossPerformancePercentage: new Big(
|
||||||
|
'0.2516103956224511061954915466429950404846'
|
||||||
|
),
|
||||||
|
investment: new Big('1013.9'),
|
||||||
|
marketPrice: 217.34,
|
||||||
|
quantity: new Big('5'),
|
||||||
|
symbol: 'VTI',
|
||||||
|
transactionCount: 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('with performance since Jan 1st, 2020', async () => {
|
it('with performance since Jan 1st, 2020', async () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
currentRateService,
|
currentRateService,
|
||||||
@ -847,7 +839,6 @@ describe('PortfolioCalculator', () => {
|
|||||||
grossPerformancePercentage: new Big(
|
grossPerformancePercentage: new Big(
|
||||||
'0.2753783814827239834392742298083677500037'
|
'0.2753783814827239834392742298083677500037'
|
||||||
),
|
),
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
|
||||||
currency: 'USD'
|
currency: 'USD'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -867,9 +858,8 @@ describe('PortfolioCalculator', () => {
|
|||||||
date: '2010-12-31',
|
date: '2010-12-31',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
name: 'Mutual Fund A',
|
|
||||||
quantity: new Big('1000000'), // 1 million
|
quantity: new Big('1000000'), // 1 million
|
||||||
symbol: 'MFA',
|
symbol: 'MFA', // Mutual Fund A
|
||||||
investment: new Big('1000000'), // 1 million
|
investment: new Big('1000000'), // 1 million
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
firstBuyDate: '2010-12-31',
|
firstBuyDate: '2010-12-31',
|
||||||
@ -881,9 +871,8 @@ describe('PortfolioCalculator', () => {
|
|||||||
date: '2011-08-15',
|
date: '2011-08-15',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
name: 'Mutual Fund A',
|
|
||||||
quantity: new Big('1086022.689344541'), // 1,000,000 + 100,000 / 1.162484
|
quantity: new Big('1086022.689344541'), // 1,000,000 + 100,000 / 1.162484
|
||||||
symbol: 'MFA',
|
symbol: 'MFA', // Mutual Fund A
|
||||||
investment: new Big('1100000'), // 1,000,000 + 100,000
|
investment: new Big('1100000'), // 1,000,000 + 100,000
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
firstBuyDate: '2010-12-31',
|
firstBuyDate: '2010-12-31',
|
||||||
@ -919,7 +908,6 @@ describe('PortfolioCalculator', () => {
|
|||||||
transactionCount: 2,
|
transactionCount: 2,
|
||||||
grossPerformance: new Big('92327.999656600898394721'), // 1'192'328 - 1'100'000 = 92'328
|
grossPerformance: new Big('92327.999656600898394721'), // 1'192'328 - 1'100'000 = 92'328
|
||||||
grossPerformancePercentage: new Big('0.09788498099999947808927632'), // 9.79 %
|
grossPerformancePercentage: new Big('0.09788498099999947808927632'), // 9.79 %
|
||||||
name: 'Mutual Fund A',
|
|
||||||
currency: 'USD'
|
currency: 'USD'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -939,18 +927,16 @@ describe('PortfolioCalculator', () => {
|
|||||||
date: '2012-12-31',
|
date: '2012-12-31',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
name: 'Sub Portfolio A',
|
|
||||||
quantity: new Big('200'),
|
quantity: new Big('200'),
|
||||||
symbol: 'SPA',
|
symbol: 'SPA', // Sub Portfolio A
|
||||||
investment: new Big('200'),
|
investment: new Big('200'),
|
||||||
currency: Currency.CHF,
|
currency: Currency.CHF,
|
||||||
firstBuyDate: '2012-12-31',
|
firstBuyDate: '2012-12-31',
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Sub Portfolio B',
|
|
||||||
quantity: new Big('300'),
|
quantity: new Big('300'),
|
||||||
symbol: 'SPB',
|
symbol: 'SPB', // Sub Portfolio B
|
||||||
investment: new Big('300'),
|
investment: new Big('300'),
|
||||||
currency: Currency.CHF,
|
currency: Currency.CHF,
|
||||||
firstBuyDate: '2012-12-31',
|
firstBuyDate: '2012-12-31',
|
||||||
@ -962,18 +948,16 @@ describe('PortfolioCalculator', () => {
|
|||||||
date: '2013-12-31',
|
date: '2013-12-31',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
name: 'Sub Portfolio A',
|
|
||||||
quantity: new Big('200'),
|
quantity: new Big('200'),
|
||||||
symbol: 'SPA',
|
symbol: 'SPA', // Sub Portfolio A
|
||||||
investment: new Big('200'),
|
investment: new Big('200'),
|
||||||
currency: Currency.CHF,
|
currency: Currency.CHF,
|
||||||
firstBuyDate: '2012-12-31',
|
firstBuyDate: '2012-12-31',
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Sub Portfolio B',
|
|
||||||
quantity: new Big('300'),
|
quantity: new Big('300'),
|
||||||
symbol: 'SPB',
|
symbol: 'SPB', // Sub Portfolio B
|
||||||
investment: new Big('300'),
|
investment: new Big('300'),
|
||||||
currency: Currency.CHF,
|
currency: Currency.CHF,
|
||||||
firstBuyDate: '2012-12-31',
|
firstBuyDate: '2012-12-31',
|
||||||
@ -1009,7 +993,6 @@ describe('PortfolioCalculator', () => {
|
|||||||
transactionCount: 1,
|
transactionCount: 1,
|
||||||
grossPerformance: new Big('5'), // 205 - 200
|
grossPerformance: new Big('5'), // 205 - 200
|
||||||
grossPerformancePercentage: new Big('0.025'),
|
grossPerformancePercentage: new Big('0.025'),
|
||||||
name: 'Sub Portfolio A',
|
|
||||||
currency: 'CHF'
|
currency: 'CHF'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1022,7 +1005,6 @@ describe('PortfolioCalculator', () => {
|
|||||||
transactionCount: 1,
|
transactionCount: 1,
|
||||||
grossPerformance: new Big('12'), // 312 - 300
|
grossPerformance: new Big('12'), // 312 - 300
|
||||||
grossPerformancePercentage: new Big('0.04'),
|
grossPerformancePercentage: new Big('0.04'),
|
||||||
name: 'Sub Portfolio B',
|
|
||||||
currency: 'CHF'
|
currency: 'CHF'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -1611,7 +1593,6 @@ describe('PortfolioCalculator', () => {
|
|||||||
date: '2019-02-01',
|
date: '2019-02-01',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
name: 'Amazon.com, Inc.',
|
|
||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
symbol: 'AMZN',
|
symbol: 'AMZN',
|
||||||
investment: new Big('10109.95'),
|
investment: new Big('10109.95'),
|
||||||
@ -1620,7 +1601,6 @@ describe('PortfolioCalculator', () => {
|
|||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
|
||||||
quantity: new Big('10'),
|
quantity: new Big('10'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('1443.8'),
|
investment: new Big('1443.8'),
|
||||||
@ -1744,7 +1724,6 @@ const orderTslaTransactionPoint: TransactionPoint[] = [
|
|||||||
date: '2021-01-01',
|
date: '2021-01-01',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
name: 'Tesla, Inc.',
|
|
||||||
quantity: new Big('1'),
|
quantity: new Big('1'),
|
||||||
symbol: 'TSLA',
|
symbol: 'TSLA',
|
||||||
investment: new Big('719.46'),
|
investment: new Big('719.46'),
|
||||||
@ -1761,7 +1740,6 @@ const ordersVTITransactionPoints: TransactionPoint[] = [
|
|||||||
date: '2019-02-01',
|
date: '2019-02-01',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
|
||||||
quantity: new Big('10'),
|
quantity: new Big('10'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('1443.8'),
|
investment: new Big('1443.8'),
|
||||||
@ -1775,7 +1753,6 @@ const ordersVTITransactionPoints: TransactionPoint[] = [
|
|||||||
date: '2019-08-03',
|
date: '2019-08-03',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
|
||||||
quantity: new Big('20'),
|
quantity: new Big('20'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('2923.7'),
|
investment: new Big('2923.7'),
|
||||||
@ -1789,7 +1766,6 @@ const ordersVTITransactionPoints: TransactionPoint[] = [
|
|||||||
date: '2020-02-02',
|
date: '2020-02-02',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
|
||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('652.55'),
|
investment: new Big('652.55'),
|
||||||
@ -1803,7 +1779,6 @@ const ordersVTITransactionPoints: TransactionPoint[] = [
|
|||||||
date: '2021-02-01',
|
date: '2021-02-01',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
|
||||||
quantity: new Big('15'),
|
quantity: new Big('15'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('2684.05'),
|
investment: new Big('2684.05'),
|
||||||
@ -1817,7 +1792,140 @@ const ordersVTITransactionPoints: TransactionPoint[] = [
|
|||||||
date: '2021-08-01',
|
date: '2021-08-01',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
quantity: new Big('25'),
|
||||||
|
symbol: 'VTI',
|
||||||
|
investment: new Big('4460.95'),
|
||||||
|
currency: Currency.USD,
|
||||||
|
firstBuyDate: '2019-02-01',
|
||||||
|
transactionCount: 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const transactionPointsBuyAndSell = [
|
||||||
|
{
|
||||||
|
date: '2019-02-01',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
quantity: new Big('10'),
|
||||||
|
symbol: 'VTI',
|
||||||
|
investment: new Big('1443.8'),
|
||||||
|
currency: Currency.USD,
|
||||||
|
firstBuyDate: '2019-02-01',
|
||||||
|
transactionCount: 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: '2019-08-03',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
quantity: new Big('20'),
|
||||||
|
symbol: 'VTI',
|
||||||
|
investment: new Big('2923.7'),
|
||||||
|
currency: Currency.USD,
|
||||||
|
firstBuyDate: '2019-02-01',
|
||||||
|
transactionCount: 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: '2019-09-01',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
quantity: new Big('5'),
|
||||||
|
symbol: 'AMZN',
|
||||||
|
investment: new Big('10109.95'),
|
||||||
|
currency: Currency.USD,
|
||||||
|
firstBuyDate: '2019-09-01',
|
||||||
|
transactionCount: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
quantity: new Big('20'),
|
||||||
|
symbol: 'VTI',
|
||||||
|
investment: new Big('2923.7'),
|
||||||
|
currency: Currency.USD,
|
||||||
|
firstBuyDate: '2019-02-01',
|
||||||
|
transactionCount: 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: '2020-02-02',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
quantity: new Big('5'),
|
||||||
|
symbol: 'AMZN',
|
||||||
|
investment: new Big('10109.95'),
|
||||||
|
currency: Currency.USD,
|
||||||
|
firstBuyDate: '2019-09-01',
|
||||||
|
transactionCount: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
quantity: new Big('5'),
|
||||||
|
symbol: 'VTI',
|
||||||
|
investment: new Big('652.55'),
|
||||||
|
currency: Currency.USD,
|
||||||
|
firstBuyDate: '2019-02-01',
|
||||||
|
transactionCount: 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: '2020-08-02',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
quantity: new Big('0'),
|
||||||
|
symbol: 'AMZN',
|
||||||
|
investment: new Big('0'),
|
||||||
|
currency: Currency.USD,
|
||||||
|
firstBuyDate: '2019-09-01',
|
||||||
|
transactionCount: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
quantity: new Big('5'),
|
||||||
|
symbol: 'VTI',
|
||||||
|
investment: new Big('652.55'),
|
||||||
|
currency: Currency.USD,
|
||||||
|
firstBuyDate: '2019-02-01',
|
||||||
|
transactionCount: 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: '2021-02-01',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
quantity: new Big('0'),
|
||||||
|
symbol: 'AMZN',
|
||||||
|
investment: new Big('0'),
|
||||||
|
currency: Currency.USD,
|
||||||
|
firstBuyDate: '2019-09-01',
|
||||||
|
transactionCount: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
quantity: new Big('15'),
|
||||||
|
symbol: 'VTI',
|
||||||
|
investment: new Big('2684.05'),
|
||||||
|
currency: Currency.USD,
|
||||||
|
firstBuyDate: '2019-02-01',
|
||||||
|
transactionCount: 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: '2021-08-01',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
quantity: new Big('0'),
|
||||||
|
symbol: 'AMZN',
|
||||||
|
investment: new Big('0'),
|
||||||
|
currency: Currency.USD,
|
||||||
|
firstBuyDate: '2019-09-01',
|
||||||
|
transactionCount: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
quantity: new Big('25'),
|
quantity: new Big('25'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('4460.95'),
|
investment: new Big('4460.95'),
|
||||||
|
@ -23,8 +23,7 @@ import {
|
|||||||
isAfter,
|
isAfter,
|
||||||
isBefore,
|
isBefore,
|
||||||
max,
|
max,
|
||||||
min,
|
min
|
||||||
subDays
|
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import { flatten } from 'lodash';
|
import { flatten } from 'lodash';
|
||||||
|
|
||||||
@ -53,17 +52,19 @@ export class PortfolioCalculator {
|
|||||||
const factor = this.getFactor(order.type);
|
const factor = this.getFactor(order.type);
|
||||||
const unitPrice = new Big(order.unitPrice);
|
const unitPrice = new Big(order.unitPrice);
|
||||||
if (oldAccumulatedSymbol) {
|
if (oldAccumulatedSymbol) {
|
||||||
|
const newQuantity = order.quantity
|
||||||
|
.mul(factor)
|
||||||
|
.plus(oldAccumulatedSymbol.quantity);
|
||||||
currentTransactionPointItem = {
|
currentTransactionPointItem = {
|
||||||
currency: order.currency,
|
currency: order.currency,
|
||||||
firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
|
firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
|
||||||
investment: unitPrice
|
investment: newQuantity.eq(0)
|
||||||
|
? new Big(0)
|
||||||
|
: unitPrice
|
||||||
.mul(order.quantity)
|
.mul(order.quantity)
|
||||||
.mul(factor)
|
.mul(factor)
|
||||||
.add(oldAccumulatedSymbol.investment),
|
.add(oldAccumulatedSymbol.investment),
|
||||||
name: order.name,
|
quantity: newQuantity,
|
||||||
quantity: order.quantity
|
|
||||||
.mul(factor)
|
|
||||||
.plus(oldAccumulatedSymbol.quantity),
|
|
||||||
symbol: order.symbol,
|
symbol: order.symbol,
|
||||||
transactionCount: oldAccumulatedSymbol.transactionCount + 1
|
transactionCount: oldAccumulatedSymbol.transactionCount + 1
|
||||||
};
|
};
|
||||||
@ -72,7 +73,6 @@ export class PortfolioCalculator {
|
|||||||
currency: order.currency,
|
currency: order.currency,
|
||||||
firstBuyDate: order.date,
|
firstBuyDate: order.date,
|
||||||
investment: unitPrice.mul(order.quantity).mul(factor),
|
investment: unitPrice.mul(order.quantity).mul(factor),
|
||||||
name: order.name,
|
|
||||||
quantity: order.quantity.mul(factor),
|
quantity: order.quantity.mul(factor),
|
||||||
symbol: order.symbol,
|
symbol: order.symbol,
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
@ -85,11 +85,7 @@ export class PortfolioCalculator {
|
|||||||
const newItems = items.filter(
|
const newItems = items.filter(
|
||||||
(transactionPointItem) => transactionPointItem.symbol !== order.symbol
|
(transactionPointItem) => transactionPointItem.symbol !== order.symbol
|
||||||
);
|
);
|
||||||
if (!currentTransactionPointItem.quantity.eq(0)) {
|
|
||||||
newItems.push(currentTransactionPointItem);
|
newItems.push(currentTransactionPointItem);
|
||||||
} else {
|
|
||||||
delete symbols[order.symbol];
|
|
||||||
}
|
|
||||||
newItems.sort((a, b) => a.symbol.localeCompare(b.symbol));
|
newItems.sort((a, b) => a.symbol.localeCompare(b.symbol));
|
||||||
if (lastDate !== currentDate || lastTransactionPoint === null) {
|
if (lastDate !== currentDate || lastTransactionPoint === null) {
|
||||||
lastTransactionPoint = {
|
lastTransactionPoint = {
|
||||||
@ -234,6 +230,7 @@ export class PortfolioCalculator {
|
|||||||
if (i === firstIndex || !initialValues[item.symbol]) {
|
if (i === firstIndex || !initialValues[item.symbol]) {
|
||||||
initialValues[item.symbol] = initialValue;
|
initialValues[item.symbol] = initialValue;
|
||||||
}
|
}
|
||||||
|
if (!item.quantity.eq(0)) {
|
||||||
if (!initialValue) {
|
if (!initialValue) {
|
||||||
invalidSymbols.push(item.symbol);
|
invalidSymbols.push(item.symbol);
|
||||||
hasErrors = true;
|
hasErrors = true;
|
||||||
@ -258,18 +255,21 @@ export class PortfolioCalculator {
|
|||||||
const currentPerformance = endValue.minus(investedValue);
|
const currentPerformance = endValue.minus(investedValue);
|
||||||
grossPerformance[item.symbol] =
|
grossPerformance[item.symbol] =
|
||||||
oldGrossPerformance.plus(currentPerformance);
|
oldGrossPerformance.plus(currentPerformance);
|
||||||
|
}
|
||||||
lastInvestments[item.symbol] = item.investment;
|
lastInvestments[item.symbol] = item.investment;
|
||||||
lastQuantities[item.symbol] = item.quantity;
|
lastQuantities[item.symbol] = item.quantity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const positions: TimelinePosition[] = [];
|
const positions: TimelinePosition[] = [];
|
||||||
|
|
||||||
for (const item of lastTransactionPoint.items) {
|
for (const item of lastTransactionPoint.items) {
|
||||||
const marketValue = marketSymbolMap[todayString]?.[item.symbol];
|
const marketValue = marketSymbolMap[todayString]?.[item.symbol];
|
||||||
const isValid = invalidSymbols.indexOf(item.symbol) === -1;
|
const isValid = invalidSymbols.indexOf(item.symbol) === -1;
|
||||||
positions.push({
|
positions.push({
|
||||||
averagePrice: item.investment.div(item.quantity),
|
averagePrice: item.quantity.eq(0)
|
||||||
|
? new Big(0)
|
||||||
|
: item.investment.div(item.quantity),
|
||||||
currency: item.currency,
|
currency: item.currency,
|
||||||
firstBuyDate: item.firstBuyDate,
|
firstBuyDate: item.firstBuyDate,
|
||||||
grossPerformance: isValid
|
grossPerformance: isValid
|
||||||
@ -281,7 +281,6 @@ export class PortfolioCalculator {
|
|||||||
: null,
|
: null,
|
||||||
investment: item.investment,
|
investment: item.investment,
|
||||||
marketPrice: marketValue?.toNumber() ?? null,
|
marketPrice: marketValue?.toNumber() ?? null,
|
||||||
name: item.name,
|
|
||||||
quantity: item.quantity,
|
quantity: item.quantity,
|
||||||
symbol: item.symbol,
|
symbol: item.symbol,
|
||||||
transactionCount: item.transactionCount
|
transactionCount: item.transactionCount
|
||||||
@ -401,7 +400,7 @@ export class PortfolioCalculator {
|
|||||||
grossPerformance = grossPerformance.plus(
|
grossPerformance = grossPerformance.plus(
|
||||||
currentPosition.grossPerformance
|
currentPosition.grossPerformance
|
||||||
);
|
);
|
||||||
} else {
|
} else if (!currentPosition.quantity.eq(0)) {
|
||||||
hasErrors = true;
|
hasErrors = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -414,20 +413,25 @@ export class PortfolioCalculator {
|
|||||||
grossPerformancePercentage = grossPerformancePercentage.plus(
|
grossPerformancePercentage = grossPerformancePercentage.plus(
|
||||||
currentPosition.grossPerformancePercentage.mul(currentInitialValue)
|
currentPosition.grossPerformancePercentage.mul(currentInitialValue)
|
||||||
);
|
);
|
||||||
} else {
|
} else if (!currentPosition.quantity.eq(0)) {
|
||||||
console.error(
|
console.error(
|
||||||
`Initial value is missing for symbol ${currentPosition.symbol}`
|
`Initial value is missing for symbol ${currentPosition.symbol}`
|
||||||
);
|
);
|
||||||
hasErrors = true;
|
hasErrors = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!completeInitialValue.eq(0)) {
|
||||||
|
grossPerformancePercentage =
|
||||||
|
grossPerformancePercentage.div(completeInitialValue);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentValue,
|
currentValue,
|
||||||
grossPerformance,
|
grossPerformance,
|
||||||
|
grossPerformancePercentage,
|
||||||
hasErrors,
|
hasErrors,
|
||||||
totalInvestment,
|
totalInvestment
|
||||||
grossPerformancePercentage:
|
|
||||||
grossPerformancePercentage.div(completeInitialValue)
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,12 +11,12 @@ export class ExperimentalService {
|
|||||||
private readonly accountService: AccountService,
|
private readonly accountService: AccountService,
|
||||||
private readonly dataProviderService: DataProviderService,
|
private readonly dataProviderService: DataProviderService,
|
||||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||||
private prisma: PrismaService,
|
private readonly prismaService: PrismaService,
|
||||||
private readonly rulesService: RulesService
|
private readonly rulesService: RulesService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async getBenchmark(aSymbol: string) {
|
public async getBenchmark(aSymbol: string) {
|
||||||
return this.prisma.marketData.findMany({
|
return this.prismaService.marketData.findMany({
|
||||||
orderBy: { date: 'asc' },
|
orderBy: { date: 'asc' },
|
||||||
select: { date: true, marketPrice: true },
|
select: { date: true, marketPrice: true },
|
||||||
where: { symbol: aSymbol }
|
where: { symbol: aSymbol }
|
||||||
|
@ -5,10 +5,10 @@ import { Injectable } from '@nestjs/common';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ExportService {
|
export class ExportService {
|
||||||
public constructor(private prisma: PrismaService) {}
|
public constructor(private readonly prismaService: PrismaService) {}
|
||||||
|
|
||||||
public async export({ userId }: { userId: string }): Promise<Export> {
|
public async export({ userId }: { userId: string }): Promise<Export> {
|
||||||
const orders = await this.prisma.order.findMany({
|
const orders = await this.prismaService.order.findMany({
|
||||||
orderBy: { date: 'desc' },
|
orderBy: { date: 'desc' },
|
||||||
select: {
|
select: {
|
||||||
currency: true,
|
currency: true,
|
||||||
|
@ -24,8 +24,7 @@ export class ImportService {
|
|||||||
type,
|
type,
|
||||||
unitPrice
|
unitPrice
|
||||||
} of orders) {
|
} of orders) {
|
||||||
await this.orderService.createOrder(
|
await this.orderService.createOrder({
|
||||||
{
|
|
||||||
currency,
|
currency,
|
||||||
dataSource,
|
dataSource,
|
||||||
fee,
|
fee,
|
||||||
@ -35,9 +34,7 @@ export class ImportService {
|
|||||||
unitPrice,
|
unitPrice,
|
||||||
date: parseISO(<string>(<unknown>date)),
|
date: parseISO(<string>(<unknown>date)),
|
||||||
User: { connect: { id: userId } }
|
User: { connect: { id: userId } }
|
||||||
},
|
});
|
||||||
userId
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,10 @@
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||||
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
||||||
|
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
|
||||||
|
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
||||||
|
import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
|
||||||
|
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
@ -14,6 +20,16 @@ import { InfoService } from './info.service';
|
|||||||
})
|
})
|
||||||
],
|
],
|
||||||
controllers: [InfoController],
|
controllers: [InfoController],
|
||||||
providers: [ConfigurationService, InfoService, PrismaService]
|
providers: [
|
||||||
|
AlphaVantageService,
|
||||||
|
ConfigurationService,
|
||||||
|
DataGatheringService,
|
||||||
|
DataProviderService,
|
||||||
|
GhostfolioScraperApiService,
|
||||||
|
InfoService,
|
||||||
|
PrismaService,
|
||||||
|
RakutenRapidApiService,
|
||||||
|
YahooFinanceService
|
||||||
|
]
|
||||||
})
|
})
|
||||||
export class InfoModule {}
|
export class InfoModule {}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { InfoItem } from '@ghostfolio/common/interfaces';
|
import { InfoItem } from '@ghostfolio/common/interfaces';
|
||||||
import { Subscription } from '@ghostfolio/common/interfaces/subscription.interface';
|
import { Subscription } from '@ghostfolio/common/interfaces/subscription.interface';
|
||||||
@ -15,13 +16,14 @@ export class InfoService {
|
|||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly configurationService: ConfigurationService,
|
private readonly configurationService: ConfigurationService,
|
||||||
private jwtService: JwtService,
|
private readonly dataGatheringService: DataGatheringService,
|
||||||
private prisma: PrismaService
|
private readonly jwtService: JwtService,
|
||||||
|
private readonly prismaService: PrismaService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async get(): Promise<InfoItem> {
|
public async get(): Promise<InfoItem> {
|
||||||
const info: Partial<InfoItem> = {};
|
const info: Partial<InfoItem> = {};
|
||||||
const platforms = await this.prisma.platform.findMany({
|
const platforms = await this.prismaService.platform.findMany({
|
||||||
orderBy: { name: 'asc' },
|
orderBy: { name: 'asc' },
|
||||||
select: { id: true, name: true }
|
select: { id: true, name: true }
|
||||||
});
|
});
|
||||||
@ -63,7 +65,7 @@ export class InfoService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async countActiveUsers(aDays: number) {
|
private async countActiveUsers(aDays: number) {
|
||||||
return await this.prisma.user.count({
|
return await this.prismaService.user.count({
|
||||||
orderBy: {
|
orderBy: {
|
||||||
Analytics: {
|
Analytics: {
|
||||||
updatedAt: 'desc'
|
updatedAt: 'desc'
|
||||||
@ -116,11 +118,10 @@ export class InfoService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getLastDataGathering() {
|
private async getLastDataGathering() {
|
||||||
const lastDataGathering = await this.prisma.property.findUnique({
|
const lastDataGathering =
|
||||||
where: { key: 'LAST_DATA_GATHERING' }
|
await this.dataGatheringService.getLastDataGathering();
|
||||||
});
|
|
||||||
|
|
||||||
return lastDataGathering?.value ? new Date(lastDataGathering.value) : null;
|
return lastDataGathering ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getStatistics() {
|
private async getStatistics() {
|
||||||
@ -144,7 +145,7 @@ export class InfoService {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const stripeConfig = await this.prisma.property.findUnique({
|
const stripeConfig = await this.prismaService.property.findUnique({
|
||||||
where: { key: 'STRIPE_CONFIG' }
|
where: { key: 'STRIPE_CONFIG' }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -52,15 +52,12 @@ export class OrderController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.orderService.deleteOrder(
|
return this.orderService.deleteOrder({
|
||||||
{
|
|
||||||
id_userId: {
|
id_userId: {
|
||||||
id,
|
id,
|
||||||
userId: this.request.user.id
|
userId: this.request.user.id
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
this.request.user.id
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@ -80,6 +77,11 @@ export class OrderController {
|
|||||||
include: {
|
include: {
|
||||||
Platform: true
|
Platform: true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
SymbolProfile: {
|
||||||
|
select: {
|
||||||
|
name: true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
orderBy: { date: 'desc' },
|
orderBy: { date: 'desc' },
|
||||||
@ -130,8 +132,7 @@ export class OrderController {
|
|||||||
const accountId = data.accountId;
|
const accountId = data.accountId;
|
||||||
delete data.accountId;
|
delete data.accountId;
|
||||||
|
|
||||||
return this.orderService.createOrder(
|
return this.orderService.createOrder({
|
||||||
{
|
|
||||||
...data,
|
...data,
|
||||||
Account: {
|
Account: {
|
||||||
connect: {
|
connect: {
|
||||||
@ -154,9 +155,7 @@ export class OrderController {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
User: { connect: { id: this.request.user.id } }
|
User: { connect: { id: this.request.user.id } }
|
||||||
},
|
});
|
||||||
this.request.user.id
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put(':id')
|
@Put(':id')
|
||||||
@ -193,8 +192,7 @@ export class OrderController {
|
|||||||
const accountId = data.accountId;
|
const accountId = data.accountId;
|
||||||
delete data.accountId;
|
delete data.accountId;
|
||||||
|
|
||||||
return this.orderService.updateOrder(
|
return this.orderService.updateOrder({
|
||||||
{
|
|
||||||
data: {
|
data: {
|
||||||
...data,
|
...data,
|
||||||
date,
|
date,
|
||||||
@ -211,8 +209,6 @@ export class OrderController {
|
|||||||
userId: this.request.user.id
|
userId: this.request.user.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
this.request.user.id
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,21 +6,19 @@ import { DataSource, Order, Prisma } from '@prisma/client';
|
|||||||
import { endOfToday, isAfter } from 'date-fns';
|
import { endOfToday, isAfter } from 'date-fns';
|
||||||
|
|
||||||
import { CacheService } from '../cache/cache.service';
|
import { CacheService } from '../cache/cache.service';
|
||||||
import { RedisCacheService } from '../redis-cache/redis-cache.service';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OrderService {
|
export class OrderService {
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly cacheService: CacheService,
|
private readonly cacheService: CacheService,
|
||||||
private readonly dataGatheringService: DataGatheringService,
|
private readonly dataGatheringService: DataGatheringService,
|
||||||
private readonly redisCacheService: RedisCacheService,
|
private readonly prismaService: PrismaService
|
||||||
private prisma: PrismaService
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async order(
|
public async order(
|
||||||
orderWhereUniqueInput: Prisma.OrderWhereUniqueInput
|
orderWhereUniqueInput: Prisma.OrderWhereUniqueInput
|
||||||
): Promise<Order | null> {
|
): Promise<Order | null> {
|
||||||
return this.prisma.order.findUnique({
|
return this.prismaService.order.findUnique({
|
||||||
where: orderWhereUniqueInput
|
where: orderWhereUniqueInput
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -35,7 +33,7 @@ export class OrderService {
|
|||||||
}): Promise<OrderWithAccount[]> {
|
}): Promise<OrderWithAccount[]> {
|
||||||
const { include, skip, take, cursor, where, orderBy } = params;
|
const { include, skip, take, cursor, where, orderBy } = params;
|
||||||
|
|
||||||
return this.prisma.order.findMany({
|
return this.prismaService.order.findMany({
|
||||||
cursor,
|
cursor,
|
||||||
include,
|
include,
|
||||||
orderBy,
|
orderBy,
|
||||||
@ -45,13 +43,10 @@ export class OrderService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createOrder(
|
public async createOrder(data: Prisma.OrderCreateInput): Promise<Order> {
|
||||||
data: Prisma.OrderCreateInput,
|
const isDraft = isAfter(data.date as Date, endOfToday());
|
||||||
aUserId: string
|
|
||||||
): Promise<Order> {
|
|
||||||
this.redisCacheService.remove(`${aUserId}.portfolio`);
|
|
||||||
|
|
||||||
if (!isAfter(data.date as Date, endOfToday())) {
|
if (!isDraft) {
|
||||||
// Gather symbol data of order in the background, if not draft
|
// Gather symbol data of order in the background, if not draft
|
||||||
this.dataGatheringService.gatherSymbols([
|
this.dataGatheringService.gatherSymbols([
|
||||||
{
|
{
|
||||||
@ -64,36 +59,59 @@ export class OrderService {
|
|||||||
|
|
||||||
this.dataGatheringService.gatherProfileData([data.symbol]);
|
this.dataGatheringService.gatherProfileData([data.symbol]);
|
||||||
|
|
||||||
await this.cacheService.flush(aUserId);
|
await this.cacheService.flush();
|
||||||
|
|
||||||
return this.prisma.order.create({
|
return this.prismaService.order.create({
|
||||||
data
|
data: {
|
||||||
|
...data,
|
||||||
|
isDraft
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteOrder(
|
public async deleteOrder(
|
||||||
where: Prisma.OrderWhereUniqueInput,
|
where: Prisma.OrderWhereUniqueInput
|
||||||
aUserId: string
|
|
||||||
): Promise<Order> {
|
): Promise<Order> {
|
||||||
this.redisCacheService.remove(`${aUserId}.portfolio`);
|
return this.prismaService.order.delete({
|
||||||
|
|
||||||
return this.prisma.order.delete({
|
|
||||||
where
|
where
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateOrder(
|
public getOrders({
|
||||||
params: {
|
includeDrafts = false,
|
||||||
|
userId
|
||||||
|
}: {
|
||||||
|
includeDrafts?: boolean;
|
||||||
|
userId: string;
|
||||||
|
}) {
|
||||||
|
const where: Prisma.OrderWhereInput = { userId };
|
||||||
|
|
||||||
|
if (includeDrafts === false) {
|
||||||
|
where.isDraft = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.orders({
|
||||||
|
where,
|
||||||
|
include: {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
Account: true,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
SymbolProfile: true
|
||||||
|
},
|
||||||
|
orderBy: { date: 'asc' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateOrder(params: {
|
||||||
where: Prisma.OrderWhereUniqueInput;
|
where: Prisma.OrderWhereUniqueInput;
|
||||||
data: Prisma.OrderUpdateInput;
|
data: Prisma.OrderUpdateInput;
|
||||||
},
|
}): Promise<Order> {
|
||||||
aUserId: string
|
|
||||||
): Promise<Order> {
|
|
||||||
const { data, where } = params;
|
const { data, where } = params;
|
||||||
|
|
||||||
this.redisCacheService.remove(`${aUserId}.portfolio`);
|
const isDraft = isAfter(data.date as Date, endOfToday());
|
||||||
|
|
||||||
// Gather symbol data of order in the background
|
if (!isDraft) {
|
||||||
|
// Gather symbol data of order in the background, if not draft
|
||||||
this.dataGatheringService.gatherSymbols([
|
this.dataGatheringService.gatherSymbols([
|
||||||
{
|
{
|
||||||
dataSource: <DataSource>data.dataSource,
|
dataSource: <DataSource>data.dataSource,
|
||||||
@ -101,11 +119,15 @@ export class OrderService {
|
|||||||
symbol: <string>data.symbol
|
symbol: <string>data.symbol
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
await this.cacheService.flush(aUserId);
|
await this.cacheService.flush();
|
||||||
|
|
||||||
return this.prisma.order.update({
|
return this.prismaService.order.update({
|
||||||
data,
|
data: {
|
||||||
|
...data,
|
||||||
|
isDraft
|
||||||
|
},
|
||||||
where
|
where
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -5,11 +5,10 @@ import {
|
|||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
||||||
import {
|
import {
|
||||||
PortfolioItem,
|
|
||||||
PortfolioOverview,
|
|
||||||
PortfolioPerformance,
|
PortfolioPerformance,
|
||||||
PortfolioPosition,
|
PortfolioPosition,
|
||||||
PortfolioReport
|
PortfolioReport,
|
||||||
|
PortfolioSummary
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||||
import {
|
import {
|
||||||
@ -31,6 +30,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 Big from 'big.js';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
|
||||||
@ -203,32 +203,6 @@ export class PortfolioController {
|
|||||||
return <any>res.json(details);
|
return <any>res.json(details);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('overview')
|
|
||||||
@UseGuards(AuthGuard('jwt'))
|
|
||||||
public async getOverview(
|
|
||||||
@Headers('impersonation-id') impersonationId
|
|
||||||
): Promise<PortfolioOverview> {
|
|
||||||
let overview = await this.portfolioService.getOverview(impersonationId);
|
|
||||||
|
|
||||||
if (
|
|
||||||
impersonationId &&
|
|
||||||
!hasPermission(
|
|
||||||
getPermissions(this.request.user.role),
|
|
||||||
permissions.readForeignPortfolio
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
overview = nullifyValuesInObject(overview, [
|
|
||||||
'cash',
|
|
||||||
'committedFunds',
|
|
||||||
'fees',
|
|
||||||
'totalBuy',
|
|
||||||
'totalSell'
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return overview;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('performance')
|
@Get('performance')
|
||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
public async getPerformance(
|
public async getPerformance(
|
||||||
@ -282,6 +256,35 @@ export class PortfolioController {
|
|||||||
return <any>res.json(result);
|
return <any>res.json(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('summary')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
public async getSummary(
|
||||||
|
@Headers('impersonation-id') impersonationId
|
||||||
|
): Promise<PortfolioSummary> {
|
||||||
|
let summary = await this.portfolioService.getSummary(impersonationId);
|
||||||
|
|
||||||
|
if (
|
||||||
|
impersonationId &&
|
||||||
|
!hasPermission(
|
||||||
|
getPermissions(this.request.user.role),
|
||||||
|
permissions.readForeignPortfolio
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
summary = nullifyValuesInObject(summary, [
|
||||||
|
'cash',
|
||||||
|
'committedFunds',
|
||||||
|
'currentGrossPerformance',
|
||||||
|
'currentNetPerformance',
|
||||||
|
'currentValue',
|
||||||
|
'fees',
|
||||||
|
'totalBuy',
|
||||||
|
'totalSell'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
@Get('position/:symbol')
|
@Get('position/:symbol')
|
||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
public async getPosition(
|
public async getPosition(
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||||
|
import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details.interface';
|
||||||
import { CurrentRateService } from '@ghostfolio/api/app/core/current-rate.service';
|
import { CurrentRateService } from '@ghostfolio/api/app/core/current-rate.service';
|
||||||
import { PortfolioOrder } from '@ghostfolio/api/app/core/interfaces/portfolio-order.interface';
|
import { PortfolioOrder } from '@ghostfolio/api/app/core/interfaces/portfolio-order.interface';
|
||||||
import { TimelineSpecification } from '@ghostfolio/api/app/core/interfaces/timeline-specification.interface';
|
import { TimelineSpecification } from '@ghostfolio/api/app/core/interfaces/timeline-specification.interface';
|
||||||
@ -17,16 +18,17 @@ import { FeeRatioInitialInvestment } from '@ghostfolio/api/models/rules/fees/fee
|
|||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
||||||
|
import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { EnhancedSymbolProfile } from '@ghostfolio/api/services/interfaces/symbol-profile.interface';
|
import { EnhancedSymbolProfile } from '@ghostfolio/api/services/interfaces/symbol-profile.interface';
|
||||||
import { RulesService } from '@ghostfolio/api/services/rules.service';
|
import { RulesService } from '@ghostfolio/api/services/rules.service';
|
||||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
||||||
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
import { UNKNOWN_KEY, ghostfolioCashSymbol } from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
||||||
import {
|
import {
|
||||||
PortfolioOverview,
|
|
||||||
PortfolioPerformance,
|
PortfolioPerformance,
|
||||||
PortfolioPosition,
|
PortfolioPosition,
|
||||||
PortfolioReport,
|
PortfolioReport,
|
||||||
|
PortfolioSummary,
|
||||||
Position,
|
Position,
|
||||||
TimelinePosition
|
TimelinePosition
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
@ -38,7 +40,12 @@ import {
|
|||||||
} from '@ghostfolio/common/types';
|
} from '@ghostfolio/common/types';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
import { Currency, DataSource, Type as TypeOfOrder } from '@prisma/client';
|
import {
|
||||||
|
AssetClass,
|
||||||
|
Currency,
|
||||||
|
DataSource,
|
||||||
|
Type as TypeOfOrder
|
||||||
|
} from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import {
|
import {
|
||||||
endOfToday,
|
endOfToday,
|
||||||
@ -83,7 +90,10 @@ export class PortfolioService {
|
|||||||
this.request.user.Settings.currency
|
this.request.user.Settings.currency
|
||||||
);
|
);
|
||||||
|
|
||||||
const { transactionPoints } = await this.getTransactionPoints(userId);
|
const { transactionPoints } = await this.getTransactionPoints({
|
||||||
|
userId,
|
||||||
|
includeDrafts: true
|
||||||
|
});
|
||||||
portfolioCalculator.setTransactionPoints(transactionPoints);
|
portfolioCalculator.setTransactionPoints(transactionPoints);
|
||||||
if (transactionPoints.length === 0) {
|
if (transactionPoints.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
@ -108,7 +118,7 @@ export class PortfolioService {
|
|||||||
this.request.user.Settings.currency
|
this.request.user.Settings.currency
|
||||||
);
|
);
|
||||||
|
|
||||||
const { transactionPoints } = await this.getTransactionPoints(userId);
|
const { transactionPoints } = await this.getTransactionPoints({ userId });
|
||||||
portfolioCalculator.setTransactionPoints(transactionPoints);
|
portfolioCalculator.setTransactionPoints(transactionPoints);
|
||||||
if (transactionPoints.length === 0) {
|
if (transactionPoints.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
@ -141,31 +151,6 @@ export class PortfolioService {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getOverview(
|
|
||||||
aImpersonationId: string
|
|
||||||
): Promise<PortfolioOverview> {
|
|
||||||
const userId = await this.getUserId(aImpersonationId);
|
|
||||||
|
|
||||||
const currency = this.request.user.Settings.currency;
|
|
||||||
const { balance } = await this.accountService.getCashDetails(
|
|
||||||
userId,
|
|
||||||
currency
|
|
||||||
);
|
|
||||||
const orders = await this.getOrders(userId);
|
|
||||||
const fees = this.getFees(orders);
|
|
||||||
|
|
||||||
const totalBuy = this.getTotalByType(orders, currency, TypeOfOrder.BUY);
|
|
||||||
const totalSell = this.getTotalByType(orders, currency, TypeOfOrder.SELL);
|
|
||||||
return {
|
|
||||||
committedFunds: totalBuy - totalSell,
|
|
||||||
fees,
|
|
||||||
cash: balance,
|
|
||||||
ordersCount: orders.length,
|
|
||||||
totalBuy: totalBuy,
|
|
||||||
totalSell: totalSell
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getDetails(
|
public async getDetails(
|
||||||
aImpersonationId: string,
|
aImpersonationId: string,
|
||||||
aDateRange: DateRange = 'max'
|
aDateRange: DateRange = 'max'
|
||||||
@ -178,9 +163,9 @@ export class PortfolioService {
|
|||||||
userCurrency
|
userCurrency
|
||||||
);
|
);
|
||||||
|
|
||||||
const { transactionPoints, orders } = await this.getTransactionPoints(
|
const { orders, transactionPoints } = await this.getTransactionPoints({
|
||||||
userId
|
userId
|
||||||
);
|
});
|
||||||
|
|
||||||
if (transactionPoints?.length <= 0) {
|
if (transactionPoints?.length <= 0) {
|
||||||
return {};
|
return {};
|
||||||
@ -198,8 +183,16 @@ export class PortfolioService {
|
|||||||
throw new Error('Missing information');
|
throw new Error('Missing information');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cashDetails = await this.accountService.getCashDetails(
|
||||||
|
userId,
|
||||||
|
userCurrency
|
||||||
|
);
|
||||||
|
|
||||||
const result: { [symbol: string]: PortfolioPosition } = {};
|
const result: { [symbol: string]: PortfolioPosition } = {};
|
||||||
const totalValue = currentPositions.currentValue;
|
const totalInvestment = currentPositions.totalInvestment.plus(
|
||||||
|
cashDetails.balance
|
||||||
|
);
|
||||||
|
const totalValue = currentPositions.currentValue.plus(cashDetails.balance);
|
||||||
|
|
||||||
const symbols = currentPositions.positions.map(
|
const symbols = currentPositions.positions.map(
|
||||||
(position) => position.symbol
|
(position) => position.symbol
|
||||||
@ -228,9 +221,8 @@ export class PortfolioService {
|
|||||||
result[item.symbol] = {
|
result[item.symbol] = {
|
||||||
accounts,
|
accounts,
|
||||||
allocationCurrent: value.div(totalValue).toNumber(),
|
allocationCurrent: value.div(totalValue).toNumber(),
|
||||||
allocationInvestment: item.investment
|
allocationInvestment: item.investment.div(totalInvestment).toNumber(),
|
||||||
.div(currentPositions.totalInvestment)
|
assetClass: symbolProfile.assetClass,
|
||||||
.toNumber(),
|
|
||||||
countries: symbolProfile.countries,
|
countries: symbolProfile.countries,
|
||||||
currency: item.currency,
|
currency: item.currency,
|
||||||
exchange: dataProviderResponse.exchange,
|
exchange: dataProviderResponse.exchange,
|
||||||
@ -239,16 +231,22 @@ export class PortfolioService {
|
|||||||
investment: item.investment.toNumber(),
|
investment: item.investment.toNumber(),
|
||||||
marketPrice: item.marketPrice,
|
marketPrice: item.marketPrice,
|
||||||
marketState: dataProviderResponse.marketState,
|
marketState: dataProviderResponse.marketState,
|
||||||
name: item.name,
|
name: symbolProfile.name,
|
||||||
quantity: item.quantity.toNumber(),
|
quantity: item.quantity.toNumber(),
|
||||||
sectors: symbolProfile.sectors,
|
sectors: symbolProfile.sectors,
|
||||||
symbol: item.symbol,
|
symbol: item.symbol,
|
||||||
transactionCount: item.transactionCount,
|
transactionCount: item.transactionCount,
|
||||||
type: dataProviderResponse.type,
|
|
||||||
value: value.toNumber()
|
value: value.toNumber()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Add a cash position for each currency
|
||||||
|
result[ghostfolioCashSymbol] = await this.getCashPosition({
|
||||||
|
cashDetails,
|
||||||
|
investment: totalInvestment,
|
||||||
|
value: totalValue
|
||||||
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -258,14 +256,11 @@ export class PortfolioService {
|
|||||||
): Promise<PortfolioPositionDetail> {
|
): Promise<PortfolioPositionDetail> {
|
||||||
const userId = await this.getUserId(aImpersonationId);
|
const userId = await this.getUserId(aImpersonationId);
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const orders = (await this.orderService.getOrders({ userId })).filter(
|
||||||
this.currentRateService,
|
(order) => order.symbol === aSymbol
|
||||||
this.request.user.Settings.currency
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const { transactionPoints } = await this.getTransactionPoints(userId);
|
if (orders.length <= 0) {
|
||||||
|
|
||||||
if (transactionPoints?.length <= 0) {
|
|
||||||
return {
|
return {
|
||||||
averagePrice: undefined,
|
averagePrice: undefined,
|
||||||
currency: undefined,
|
currency: undefined,
|
||||||
@ -283,7 +278,24 @@ export class PortfolioService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
portfolioCalculator.setTransactionPoints(transactionPoints);
|
const positionCurrency = orders[0].currency;
|
||||||
|
|
||||||
|
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
|
||||||
|
currency: order.currency,
|
||||||
|
date: format(order.date, DATE_FORMAT),
|
||||||
|
name: order.SymbolProfile?.name,
|
||||||
|
quantity: new Big(order.quantity),
|
||||||
|
symbol: order.symbol,
|
||||||
|
type: <OrderType>order.type,
|
||||||
|
unitPrice: new Big(order.unitPrice)
|
||||||
|
}));
|
||||||
|
|
||||||
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
|
this.currentRateService,
|
||||||
|
positionCurrency
|
||||||
|
);
|
||||||
|
portfolioCalculator.computeTransactionPoints(portfolioOrders);
|
||||||
|
const transactionPoints = portfolioCalculator.getTransactionPoints();
|
||||||
|
|
||||||
const portfolioStart = parseDate(transactionPoints[0].date);
|
const portfolioStart = parseDate(transactionPoints[0].date);
|
||||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||||
@ -299,12 +311,24 @@ export class PortfolioService {
|
|||||||
averagePrice,
|
averagePrice,
|
||||||
currency,
|
currency,
|
||||||
firstBuyDate,
|
firstBuyDate,
|
||||||
investment,
|
|
||||||
marketPrice,
|
marketPrice,
|
||||||
quantity,
|
quantity,
|
||||||
transactionCount
|
transactionCount
|
||||||
} = position;
|
} = position;
|
||||||
|
|
||||||
|
// Convert investment and gross performance to currency of user
|
||||||
|
const userCurrency = this.request.user.Settings.currency;
|
||||||
|
const investment = this.exchangeRateDataService.toCurrency(
|
||||||
|
position.investment.toNumber(),
|
||||||
|
currency,
|
||||||
|
userCurrency
|
||||||
|
);
|
||||||
|
const grossPerformance = this.exchangeRateDataService.toCurrency(
|
||||||
|
position.grossPerformance.toNumber(),
|
||||||
|
currency,
|
||||||
|
userCurrency
|
||||||
|
);
|
||||||
|
|
||||||
const historicalData = await this.dataProviderService.getHistorical(
|
const historicalData = await this.dataProviderService.getHistorical(
|
||||||
[aSymbol],
|
[aSymbol],
|
||||||
'day',
|
'day',
|
||||||
@ -313,8 +337,17 @@ export class PortfolioService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const historicalDataArray: HistoricalDataItem[] = [];
|
const historicalDataArray: HistoricalDataItem[] = [];
|
||||||
let maxPrice = marketPrice;
|
let maxPrice = orders[0].unitPrice;
|
||||||
let minPrice = marketPrice;
|
let minPrice = orders[0].unitPrice;
|
||||||
|
|
||||||
|
if (!historicalData[aSymbol][firstBuyDate]) {
|
||||||
|
// Add historical entry for buy date, if no historical data available
|
||||||
|
historicalDataArray.push({
|
||||||
|
averagePrice: orders[0].unitPrice,
|
||||||
|
date: firstBuyDate,
|
||||||
|
value: orders[0].unitPrice
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (historicalData[aSymbol]) {
|
if (historicalData[aSymbol]) {
|
||||||
let j = -1;
|
let j = -1;
|
||||||
@ -332,9 +365,9 @@ export class PortfolioService {
|
|||||||
(item) => item.symbol === aSymbol
|
(item) => item.symbol === aSymbol
|
||||||
);
|
);
|
||||||
if (currentSymbol) {
|
if (currentSymbol) {
|
||||||
currentAveragePrice = currentSymbol.investment
|
currentAveragePrice = currentSymbol.quantity.eq(0)
|
||||||
.div(currentSymbol.quantity)
|
? 0
|
||||||
.toNumber();
|
: currentSymbol.investment.div(currentSymbol.quantity).toNumber();
|
||||||
}
|
}
|
||||||
|
|
||||||
historicalDataArray.push({
|
historicalDataArray.push({
|
||||||
@ -351,20 +384,21 @@ export class PortfolioService {
|
|||||||
return {
|
return {
|
||||||
currency,
|
currency,
|
||||||
firstBuyDate,
|
firstBuyDate,
|
||||||
|
grossPerformance,
|
||||||
|
investment,
|
||||||
marketPrice,
|
marketPrice,
|
||||||
maxPrice,
|
maxPrice,
|
||||||
minPrice,
|
minPrice,
|
||||||
transactionCount,
|
transactionCount,
|
||||||
averagePrice: averagePrice.toNumber(),
|
averagePrice: averagePrice.toNumber(),
|
||||||
grossPerformance: position.grossPerformance.toNumber(),
|
|
||||||
grossPerformancePercent: position.grossPerformancePercentage.toNumber(),
|
grossPerformancePercent: position.grossPerformancePercentage.toNumber(),
|
||||||
historicalData: historicalDataArray,
|
historicalData: historicalDataArray,
|
||||||
investment: investment.toNumber(),
|
|
||||||
quantity: quantity.toNumber(),
|
quantity: quantity.toNumber(),
|
||||||
symbol: aSymbol
|
symbol: aSymbol
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const currentData = await this.dataProviderService.get([aSymbol]);
|
const currentData = await this.dataProviderService.get([aSymbol]);
|
||||||
|
const marketPrice = currentData[aSymbol]?.marketPrice;
|
||||||
|
|
||||||
let historicalData = await this.dataProviderService.getHistorical(
|
let historicalData = await this.dataProviderService.getHistorical(
|
||||||
[aSymbol],
|
[aSymbol],
|
||||||
@ -382,28 +416,33 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const historicalDataArray: HistoricalDataItem[] = [];
|
const historicalDataArray: HistoricalDataItem[] = [];
|
||||||
|
let maxPrice = marketPrice;
|
||||||
|
let minPrice = marketPrice;
|
||||||
|
|
||||||
for (const [date, { marketPrice }] of Object.entries(
|
for (const [date, { marketPrice }] of Object.entries(
|
||||||
historicalData[aSymbol]
|
historicalData[aSymbol]
|
||||||
).reverse()) {
|
)) {
|
||||||
historicalDataArray.push({
|
historicalDataArray.push({
|
||||||
date,
|
date,
|
||||||
value: marketPrice
|
value: marketPrice
|
||||||
});
|
});
|
||||||
|
|
||||||
|
maxPrice = Math.max(marketPrice ?? 0, maxPrice);
|
||||||
|
minPrice = Math.min(marketPrice ?? Number.MAX_SAFE_INTEGER, minPrice);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
averagePrice: undefined,
|
marketPrice,
|
||||||
|
maxPrice,
|
||||||
|
minPrice,
|
||||||
|
averagePrice: 0,
|
||||||
currency: currentData[aSymbol]?.currency,
|
currency: currentData[aSymbol]?.currency,
|
||||||
firstBuyDate: undefined,
|
firstBuyDate: undefined,
|
||||||
grossPerformance: undefined,
|
grossPerformance: undefined,
|
||||||
grossPerformancePercent: undefined,
|
grossPerformancePercent: undefined,
|
||||||
historicalData: historicalDataArray,
|
historicalData: historicalDataArray,
|
||||||
investment: undefined,
|
investment: 0,
|
||||||
marketPrice: currentData[aSymbol]?.marketPrice,
|
quantity: 0,
|
||||||
maxPrice: undefined,
|
|
||||||
minPrice: undefined,
|
|
||||||
quantity: undefined,
|
|
||||||
symbol: aSymbol,
|
symbol: aSymbol,
|
||||||
transactionCount: undefined
|
transactionCount: undefined
|
||||||
};
|
};
|
||||||
@ -421,7 +460,7 @@ export class PortfolioService {
|
|||||||
this.request.user.Settings.currency
|
this.request.user.Settings.currency
|
||||||
);
|
);
|
||||||
|
|
||||||
const { transactionPoints } = await this.getTransactionPoints(userId);
|
const { transactionPoints } = await this.getTransactionPoints({ userId });
|
||||||
|
|
||||||
if (transactionPoints?.length <= 0) {
|
if (transactionPoints?.length <= 0) {
|
||||||
return {
|
return {
|
||||||
@ -438,17 +477,34 @@ export class PortfolioService {
|
|||||||
startDate
|
startDate
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const positions = currentPositions.positions.filter(
|
||||||
|
(item) => !item.quantity.eq(0)
|
||||||
|
);
|
||||||
|
const symbols = positions.map((position) => position.symbol);
|
||||||
|
|
||||||
|
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
||||||
|
this.dataProviderService.get(symbols),
|
||||||
|
this.symbolProfileService.getSymbolProfiles(symbols)
|
||||||
|
]);
|
||||||
|
|
||||||
|
const symbolProfileMap: { [symbol: string]: EnhancedSymbolProfile } = {};
|
||||||
|
for (const symbolProfile of symbolProfiles) {
|
||||||
|
symbolProfileMap[symbolProfile.symbol] = symbolProfile;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hasErrors: currentPositions.hasErrors,
|
hasErrors: currentPositions.hasErrors,
|
||||||
positions: currentPositions.positions.map((position) => {
|
positions: positions.map((position) => {
|
||||||
return {
|
return {
|
||||||
...position,
|
...position,
|
||||||
|
assetClass: symbolProfileMap[position.symbol].assetClass,
|
||||||
averagePrice: new Big(position.averagePrice).toNumber(),
|
averagePrice: new Big(position.averagePrice).toNumber(),
|
||||||
grossPerformance: position.grossPerformance?.toNumber() ?? null,
|
grossPerformance: position.grossPerformance?.toNumber() ?? null,
|
||||||
grossPerformancePercentage:
|
grossPerformancePercentage:
|
||||||
position.grossPerformancePercentage?.toNumber() ?? null,
|
position.grossPerformancePercentage?.toNumber() ?? null,
|
||||||
investment: new Big(position.investment).toNumber(),
|
investment: new Big(position.investment).toNumber(),
|
||||||
name: position.name,
|
marketState: dataProviderResponses[position.symbol].marketState,
|
||||||
|
name: symbolProfileMap[position.symbol].name,
|
||||||
quantity: new Big(position.quantity).toNumber()
|
quantity: new Big(position.quantity).toNumber()
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@ -466,7 +522,7 @@ export class PortfolioService {
|
|||||||
this.request.user.Settings.currency
|
this.request.user.Settings.currency
|
||||||
);
|
);
|
||||||
|
|
||||||
const { transactionPoints } = await this.getTransactionPoints(userId);
|
const { transactionPoints } = await this.getTransactionPoints({ userId });
|
||||||
|
|
||||||
if (transactionPoints?.length <= 0) {
|
if (transactionPoints?.length <= 0) {
|
||||||
return {
|
return {
|
||||||
@ -528,9 +584,9 @@ export class PortfolioService {
|
|||||||
const userId = await this.getUserId(impersonationId);
|
const userId = await this.getUserId(impersonationId);
|
||||||
const baseCurrency = this.request.user.Settings.currency;
|
const baseCurrency = this.request.user.Settings.currency;
|
||||||
|
|
||||||
const { transactionPoints, orders } = await this.getTransactionPoints(
|
const { orders, transactionPoints } = await this.getTransactionPoints({
|
||||||
userId
|
userId
|
||||||
);
|
});
|
||||||
|
|
||||||
if (isEmpty(orders)) {
|
if (isEmpty(orders)) {
|
||||||
return {
|
return {
|
||||||
@ -608,6 +664,82 @@ export class PortfolioService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getSummary(aImpersonationId: string): Promise<PortfolioSummary> {
|
||||||
|
const currency = this.request.user.Settings.currency;
|
||||||
|
const userId = await this.getUserId(aImpersonationId);
|
||||||
|
|
||||||
|
const performanceInformation = await this.getPerformance(userId);
|
||||||
|
|
||||||
|
const { balance } = await this.accountService.getCashDetails(
|
||||||
|
userId,
|
||||||
|
currency
|
||||||
|
);
|
||||||
|
const orders = await this.orderService.getOrders({ userId });
|
||||||
|
const fees = this.getFees(orders);
|
||||||
|
const firstOrderDate = orders[0]?.date;
|
||||||
|
|
||||||
|
const totalBuy = this.getTotalByType(orders, currency, TypeOfOrder.BUY);
|
||||||
|
const totalSell = this.getTotalByType(orders, currency, TypeOfOrder.SELL);
|
||||||
|
|
||||||
|
const committedFunds = new Big(totalBuy).sub(totalSell);
|
||||||
|
|
||||||
|
const netWorth = new Big(balance)
|
||||||
|
.plus(performanceInformation.performance.currentValue)
|
||||||
|
.toNumber();
|
||||||
|
|
||||||
|
return {
|
||||||
|
...performanceInformation.performance,
|
||||||
|
fees,
|
||||||
|
firstOrderDate,
|
||||||
|
netWorth,
|
||||||
|
cash: balance,
|
||||||
|
committedFunds: committedFunds.toNumber(),
|
||||||
|
ordersCount: orders.length,
|
||||||
|
totalBuy: totalBuy,
|
||||||
|
totalSell: totalSell
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getCashPosition({
|
||||||
|
cashDetails,
|
||||||
|
investment,
|
||||||
|
value
|
||||||
|
}: {
|
||||||
|
cashDetails: CashDetails;
|
||||||
|
investment: Big;
|
||||||
|
value: Big;
|
||||||
|
}) {
|
||||||
|
const accounts = {};
|
||||||
|
const cashValue = new Big(cashDetails.balance);
|
||||||
|
|
||||||
|
cashDetails.accounts.forEach((account) => {
|
||||||
|
accounts[account.name] = {
|
||||||
|
current: account.balance,
|
||||||
|
original: account.balance
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
accounts,
|
||||||
|
allocationCurrent: cashValue.div(value).toNumber(),
|
||||||
|
allocationInvestment: cashValue.div(investment).toNumber(),
|
||||||
|
assetClass: AssetClass.CASH,
|
||||||
|
countries: [],
|
||||||
|
currency: Currency.CHF,
|
||||||
|
grossPerformance: 0,
|
||||||
|
grossPerformancePercent: 0,
|
||||||
|
investment: cashValue.toNumber(),
|
||||||
|
marketPrice: 0,
|
||||||
|
marketState: MarketState.open,
|
||||||
|
name: 'Cash',
|
||||||
|
quantity: 0,
|
||||||
|
sectors: [],
|
||||||
|
symbol: ghostfolioCashSymbol,
|
||||||
|
transactionCount: 0,
|
||||||
|
value: cashValue.toNumber()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private getStartDate(aDateRange: DateRange, portfolioStart: Date) {
|
private getStartDate(aDateRange: DateRange, portfolioStart: Date) {
|
||||||
switch (aDateRange) {
|
switch (aDateRange) {
|
||||||
case '1d':
|
case '1d':
|
||||||
@ -626,11 +758,17 @@ export class PortfolioService {
|
|||||||
return portfolioStart;
|
return portfolioStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getTransactionPoints(userId: string): Promise<{
|
private async getTransactionPoints({
|
||||||
|
includeDrafts = false,
|
||||||
|
userId
|
||||||
|
}: {
|
||||||
|
includeDrafts?: boolean;
|
||||||
|
userId: string;
|
||||||
|
}): Promise<{
|
||||||
transactionPoints: TransactionPoint[];
|
transactionPoints: TransactionPoint[];
|
||||||
orders: OrderWithAccount[];
|
orders: OrderWithAccount[];
|
||||||
}> {
|
}> {
|
||||||
const orders = await this.getOrders(userId);
|
const orders = await this.orderService.getOrders({ includeDrafts, userId });
|
||||||
|
|
||||||
if (orders.length <= 0) {
|
if (orders.length <= 0) {
|
||||||
return { transactionPoints: [], orders: [] };
|
return { transactionPoints: [], orders: [] };
|
||||||
@ -702,19 +840,6 @@ export class PortfolioService {
|
|||||||
return accounts;
|
return accounts;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getOrders(aUserId: string) {
|
|
||||||
return this.orderService.orders({
|
|
||||||
include: {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
||||||
Account: true,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
||||||
SymbolProfile: true
|
|
||||||
},
|
|
||||||
orderBy: { date: 'asc' },
|
|
||||||
where: { userId: aUserId }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getUserId(aImpersonationId: string) {
|
private async getUserId(aImpersonationId: string) {
|
||||||
const impersonationUserId =
|
const impersonationUserId =
|
||||||
await this.impersonationService.validateImpersonationId(
|
await this.impersonationService.validateImpersonationId(
|
||||||
|
@ -10,7 +10,7 @@ export class SubscriptionService {
|
|||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly configurationService: ConfigurationService,
|
private readonly configurationService: ConfigurationService,
|
||||||
private prisma: PrismaService
|
private readonly prismaService: PrismaService
|
||||||
) {
|
) {
|
||||||
this.stripe = new Stripe(
|
this.stripe = new Stripe(
|
||||||
this.configurationService.get('STRIPE_SECRET_KEY'),
|
this.configurationService.get('STRIPE_SECRET_KEY'),
|
||||||
@ -68,7 +68,7 @@ export class SubscriptionService {
|
|||||||
aCheckoutSessionId
|
aCheckoutSessionId
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.prisma.subscription.create({
|
await this.prismaService.subscription.create({
|
||||||
data: {
|
data: {
|
||||||
expiresAt: addDays(new Date(), 365),
|
expiresAt: addDays(new Date(), 365),
|
||||||
User: {
|
User: {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
||||||
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { convertFromYahooSymbol } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Currency, DataSource } from '@prisma/client';
|
import { Currency, DataSource } from '@prisma/client';
|
||||||
|
|
||||||
@ -11,7 +10,7 @@ import { SymbolItem } from './interfaces/symbol-item.interface';
|
|||||||
export class SymbolService {
|
export class SymbolService {
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly dataProviderService: DataProviderService,
|
private readonly dataProviderService: DataProviderService,
|
||||||
private readonly ghostfolioScraperApiService: GhostfolioScraperApiService
|
private readonly prismaService: PrismaService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async get(aSymbol: string): Promise<SymbolItem> {
|
public async get(aSymbol: string): Promise<SymbolItem> {
|
||||||
@ -37,17 +36,29 @@ export class SymbolService {
|
|||||||
results.items = items;
|
results.items = items;
|
||||||
|
|
||||||
// Add custom symbols
|
// Add custom symbols
|
||||||
const scraperConfigurations = await this.ghostfolioScraperApiService.getScraperConfigurations();
|
const ghostfolioSymbolProfiles =
|
||||||
scraperConfigurations.forEach((scraperConfiguration) => {
|
await this.prismaService.symbolProfile.findMany({
|
||||||
if (scraperConfiguration.name.toLowerCase().startsWith(aQuery)) {
|
select: {
|
||||||
results.items.push({
|
dataSource: true,
|
||||||
|
name: true,
|
||||||
|
symbol: true
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
AND: [
|
||||||
|
{
|
||||||
dataSource: DataSource.GHOSTFOLIO,
|
dataSource: DataSource.GHOSTFOLIO,
|
||||||
name: scraperConfiguration.name,
|
name: {
|
||||||
symbol: scraperConfiguration.symbol
|
startsWith: aQuery
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const ghostfolioSymbolProfile of ghostfolioSymbolProfiles) {
|
||||||
|
results.items.push(ghostfolioSymbolProfile);
|
||||||
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
@ -18,7 +18,7 @@ export class UserService {
|
|||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly configurationService: ConfigurationService,
|
private readonly configurationService: ConfigurationService,
|
||||||
private prisma: PrismaService
|
private readonly prismaService: PrismaService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async getUser({
|
public async getUser({
|
||||||
@ -29,7 +29,7 @@ export class UserService {
|
|||||||
Settings,
|
Settings,
|
||||||
subscription
|
subscription
|
||||||
}: UserWithSettings): Promise<IUser> {
|
}: UserWithSettings): Promise<IUser> {
|
||||||
const access = await this.prisma.access.findMany({
|
const access = await this.prismaService.access.findMany({
|
||||||
include: {
|
include: {
|
||||||
User: true
|
User: true
|
||||||
},
|
},
|
||||||
@ -60,7 +60,7 @@ export class UserService {
|
|||||||
public async user(
|
public async user(
|
||||||
userWhereUniqueInput: Prisma.UserWhereUniqueInput
|
userWhereUniqueInput: Prisma.UserWhereUniqueInput
|
||||||
): Promise<UserWithSettings | null> {
|
): Promise<UserWithSettings | null> {
|
||||||
const userFromDatabase = await this.prisma.user.findUnique({
|
const userFromDatabase = await this.prismaService.user.findUnique({
|
||||||
include: { Account: true, Settings: true, Subscription: true },
|
include: { Account: true, Settings: true, Subscription: true },
|
||||||
where: userWhereUniqueInput
|
where: userWhereUniqueInput
|
||||||
});
|
});
|
||||||
@ -129,7 +129,7 @@ export class UserService {
|
|||||||
orderBy?: Prisma.UserOrderByInput;
|
orderBy?: Prisma.UserOrderByInput;
|
||||||
}): Promise<User[]> {
|
}): Promise<User[]> {
|
||||||
const { skip, take, cursor, where, orderBy } = params;
|
const { skip, take, cursor, where, orderBy } = params;
|
||||||
return this.prisma.user.findMany({
|
return this.prismaService.user.findMany({
|
||||||
skip,
|
skip,
|
||||||
take,
|
take,
|
||||||
cursor,
|
cursor,
|
||||||
@ -146,7 +146,7 @@ export class UserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async createUser(data?: Prisma.UserCreateInput): Promise<User> {
|
public async createUser(data?: Prisma.UserCreateInput): Promise<User> {
|
||||||
let user = await this.prisma.user.create({
|
let user = await this.prismaService.user.create({
|
||||||
data: {
|
data: {
|
||||||
...data,
|
...data,
|
||||||
Account: {
|
Account: {
|
||||||
@ -169,7 +169,7 @@ export class UserService {
|
|||||||
process.env.ACCESS_TOKEN_SALT
|
process.env.ACCESS_TOKEN_SALT
|
||||||
);
|
);
|
||||||
|
|
||||||
user = await this.prisma.user.update({
|
user = await this.prismaService.user.update({
|
||||||
data: { accessToken: hashedAccessToken },
|
data: { accessToken: hashedAccessToken },
|
||||||
where: { id: user.id }
|
where: { id: user.id }
|
||||||
});
|
});
|
||||||
@ -185,36 +185,36 @@ export class UserService {
|
|||||||
data: Prisma.UserUpdateInput;
|
data: Prisma.UserUpdateInput;
|
||||||
}): Promise<User> {
|
}): Promise<User> {
|
||||||
const { where, data } = params;
|
const { where, data } = params;
|
||||||
return this.prisma.user.update({
|
return this.prismaService.user.update({
|
||||||
data,
|
data,
|
||||||
where
|
where
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteUser(where: Prisma.UserWhereUniqueInput): Promise<User> {
|
public async deleteUser(where: Prisma.UserWhereUniqueInput): Promise<User> {
|
||||||
await this.prisma.access.deleteMany({
|
await this.prismaService.access.deleteMany({
|
||||||
where: { OR: [{ granteeUserId: where.id }, { userId: where.id }] }
|
where: { OR: [{ granteeUserId: where.id }, { userId: where.id }] }
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.prisma.account.deleteMany({
|
await this.prismaService.account.deleteMany({
|
||||||
where: { userId: where.id }
|
where: { userId: where.id }
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.prisma.analytics.delete({
|
await this.prismaService.analytics.delete({
|
||||||
where: { userId: where.id }
|
where: { userId: where.id }
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.prisma.order.deleteMany({
|
await this.prismaService.order.deleteMany({
|
||||||
where: { userId: where.id }
|
where: { userId: where.id }
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.prisma.settings.delete({
|
await this.prismaService.settings.delete({
|
||||||
where: { userId: where.id }
|
where: { userId: where.id }
|
||||||
});
|
});
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
return this.prisma.user.delete({
|
return this.prismaService.user.delete({
|
||||||
where
|
where
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -224,7 +224,7 @@ export class UserService {
|
|||||||
userId,
|
userId,
|
||||||
viewMode
|
viewMode
|
||||||
}: UserSettingsParams) {
|
}: UserSettingsParams) {
|
||||||
await this.prisma.settings.upsert({
|
await this.prismaService.settings.upsert({
|
||||||
create: {
|
create: {
|
||||||
currency,
|
currency,
|
||||||
User: {
|
User: {
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Account, Currency, SymbolProfile } from '@prisma/client';
|
import { Account, Currency, SymbolProfile } from '@prisma/client';
|
||||||
import { endOfToday, isAfter, parseISO } from 'date-fns';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import { IOrder } from '../services/interfaces/interfaces';
|
import { IOrder } from '../services/interfaces/interfaces';
|
||||||
@ -11,6 +10,7 @@ export class Order {
|
|||||||
private fee: number;
|
private fee: number;
|
||||||
private date: string;
|
private date: string;
|
||||||
private id: string;
|
private id: string;
|
||||||
|
private isDraft: boolean;
|
||||||
private quantity: number;
|
private quantity: number;
|
||||||
private symbol: string;
|
private symbol: string;
|
||||||
private symbolProfile: SymbolProfile;
|
private symbolProfile: SymbolProfile;
|
||||||
@ -24,6 +24,7 @@ export class Order {
|
|||||||
this.fee = data.fee;
|
this.fee = data.fee;
|
||||||
this.date = data.date;
|
this.date = data.date;
|
||||||
this.id = data.id || uuidv4();
|
this.id = data.id || uuidv4();
|
||||||
|
this.isDraft = data.isDraft;
|
||||||
this.quantity = data.quantity;
|
this.quantity = data.quantity;
|
||||||
this.symbol = data.symbol;
|
this.symbol = data.symbol;
|
||||||
this.symbolProfile = data.symbolProfile;
|
this.symbolProfile = data.symbolProfile;
|
||||||
@ -54,7 +55,7 @@ export class Order {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getIsDraft() {
|
public getIsDraft() {
|
||||||
return isAfter(parseISO(this.date), endOfToday());
|
return this.isDraft;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getQuantity() {
|
public getQuantity() {
|
||||||
|
@ -3,7 +3,6 @@ import {
|
|||||||
DATE_FORMAT,
|
DATE_FORMAT,
|
||||||
getUtc,
|
getUtc,
|
||||||
isGhostfolioScraperApiSymbol,
|
isGhostfolioScraperApiSymbol,
|
||||||
isRakutenRapidApiSymbol,
|
|
||||||
resetHours
|
resetHours
|
||||||
} from '@ghostfolio/common/helper';
|
} from '@ghostfolio/common/helper';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
@ -31,7 +30,7 @@ export class DataGatheringService {
|
|||||||
private readonly configurationService: ConfigurationService,
|
private readonly configurationService: ConfigurationService,
|
||||||
private readonly dataProviderService: DataProviderService,
|
private readonly dataProviderService: DataProviderService,
|
||||||
private readonly ghostfolioScraperApi: GhostfolioScraperApiService,
|
private readonly ghostfolioScraperApi: GhostfolioScraperApiService,
|
||||||
private prisma: PrismaService
|
private readonly prismaService: PrismaService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async gather7Days() {
|
public async gather7Days() {
|
||||||
@ -41,7 +40,7 @@ export class DataGatheringService {
|
|||||||
console.log('7d data gathering has been started.');
|
console.log('7d data gathering has been started.');
|
||||||
console.time('7d-data-gathering');
|
console.time('7d-data-gathering');
|
||||||
|
|
||||||
await this.prisma.property.create({
|
await this.prismaService.property.create({
|
||||||
data: {
|
data: {
|
||||||
key: 'LOCKED_DATA_GATHERING',
|
key: 'LOCKED_DATA_GATHERING',
|
||||||
value: new Date().toISOString()
|
value: new Date().toISOString()
|
||||||
@ -53,7 +52,7 @@ export class DataGatheringService {
|
|||||||
try {
|
try {
|
||||||
await this.gatherSymbols(symbols);
|
await this.gatherSymbols(symbols);
|
||||||
|
|
||||||
await this.prisma.property.upsert({
|
await this.prismaService.property.upsert({
|
||||||
create: {
|
create: {
|
||||||
key: 'LAST_DATA_GATHERING',
|
key: 'LAST_DATA_GATHERING',
|
||||||
value: new Date().toISOString()
|
value: new Date().toISOString()
|
||||||
@ -65,7 +64,7 @@ export class DataGatheringService {
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.prisma.property.delete({
|
await this.prismaService.property.delete({
|
||||||
where: {
|
where: {
|
||||||
key: 'LOCKED_DATA_GATHERING'
|
key: 'LOCKED_DATA_GATHERING'
|
||||||
}
|
}
|
||||||
@ -77,7 +76,7 @@ export class DataGatheringService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async gatherMax() {
|
public async gatherMax() {
|
||||||
const isDataGatheringLocked = await this.prisma.property.findUnique({
|
const isDataGatheringLocked = await this.prismaService.property.findUnique({
|
||||||
where: { key: 'LOCKED_DATA_GATHERING' }
|
where: { key: 'LOCKED_DATA_GATHERING' }
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -85,7 +84,7 @@ export class DataGatheringService {
|
|||||||
console.log('Max data gathering has been started.');
|
console.log('Max data gathering has been started.');
|
||||||
console.time('max-data-gathering');
|
console.time('max-data-gathering');
|
||||||
|
|
||||||
await this.prisma.property.create({
|
await this.prismaService.property.create({
|
||||||
data: {
|
data: {
|
||||||
key: 'LOCKED_DATA_GATHERING',
|
key: 'LOCKED_DATA_GATHERING',
|
||||||
value: new Date().toISOString()
|
value: new Date().toISOString()
|
||||||
@ -97,7 +96,7 @@ export class DataGatheringService {
|
|||||||
try {
|
try {
|
||||||
await this.gatherSymbols(symbols);
|
await this.gatherSymbols(symbols);
|
||||||
|
|
||||||
await this.prisma.property.upsert({
|
await this.prismaService.property.upsert({
|
||||||
create: {
|
create: {
|
||||||
key: 'LAST_DATA_GATHERING',
|
key: 'LAST_DATA_GATHERING',
|
||||||
value: new Date().toISOString()
|
value: new Date().toISOString()
|
||||||
@ -109,7 +108,7 @@ export class DataGatheringService {
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.prisma.property.delete({
|
await this.prismaService.property.delete({
|
||||||
where: {
|
where: {
|
||||||
key: 'LOCKED_DATA_GATHERING'
|
key: 'LOCKED_DATA_GATHERING'
|
||||||
}
|
}
|
||||||
@ -135,18 +134,21 @@ export class DataGatheringService {
|
|||||||
|
|
||||||
const currentData = await this.dataProviderService.get(symbols);
|
const currentData = await this.dataProviderService.get(symbols);
|
||||||
|
|
||||||
for (const [symbol, { currency, dataSource, name }] of Object.entries(
|
for (const [
|
||||||
currentData
|
symbol,
|
||||||
)) {
|
{ assetClass, currency, dataSource, name }
|
||||||
|
] of Object.entries(currentData)) {
|
||||||
try {
|
try {
|
||||||
await this.prisma.symbolProfile.upsert({
|
await this.prismaService.symbolProfile.upsert({
|
||||||
create: {
|
create: {
|
||||||
|
assetClass,
|
||||||
currency,
|
currency,
|
||||||
dataSource,
|
dataSource,
|
||||||
name,
|
name,
|
||||||
symbol
|
symbol
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
|
assetClass,
|
||||||
currency,
|
currency,
|
||||||
name
|
name
|
||||||
},
|
},
|
||||||
@ -203,7 +205,7 @@ export class DataGatheringService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.prisma.marketData.create({
|
await this.prismaService.marketData.create({
|
||||||
data: {
|
data: {
|
||||||
symbol,
|
symbol,
|
||||||
date: currentDate,
|
date: currentDate,
|
||||||
@ -248,6 +250,34 @@ export class DataGatheringService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getIsInProgress() {
|
||||||
|
return await this.prismaService.property.findUnique({
|
||||||
|
where: { key: 'LOCKED_DATA_GATHERING' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getLastDataGathering() {
|
||||||
|
const lastDataGathering = await this.prismaService.property.findUnique({
|
||||||
|
where: { key: 'LAST_DATA_GATHERING' }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (lastDataGathering?.value) {
|
||||||
|
return new Date(lastDataGathering.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async reset() {
|
||||||
|
console.log('Data gathering has been reset.');
|
||||||
|
|
||||||
|
await this.prismaService.property.deleteMany({
|
||||||
|
where: {
|
||||||
|
OR: [{ key: 'LAST_DATA_GATHERING' }, { key: 'LOCKED_DATA_GATHERING' }]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private getBenchmarksToGather(startDate: Date): IDataGatheringItem[] {
|
private getBenchmarksToGather(startDate: Date): IDataGatheringItem[] {
|
||||||
const benchmarksToGather = benchmarks.map(({ dataSource, symbol }) => {
|
const benchmarksToGather = benchmarks.map(({ dataSource, symbol }) => {
|
||||||
return {
|
return {
|
||||||
@ -271,7 +301,7 @@ export class DataGatheringService {
|
|||||||
private async getSymbols7D(): Promise<IDataGatheringItem[]> {
|
private async getSymbols7D(): Promise<IDataGatheringItem[]> {
|
||||||
const startDate = subDays(resetHours(new Date()), 7);
|
const startDate = subDays(resetHours(new Date()), 7);
|
||||||
|
|
||||||
const distinctOrders = await this.prisma.order.findMany({
|
const distinctOrders = await this.prismaService.order.findMany({
|
||||||
distinct: ['symbol'],
|
distinct: ['symbol'],
|
||||||
orderBy: [{ symbol: 'asc' }],
|
orderBy: [{ symbol: 'asc' }],
|
||||||
select: { dataSource: true, symbol: true },
|
select: { dataSource: true, symbol: true },
|
||||||
@ -303,9 +333,8 @@ export class DataGatheringService {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const customSymbolsToGather = await this.getCustomSymbolsToGather(
|
const customSymbolsToGather =
|
||||||
startDate
|
await this.ghostfolioScraperApi.getCustomSymbolsToGather(startDate);
|
||||||
);
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...this.getBenchmarksToGather(startDate),
|
...this.getBenchmarksToGather(startDate),
|
||||||
@ -318,9 +347,8 @@ export class DataGatheringService {
|
|||||||
private async getSymbolsMax(): Promise<IDataGatheringItem[]> {
|
private async getSymbolsMax(): Promise<IDataGatheringItem[]> {
|
||||||
const startDate = new Date(getUtc('2015-01-01'));
|
const startDate = new Date(getUtc('2015-01-01'));
|
||||||
|
|
||||||
const customSymbolsToGather = await this.getCustomSymbolsToGather(
|
const customSymbolsToGather =
|
||||||
startDate
|
await this.ghostfolioScraperApi.getCustomSymbolsToGather(startDate);
|
||||||
);
|
|
||||||
|
|
||||||
const currencyPairsToGather = currencyPairs.map(
|
const currencyPairsToGather = currencyPairs.map(
|
||||||
({ dataSource, symbol }) => {
|
({ dataSource, symbol }) => {
|
||||||
@ -332,7 +360,7 @@ export class DataGatheringService {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const distinctOrders = await this.prisma.order.findMany({
|
const distinctOrders = await this.prismaService.order.findMany({
|
||||||
distinct: ['symbol'],
|
distinct: ['symbol'],
|
||||||
orderBy: [{ date: 'asc' }],
|
orderBy: [{ date: 'asc' }],
|
||||||
select: { dataSource: true, date: true, symbol: true },
|
select: { dataSource: true, date: true, symbol: true },
|
||||||
@ -354,7 +382,7 @@ export class DataGatheringService {
|
|||||||
private async getSymbolsProfileData(): Promise<IDataGatheringItem[]> {
|
private async getSymbolsProfileData(): Promise<IDataGatheringItem[]> {
|
||||||
const startDate = subDays(resetHours(new Date()), 7);
|
const startDate = subDays(resetHours(new Date()), 7);
|
||||||
|
|
||||||
const distinctOrders = await this.prisma.order.findMany({
|
const distinctOrders = await this.prismaService.order.findMany({
|
||||||
distinct: ['symbol'],
|
distinct: ['symbol'],
|
||||||
orderBy: [{ symbol: 'asc' }],
|
orderBy: [{ symbol: 'asc' }],
|
||||||
select: { dataSource: true, symbol: true }
|
select: { dataSource: true, symbol: true }
|
||||||
@ -371,18 +399,13 @@ export class DataGatheringService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async isDataGatheringNeeded() {
|
private async isDataGatheringNeeded() {
|
||||||
const lastDataGathering = await this.prisma.property.findUnique({
|
const lastDataGathering = await this.getLastDataGathering();
|
||||||
where: { key: 'LAST_DATA_GATHERING' }
|
|
||||||
});
|
|
||||||
|
|
||||||
const isDataGatheringLocked = await this.prisma.property.findUnique({
|
const isDataGatheringLocked = await this.prismaService.property.findUnique({
|
||||||
where: { key: 'LOCKED_DATA_GATHERING' }
|
where: { key: 'LOCKED_DATA_GATHERING' }
|
||||||
});
|
});
|
||||||
|
|
||||||
const diffInHours = differenceInHours(
|
const diffInHours = differenceInHours(new Date(), lastDataGathering);
|
||||||
new Date(),
|
|
||||||
new Date(lastDataGathering?.value)
|
|
||||||
);
|
|
||||||
|
|
||||||
return (diffInHours >= 1 || !lastDataGathering) && !isDataGatheringLocked;
|
return (diffInHours >= 1 || !lastDataGathering) && !isDataGatheringLocked;
|
||||||
}
|
}
|
||||||
|
@ -26,11 +26,11 @@ export class DataProviderService {
|
|||||||
private readonly alphaVantageService: AlphaVantageService,
|
private readonly alphaVantageService: AlphaVantageService,
|
||||||
private readonly configurationService: ConfigurationService,
|
private readonly configurationService: ConfigurationService,
|
||||||
private readonly ghostfolioScraperApiService: GhostfolioScraperApiService,
|
private readonly ghostfolioScraperApiService: GhostfolioScraperApiService,
|
||||||
private prisma: PrismaService,
|
private readonly prismaService: PrismaService,
|
||||||
private readonly rakutenRapidApiService: RakutenRapidApiService,
|
private readonly rakutenRapidApiService: RakutenRapidApiService,
|
||||||
private readonly yahooFinanceService: YahooFinanceService
|
private readonly yahooFinanceService: YahooFinanceService
|
||||||
) {
|
) {
|
||||||
this.rakutenRapidApiService?.setPrisma(this.prisma);
|
this.rakutenRapidApiService?.setPrisma(this.prismaService);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async get(
|
public async get(
|
||||||
@ -112,9 +112,8 @@ export class DataProviderService {
|
|||||||
`','`
|
`','`
|
||||||
)}') ${granularityQuery} ${rangeQuery} ORDER BY date;`;
|
)}') ${granularityQuery} ${rangeQuery} ORDER BY date;`;
|
||||||
|
|
||||||
const marketDataByGranularity: MarketData[] = await this.prisma.$queryRaw(
|
const marketDataByGranularity: MarketData[] =
|
||||||
queryRaw
|
await this.prismaService.$queryRaw(queryRaw);
|
||||||
);
|
|
||||||
|
|
||||||
response = marketDataByGranularity.reduce((r, marketData) => {
|
response = marketDataByGranularity.reduce((r, marketData) => {
|
||||||
const { date, marketPrice, symbol } = marketData;
|
const { date, marketPrice, symbol } = marketData;
|
||||||
|
@ -12,6 +12,7 @@ import { format } from 'date-fns';
|
|||||||
|
|
||||||
import { DataProviderInterface } from '../../interfaces/data-provider.interface';
|
import { DataProviderInterface } from '../../interfaces/data-provider.interface';
|
||||||
import {
|
import {
|
||||||
|
IDataGatheringItem,
|
||||||
IDataProviderHistoricalResponse,
|
IDataProviderHistoricalResponse,
|
||||||
IDataProviderResponse,
|
IDataProviderResponse,
|
||||||
MarketState
|
MarketState
|
||||||
@ -23,7 +24,7 @@ import { ScraperConfig } from './interfaces/scraper-config.interface';
|
|||||||
export class GhostfolioScraperApiService implements DataProviderInterface {
|
export class GhostfolioScraperApiService implements DataProviderInterface {
|
||||||
private static NUMERIC_REGEXP = /[-]{0,1}[\d]*[.,]{0,1}[\d]+/g;
|
private static NUMERIC_REGEXP = /[-]{0,1}[\d]*[.,]{0,1}[\d]+/g;
|
||||||
|
|
||||||
public constructor(private prisma: PrismaService) {}
|
public constructor(private readonly prismaService: PrismaService) {}
|
||||||
|
|
||||||
public canHandle(symbol: string) {
|
public canHandle(symbol: string) {
|
||||||
return isGhostfolioScraperApiSymbol(symbol);
|
return isGhostfolioScraperApiSymbol(symbol);
|
||||||
@ -41,7 +42,7 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
|||||||
|
|
||||||
const scraperConfig = await this.getScraperConfigurationBySymbol(symbol);
|
const scraperConfig = await this.getScraperConfigurationBySymbol(symbol);
|
||||||
|
|
||||||
const { marketPrice } = await this.prisma.marketData.findFirst({
|
const { marketPrice } = await this.prismaService.marketData.findFirst({
|
||||||
orderBy: {
|
orderBy: {
|
||||||
date: 'desc'
|
date: 'desc'
|
||||||
},
|
},
|
||||||
@ -55,8 +56,7 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
|||||||
marketPrice,
|
marketPrice,
|
||||||
currency: scraperConfig?.currency,
|
currency: scraperConfig?.currency,
|
||||||
dataSource: DataSource.GHOSTFOLIO,
|
dataSource: DataSource.GHOSTFOLIO,
|
||||||
marketState: MarketState.delayed,
|
marketState: MarketState.delayed
|
||||||
name: scraperConfig?.name
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -66,6 +66,25 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getCustomSymbolsToGather(
|
||||||
|
startDate?: Date
|
||||||
|
): Promise<IDataGatheringItem[]> {
|
||||||
|
const ghostfolioSymbolProfiles =
|
||||||
|
await this.prismaService.symbolProfile.findMany({
|
||||||
|
where: {
|
||||||
|
dataSource: DataSource.GHOSTFOLIO
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return ghostfolioSymbolProfiles.map(({ dataSource, symbol }) => {
|
||||||
|
return {
|
||||||
|
dataSource,
|
||||||
|
symbol,
|
||||||
|
date: startDate
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public async getHistorical(
|
public async getHistorical(
|
||||||
aSymbols: string[],
|
aSymbols: string[],
|
||||||
aGranularity: Granularity = 'day',
|
aGranularity: Granularity = 'day',
|
||||||
@ -111,7 +130,7 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
|||||||
public async getScraperConfigurations(): Promise<ScraperConfig[]> {
|
public async getScraperConfigurations(): Promise<ScraperConfig[]> {
|
||||||
try {
|
try {
|
||||||
const { value: scraperConfigString } =
|
const { value: scraperConfigString } =
|
||||||
await this.prisma.property.findFirst({
|
await this.prismaService.property.findFirst({
|
||||||
select: {
|
select: {
|
||||||
value: true
|
value: true
|
||||||
},
|
},
|
||||||
|
@ -2,7 +2,6 @@ import { Currency } from '@prisma/client';
|
|||||||
|
|
||||||
export interface ScraperConfig {
|
export interface ScraperConfig {
|
||||||
currency: Currency;
|
currency: Currency;
|
||||||
name: string;
|
|
||||||
selector: string;
|
selector: string;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
@ -23,7 +23,7 @@ import { PrismaService } from '../../prisma.service';
|
|||||||
export class RakutenRapidApiService implements DataProviderInterface {
|
export class RakutenRapidApiService implements DataProviderInterface {
|
||||||
public static FEAR_AND_GREED_INDEX_NAME = 'Fear & Greed Index';
|
public static FEAR_AND_GREED_INDEX_NAME = 'Fear & Greed Index';
|
||||||
|
|
||||||
private prisma: PrismaService;
|
private prismaService: PrismaService;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly configurationService: ConfigurationService
|
private readonly configurationService: ConfigurationService
|
||||||
@ -89,7 +89,7 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
|||||||
// TODO: can be removed after all data from the last year has been gathered
|
// TODO: can be removed after all data from the last year has been gathered
|
||||||
// (introduced on 27.03.2021)
|
// (introduced on 27.03.2021)
|
||||||
|
|
||||||
await this.prisma.marketData.create({
|
await this.prismaService.marketData.create({
|
||||||
data: {
|
data: {
|
||||||
symbol,
|
symbol,
|
||||||
date: subWeeks(getToday(), 1),
|
date: subWeeks(getToday(), 1),
|
||||||
@ -97,7 +97,7 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.prisma.marketData.create({
|
await this.prismaService.marketData.create({
|
||||||
data: {
|
data: {
|
||||||
symbol,
|
symbol,
|
||||||
date: subMonths(getToday(), 1),
|
date: subMonths(getToday(), 1),
|
||||||
@ -105,7 +105,7 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.prisma.marketData.create({
|
await this.prismaService.marketData.create({
|
||||||
data: {
|
data: {
|
||||||
symbol,
|
symbol,
|
||||||
date: subYears(getToday(), 1),
|
date: subYears(getToday(), 1),
|
||||||
@ -134,7 +134,7 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public setPrisma(aPrismaService: PrismaService) {
|
public setPrisma(aPrismaService: PrismaService) {
|
||||||
this.prisma = aPrismaService;
|
this.prismaService = aPrismaService;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getFearAndGreedIndex(): Promise<{
|
private async getFearAndGreedIndex(): Promise<{
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
} from '@ghostfolio/common/helper';
|
} from '@ghostfolio/common/helper';
|
||||||
import { Granularity } from '@ghostfolio/common/types';
|
import { Granularity } from '@ghostfolio/common/types';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { DataSource } from '@prisma/client';
|
import { AssetClass, Currency, DataSource } from '@prisma/client';
|
||||||
import * as bent from 'bent';
|
import * as bent from 'bent';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import * as yahooFinance from 'yahoo-finance';
|
import * as yahooFinance from 'yahoo-finance';
|
||||||
@ -17,8 +17,7 @@ import { DataProviderInterface } from '../../interfaces/data-provider.interface'
|
|||||||
import {
|
import {
|
||||||
IDataProviderHistoricalResponse,
|
IDataProviderHistoricalResponse,
|
||||||
IDataProviderResponse,
|
IDataProviderResponse,
|
||||||
MarketState,
|
MarketState
|
||||||
Type
|
|
||||||
} from '../../interfaces/interfaces';
|
} from '../../interfaces/interfaces';
|
||||||
import {
|
import {
|
||||||
IYahooFinanceHistoricalResponse,
|
IYahooFinanceHistoricalResponse,
|
||||||
@ -61,6 +60,7 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
const symbol = convertFromYahooSymbol(yahooSymbol);
|
const symbol = convertFromYahooSymbol(yahooSymbol);
|
||||||
|
|
||||||
response[symbol] = {
|
response[symbol] = {
|
||||||
|
assetClass: this.parseAssetClass(value.price?.quoteType),
|
||||||
currency: parseCurrency(value.price?.currency),
|
currency: parseCurrency(value.price?.currency),
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
exchange: this.parseExchange(value.price?.exchangeName),
|
exchange: this.parseExchange(value.price?.exchangeName),
|
||||||
@ -69,8 +69,7 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
? MarketState.open
|
? MarketState.open
|
||||||
: MarketState.closed,
|
: MarketState.closed,
|
||||||
marketPrice: value.price?.regularMarketPrice || 0,
|
marketPrice: value.price?.regularMarketPrice || 0,
|
||||||
name: value.price?.longName || value.price?.shortName || symbol,
|
name: value.price?.longName || value.price?.shortName || symbol
|
||||||
type: this.parseType(this.getType(symbol, value))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const url = value.summaryProfile?.website;
|
const url = value.summaryProfile?.website;
|
||||||
@ -148,8 +147,23 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
200
|
200
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await get();
|
const searchResult = await get();
|
||||||
items = result.quotes
|
|
||||||
|
const symbols: string[] = searchResult.quotes
|
||||||
|
.filter((quote) => {
|
||||||
|
// filter out undefined symbols
|
||||||
|
return quote.symbol;
|
||||||
|
})
|
||||||
|
.filter(({ quoteType }) => {
|
||||||
|
return quoteType === 'EQUITY' || quoteType === 'ETF';
|
||||||
|
})
|
||||||
|
.map(({ symbol }) => {
|
||||||
|
return symbol;
|
||||||
|
});
|
||||||
|
|
||||||
|
const marketData = await this.get(symbols);
|
||||||
|
|
||||||
|
items = searchResult.quotes
|
||||||
.filter((quote) => {
|
.filter((quote) => {
|
||||||
return quote.isYahooFinance;
|
return quote.isYahooFinance;
|
||||||
})
|
})
|
||||||
@ -163,7 +177,12 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
.filter(({ quoteType, symbol }) => {
|
.filter(({ quoteType, symbol }) => {
|
||||||
if (quoteType === 'CRYPTOCURRENCY') {
|
if (quoteType === 'CRYPTOCURRENCY') {
|
||||||
// Only allow cryptocurrencies in USD
|
// Only allow cryptocurrencies in USD
|
||||||
return symbol.includes('USD');
|
return symbol.includes(Currency.USD);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!marketData[symbol]?.currency) {
|
||||||
|
// Only allow symbols with supported currency
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -203,14 +222,20 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
return aSymbol;
|
return aSymbol;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getType(aSymbol: string, aValue: IYahooFinanceQuoteResponse): Type {
|
private parseAssetClass(aString: string): AssetClass {
|
||||||
if (isCrypto(aSymbol)) {
|
let assetClass: AssetClass;
|
||||||
return Type.Cryptocurrency;
|
|
||||||
} else if (aValue.price?.quoteType.toLowerCase() === 'equity') {
|
switch (aString?.toLowerCase()) {
|
||||||
return Type.Stock;
|
case 'cryptocurrency':
|
||||||
|
assetClass = AssetClass.CASH;
|
||||||
|
break;
|
||||||
|
case 'equity':
|
||||||
|
case 'etf':
|
||||||
|
assetClass = AssetClass.EQUITY;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return aValue.price?.quoteType.toLowerCase();
|
return assetClass;
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseExchange(aString: string): string {
|
private parseExchange(aString: string): string {
|
||||||
@ -220,18 +245,6 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
|
|
||||||
return aString;
|
return aString;
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseType(aString: string): Type {
|
|
||||||
if (aString?.toLowerCase() === 'cryptocurrency') {
|
|
||||||
return Type.Cryptocurrency;
|
|
||||||
} else if (aString?.toLowerCase() === 'etf') {
|
|
||||||
return Type.ETF;
|
|
||||||
} else if (aString?.toLowerCase() === 'stock') {
|
|
||||||
return Type.Stock;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Type.Unknown;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const convertFromYahooSymbol = (aSymbol: string) => {
|
export const convertFromYahooSymbol = (aSymbol: string) => {
|
||||||
|
@ -2,6 +2,7 @@ import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Currency } from '@prisma/client';
|
import { Currency } from '@prisma/client';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
|
import { isNumber } from 'lodash';
|
||||||
|
|
||||||
import { DataProviderService } from './data-provider.service';
|
import { DataProviderService } from './data-provider.service';
|
||||||
|
|
||||||
@ -83,9 +84,17 @@ export class ExchangeRateDataService {
|
|||||||
factor = this.currencies[`${aFromCurrency}${aToCurrency}`];
|
factor = this.currencies[`${aFromCurrency}${aToCurrency}`];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isNumber(factor)) {
|
||||||
return factor * aValue;
|
return factor * aValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback with error, if currencies are not available
|
||||||
|
console.error(
|
||||||
|
`No exchange rate has been found for ${aFromCurrency}${aToCurrency}`
|
||||||
|
);
|
||||||
|
return aValue;
|
||||||
|
}
|
||||||
|
|
||||||
private addPairs(aCurrency1: Currency, aCurrency2: Currency) {
|
private addPairs(aCurrency1: Currency, aCurrency2: Currency) {
|
||||||
this.pairs.push(`${aCurrency1}${aCurrency2}`);
|
this.pairs.push(`${aCurrency1}${aCurrency2}`);
|
||||||
this.pairs.push(`${aCurrency2}${aCurrency1}`);
|
this.pairs.push(`${aCurrency2}${aCurrency1}`);
|
||||||
|
@ -4,10 +4,10 @@ import { PrismaService } from './prisma.service';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ImpersonationService {
|
export class ImpersonationService {
|
||||||
public constructor(private prisma: PrismaService) {}
|
public constructor(private readonly prismaService: PrismaService) {}
|
||||||
|
|
||||||
public async validateImpersonationId(aId = '', aUserId: string) {
|
public async validateImpersonationId(aId = '', aUserId: string) {
|
||||||
const accessObject = await this.prisma.access.findFirst({
|
const accessObject = await this.prismaService.access.findFirst({
|
||||||
where: { GranteeUser: { id: aUserId }, id: aId }
|
where: { GranteeUser: { id: aUserId }, id: aId }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
import {
|
||||||
import { Account, Currency, DataSource, SymbolProfile } from '@prisma/client';
|
Account,
|
||||||
|
AssetClass,
|
||||||
|
Currency,
|
||||||
|
DataSource,
|
||||||
|
SymbolProfile
|
||||||
|
} from '@prisma/client';
|
||||||
|
|
||||||
import { OrderType } from '../../models/order-type';
|
import { OrderType } from '../../models/order-type';
|
||||||
|
|
||||||
@ -9,20 +14,13 @@ export const MarketState = {
|
|||||||
open: 'open'
|
open: 'open'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Type = {
|
|
||||||
Cash: 'Cash',
|
|
||||||
Cryptocurrency: 'Cryptocurrency',
|
|
||||||
ETF: 'ETF',
|
|
||||||
Stock: 'Stock',
|
|
||||||
Unknown: UNKNOWN_KEY
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface IOrder {
|
export interface IOrder {
|
||||||
account: Account;
|
account: Account;
|
||||||
currency: Currency;
|
currency: Currency;
|
||||||
date: string;
|
date: string;
|
||||||
fee: number;
|
fee: number;
|
||||||
id?: string;
|
id?: string;
|
||||||
|
isDraft: boolean;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
symbolProfile: SymbolProfile;
|
symbolProfile: SymbolProfile;
|
||||||
@ -36,6 +34,7 @@ export interface IDataProviderHistoricalResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IDataProviderResponse {
|
export interface IDataProviderResponse {
|
||||||
|
assetClass?: AssetClass;
|
||||||
currency: Currency;
|
currency: Currency;
|
||||||
dataSource: DataSource;
|
dataSource: DataSource;
|
||||||
exchange?: string;
|
exchange?: string;
|
||||||
@ -43,8 +42,7 @@ export interface IDataProviderResponse {
|
|||||||
marketChangePercent?: number;
|
marketChangePercent?: number;
|
||||||
marketPrice: number;
|
marketPrice: number;
|
||||||
marketState: MarketState;
|
marketState: MarketState;
|
||||||
name: string;
|
name?: string;
|
||||||
type?: Type;
|
|
||||||
url?: string;
|
url?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,5 +53,3 @@ export interface IDataGatheringItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type MarketState = typeof MarketState[keyof typeof MarketState];
|
export type MarketState = typeof MarketState[keyof typeof MarketState];
|
||||||
|
|
||||||
export type Type = typeof Type[keyof typeof Type];
|
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
||||||
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
||||||
import { Currency, DataSource } from '@prisma/client';
|
import { AssetClass, Currency, DataSource } from '@prisma/client';
|
||||||
|
|
||||||
export interface EnhancedSymbolProfile {
|
export interface EnhancedSymbolProfile {
|
||||||
|
assetClass: AssetClass;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
currency: Currency | null;
|
currency: Currency | null;
|
||||||
dataSource: DataSource;
|
dataSource: DataSource;
|
||||||
|
@ -9,12 +9,12 @@ import { continents, countries } from 'countries-list';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SymbolProfileService {
|
export class SymbolProfileService {
|
||||||
constructor(private prisma: PrismaService) {}
|
constructor(private readonly prismaService: PrismaService) {}
|
||||||
|
|
||||||
public async getSymbolProfiles(
|
public async getSymbolProfiles(
|
||||||
symbols: string[]
|
symbols: string[]
|
||||||
): Promise<EnhancedSymbolProfile[]> {
|
): Promise<EnhancedSymbolProfile[]> {
|
||||||
return this.prisma.symbolProfile
|
return this.prismaService.symbolProfile
|
||||||
.findMany({
|
.findMany({
|
||||||
where: {
|
where: {
|
||||||
symbol: {
|
symbol: {
|
||||||
|
@ -52,6 +52,41 @@ const routes: Routes = [
|
|||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./pages/home/home-page.module').then((m) => m.HomePageModule)
|
import('./pages/home/home-page.module').then((m) => m.HomePageModule)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'portfolio',
|
||||||
|
loadChildren: () =>
|
||||||
|
import('./pages/portfolio/portfolio-page.module').then(
|
||||||
|
(m) => m.PortfolioPageModule
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'portfolio/allocations',
|
||||||
|
loadChildren: () =>
|
||||||
|
import('./pages/portfolio/allocations/allocations-page.module').then(
|
||||||
|
(m) => m.AllocationsPageModule
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'portfolio/analysis',
|
||||||
|
loadChildren: () =>
|
||||||
|
import('./pages/portfolio/analysis/analysis-page.module').then(
|
||||||
|
(m) => m.AnalysisPageModule
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'portfolio/report',
|
||||||
|
loadChildren: () =>
|
||||||
|
import('./pages/portfolio/report/report-page.module').then(
|
||||||
|
(m) => m.ReportPageModule
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'portfolio/transactions',
|
||||||
|
loadChildren: () =>
|
||||||
|
import('./pages/portfolio/transactions/transactions-page.module').then(
|
||||||
|
(m) => m.TransactionsPageModule
|
||||||
|
)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'pricing',
|
path: 'pricing',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
@ -80,32 +115,6 @@ const routes: Routes = [
|
|||||||
(m) => m.LandingPageModule
|
(m) => m.LandingPageModule
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'tools',
|
|
||||||
loadChildren: () =>
|
|
||||||
import('./pages/tools/tools-page.module').then((m) => m.ToolsPageModule)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'tools/analysis',
|
|
||||||
loadChildren: () =>
|
|
||||||
import('./pages/tools/analysis/analysis-page.module').then(
|
|
||||||
(m) => m.AnalysisPageModule
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'tools/report',
|
|
||||||
loadChildren: () =>
|
|
||||||
import('./pages/tools/report/report-page.module').then(
|
|
||||||
(m) => m.ReportPageModule
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'transactions',
|
|
||||||
loadChildren: () =>
|
|
||||||
import('./pages/transactions/transactions-page.module').then(
|
|
||||||
(m) => m.TransactionsPageModule
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'webauthn',
|
path: 'webauthn',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
|
@ -2,7 +2,13 @@
|
|||||||
<ng-container matColumnDef="account">
|
<ng-container matColumnDef="account">
|
||||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Name</th>
|
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Name</th>
|
||||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
{{ element.name }}
|
<gf-symbol-icon
|
||||||
|
*ngIf="element.Platform?.url"
|
||||||
|
class="d-inline d-sm-none mr-1"
|
||||||
|
[tooltip]="element.Platform?.name"
|
||||||
|
[url]="element.Platform?.url"
|
||||||
|
></gf-symbol-icon>
|
||||||
|
<span>{{ element.name }}</span>
|
||||||
<span
|
<span
|
||||||
*ngIf="element.isDefault"
|
*ngIf="element.isDefault"
|
||||||
class="d-lg-inline-block d-none text-muted"
|
class="d-lg-inline-block d-none text-muted"
|
||||||
@ -12,13 +18,20 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="platform">
|
<ng-container matColumnDef="platform">
|
||||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Platform</th>
|
<th
|
||||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
*matHeaderCellDef
|
||||||
|
class="d-none d-lg-table-cell px-1"
|
||||||
|
i18n
|
||||||
|
mat-header-cell
|
||||||
|
>
|
||||||
|
Platform
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<gf-symbol-icon
|
<gf-symbol-icon
|
||||||
*ngIf="element.Platform?.url"
|
*ngIf="element.Platform?.url"
|
||||||
class="mr-1"
|
class="mr-1"
|
||||||
[tooltip]=""
|
[tooltip]="element.Platform?.name"
|
||||||
[url]="element.Platform?.url"
|
[url]="element.Platform?.url"
|
||||||
></gf-symbol-icon>
|
></gf-symbol-icon>
|
||||||
<span>{{ element.Platform?.name }}</span>
|
<span>{{ element.Platform?.name }}</span>
|
||||||
@ -27,17 +40,20 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="transactions">
|
<ng-container matColumnDef="transactions">
|
||||||
<th *matHeaderCellDef class="text-right" i18n mat-header-cell>
|
<th *matHeaderCellDef class="px-1 text-right" mat-header-cell>
|
||||||
Transactions
|
<span class="d-block d-sm-none">#</span>
|
||||||
|
<span class="d-none d-sm-block" i18n>Transactions</span>
|
||||||
</th>
|
</th>
|
||||||
<td *matCellDef="let element" class="text-right" mat-cell>
|
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
||||||
{{ element.Order?.length }}
|
{{ element.Order?.length }}
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="balance">
|
<ng-container matColumnDef="balance">
|
||||||
<th *matHeaderCellDef class="text-right" i18n mat-header-cell>Balance</th>
|
<th *matHeaderCellDef class="px-1 text-right" i18n mat-header-cell>
|
||||||
<td *matCellDef="let element" class="text-right" mat-cell>
|
Balance
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
||||||
<gf-value
|
<gf-value
|
||||||
class="d-inline-block justify-content-end"
|
class="d-inline-block justify-content-end"
|
||||||
[currency]="element.currency"
|
[currency]="element.currency"
|
||||||
|
@ -22,27 +22,22 @@
|
|||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
'font-weight-bold':
|
'font-weight-bold': currentRoute === 'portfolio',
|
||||||
currentRoute === 'analysis' ||
|
'text-decoration-underline': currentRoute === 'portfolio'
|
||||||
currentRoute === 'report' ||
|
|
||||||
currentRoute === 'tools',
|
|
||||||
'text-decoration-underline':
|
|
||||||
currentRoute === 'analysis' ||
|
|
||||||
currentRoute === 'report' ||
|
|
||||||
currentRoute === 'tools'
|
|
||||||
}"
|
}"
|
||||||
[routerLink]="['/tools']"
|
[routerLink]="['/portfolio']"
|
||||||
>Tools</a
|
>Portfolio</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
|
*ngIf="user?.settings?.viewMode !== 'DEFAULT'"
|
||||||
class="d-none d-sm-block mx-1"
|
class="d-none d-sm-block mx-1"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
'font-weight-bold': currentRoute === 'transactions',
|
'font-weight-bold': currentRoute === 'portfolio',
|
||||||
'text-decoration-underline': currentRoute === 'transactions'
|
'text-decoration-underline': currentRoute === 'portfolio'
|
||||||
}"
|
}"
|
||||||
[routerLink]="['/transactions']"
|
[routerLink]="['/portfolio', 'transactions']"
|
||||||
>Transactions</a
|
>Transactions</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@ -166,22 +161,20 @@
|
|||||||
i18n
|
i18n
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
'font-weight-bold':
|
'font-weight-bold': currentRoute === 'portfolio'
|
||||||
currentRoute === 'analysis' ||
|
|
||||||
currentRoute === 'report' ||
|
|
||||||
currentRoute === 'tools'
|
|
||||||
}"
|
}"
|
||||||
[routerLink]="['/tools']"
|
[routerLink]="['/portfolio']"
|
||||||
>Tools</a
|
>Portfolio</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
|
*ngIf="user?.settings?.viewMode !== 'DEFAULT'"
|
||||||
class="d-block d-sm-none"
|
class="d-block d-sm-none"
|
||||||
i18n
|
i18n
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
'font-weight-bold': currentRoute === 'transactions'
|
'font-weight-bold': currentRoute === 'portfolio'
|
||||||
}"
|
}"
|
||||||
[routerLink]="['/transactions']"
|
[routerLink]="['/portfolio', 'transactions']"
|
||||||
>Transactions</a
|
>Transactions</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
|
@ -73,7 +73,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
const lastItem = this.investments[this.investments.length - 1];
|
const lastItem = this.investments[this.investments.length - 1];
|
||||||
this.investments.push({
|
this.investments.push({
|
||||||
...lastItem,
|
...lastItem,
|
||||||
date: addMonths(parseISO(lastItem.date), 3).toISOString()
|
date: addMonths(new Date(), 3).toISOString()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,8 +154,8 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private isInFuture(aContext: any, aValue: any) {
|
private isInFuture<T>(aContext: any, aValue: T) {
|
||||||
return isAfter(new Date(aContext?.p0?.parsed?.x), new Date())
|
return isAfter(new Date(aContext?.p1?.parsed?.x), new Date())
|
||||||
? aValue
|
? aValue
|
||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<a
|
<a
|
||||||
class="align-items-center justify-content-center"
|
class="align-items-center justify-content-center"
|
||||||
color="primary"
|
color="primary"
|
||||||
[routerLink]="['/transactions']"
|
[routerLink]="['/portfolio', 'transactions']"
|
||||||
mat-button
|
mat-button
|
||||||
>
|
>
|
||||||
<span i18n>Time to add your first transaction.</span>
|
<span i18n>Time to add your first transaction.</span>
|
||||||
|
@ -1,74 +0,0 @@
|
|||||||
<div class="container p-0">
|
|
||||||
<div class="row px-3 py-1">
|
|
||||||
<div class="d-flex flex-grow-1" i18n>Cash</div>
|
|
||||||
<div class="d-flex justify-content-end">
|
|
||||||
<gf-value
|
|
||||||
class="justify-content-end"
|
|
||||||
[currency]="baseCurrency"
|
|
||||||
[locale]="locale"
|
|
||||||
[value]="isLoading ? undefined : overview?.cash"
|
|
||||||
></gf-value>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col"><hr /></div>
|
|
||||||
</div>
|
|
||||||
<div class="row px-3 py-1">
|
|
||||||
<div class="d-flex flex-grow-1" i18n>Buy</div>
|
|
||||||
<div class="d-flex justify-content-end">
|
|
||||||
<gf-value
|
|
||||||
class="justify-content-end"
|
|
||||||
[currency]="baseCurrency"
|
|
||||||
[locale]="locale"
|
|
||||||
[value]="isLoading ? undefined : overview?.totalBuy"
|
|
||||||
></gf-value>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row px-3 py-1">
|
|
||||||
<div class="d-flex flex-grow-1" i18n>Sell</div>
|
|
||||||
<div class="d-flex justify-content-end">
|
|
||||||
<span
|
|
||||||
*ngIf="overview?.totalSell || overview?.totalSell === 0"
|
|
||||||
class="mr-1"
|
|
||||||
>-</span
|
|
||||||
>
|
|
||||||
<gf-value
|
|
||||||
class="justify-content-end"
|
|
||||||
[currency]="baseCurrency"
|
|
||||||
[locale]="locale"
|
|
||||||
[value]="isLoading ? undefined : overview?.totalSell"
|
|
||||||
></gf-value>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col"><hr /></div>
|
|
||||||
</div>
|
|
||||||
<div class="row px-3">
|
|
||||||
<div class="d-flex flex-grow-1" i18n>Investment</div>
|
|
||||||
<div class="d-flex justify-content-end">
|
|
||||||
<gf-value
|
|
||||||
class="justify-content-end"
|
|
||||||
[currency]="baseCurrency"
|
|
||||||
[locale]="locale"
|
|
||||||
[value]="isLoading ? undefined : overview?.committedFunds"
|
|
||||||
></gf-value>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col"><hr /></div>
|
|
||||||
</div>
|
|
||||||
<div class="row px-3">
|
|
||||||
<div class="d-flex flex-grow-1" i18n>
|
|
||||||
Fees for {{ overview?.ordersCount }} {overview?.ordersCount, plural, =1
|
|
||||||
{order} other {orders}}
|
|
||||||
</div>
|
|
||||||
<div class="d-flex justify-content-end">
|
|
||||||
<gf-value
|
|
||||||
class="justify-content-end"
|
|
||||||
[currency]="baseCurrency"
|
|
||||||
[locale]="locale"
|
|
||||||
[value]="isLoading ? undefined : overview?.fees"
|
|
||||||
></gf-value>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,28 +0,0 @@
|
|||||||
import {
|
|
||||||
ChangeDetectionStrategy,
|
|
||||||
Component,
|
|
||||||
Input,
|
|
||||||
OnChanges,
|
|
||||||
OnInit
|
|
||||||
} from '@angular/core';
|
|
||||||
import { PortfolioOverview } from '@ghostfolio/common/interfaces';
|
|
||||||
import { Currency } from '@prisma/client';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'gf-portfolio-overview',
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
templateUrl: './portfolio-overview.component.html',
|
|
||||||
styleUrls: ['./portfolio-overview.component.scss']
|
|
||||||
})
|
|
||||||
export class PortfolioOverviewComponent implements OnChanges, OnInit {
|
|
||||||
@Input() baseCurrency: Currency;
|
|
||||||
@Input() isLoading: boolean;
|
|
||||||
@Input() locale: string;
|
|
||||||
@Input() overview: PortfolioOverview;
|
|
||||||
|
|
||||||
public constructor() {}
|
|
||||||
|
|
||||||
public ngOnInit() {}
|
|
||||||
|
|
||||||
public ngOnChanges() {}
|
|
||||||
}
|
|
@ -1,54 +0,0 @@
|
|||||||
<div class="container p-0">
|
|
||||||
<div class="row no-gutters">
|
|
||||||
<div class="flex-grow-1"></div>
|
|
||||||
<div *ngIf="isLoading" class="align-items-center d-flex">
|
|
||||||
<ngx-skeleton-loader
|
|
||||||
animation="pulse"
|
|
||||||
class="mb-2"
|
|
||||||
[theme]="{
|
|
||||||
height: '4rem',
|
|
||||||
width: '15rem'
|
|
||||||
}"
|
|
||||||
></ngx-skeleton-loader>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
[hidden]="isLoading"
|
|
||||||
class="display-4 font-weight-bold m-0 text-center value-container"
|
|
||||||
>
|
|
||||||
<span #value id="value"></span>
|
|
||||||
</div>
|
|
||||||
<div class="flex-grow-1 px-1">
|
|
||||||
<ngx-skeleton-loader
|
|
||||||
*ngIf="isLoading"
|
|
||||||
animation="pulse"
|
|
||||||
[theme]="{
|
|
||||||
height: '1.3rem',
|
|
||||||
width: '2.5rem'
|
|
||||||
}"
|
|
||||||
></ngx-skeleton-loader>
|
|
||||||
<div *ngIf="!isLoading">
|
|
||||||
{{ unit }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div *ngIf="showDetails" class="row">
|
|
||||||
<div class="d-flex col justify-content-end">
|
|
||||||
<gf-value
|
|
||||||
[colorizeSign]="true"
|
|
||||||
[isCurrency]="true"
|
|
||||||
[locale]="locale"
|
|
||||||
[value]="isLoading ? undefined : performance?.currentNetPerformance"
|
|
||||||
></gf-value>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<gf-value
|
|
||||||
[colorizeSign]="true"
|
|
||||||
[isPercent]="true"
|
|
||||||
[locale]="locale"
|
|
||||||
[value]="
|
|
||||||
isLoading ? undefined : performance?.currentNetPerformancePercent
|
|
||||||
"
|
|
||||||
></gf-value>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,9 +0,0 @@
|
|||||||
:host {
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
.value-container {
|
|
||||||
#value {
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,65 +0,0 @@
|
|||||||
import {
|
|
||||||
ChangeDetectionStrategy,
|
|
||||||
Component,
|
|
||||||
ElementRef,
|
|
||||||
Input,
|
|
||||||
OnChanges,
|
|
||||||
OnInit,
|
|
||||||
ViewChild
|
|
||||||
} from '@angular/core';
|
|
||||||
import { PortfolioPerformance } from '@ghostfolio/common/interfaces';
|
|
||||||
import { Currency } from '@prisma/client';
|
|
||||||
import { CountUp } from 'countup.js';
|
|
||||||
import { isNumber } from 'lodash';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'gf-portfolio-performance-summary',
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
templateUrl: './portfolio-performance-summary.component.html',
|
|
||||||
styleUrls: ['./portfolio-performance-summary.component.scss']
|
|
||||||
})
|
|
||||||
export class PortfolioPerformanceSummaryComponent implements OnChanges, OnInit {
|
|
||||||
@Input() baseCurrency: Currency;
|
|
||||||
@Input() isLoading: boolean;
|
|
||||||
@Input() locale: string;
|
|
||||||
@Input() performance: PortfolioPerformance;
|
|
||||||
@Input() showDetails: boolean;
|
|
||||||
|
|
||||||
@ViewChild('value') value: ElementRef;
|
|
||||||
|
|
||||||
public unit: string;
|
|
||||||
|
|
||||||
public constructor() {}
|
|
||||||
|
|
||||||
public ngOnInit() {}
|
|
||||||
|
|
||||||
public ngOnChanges() {
|
|
||||||
if (this.isLoading) {
|
|
||||||
if (this.value?.nativeElement) {
|
|
||||||
this.value.nativeElement.innerHTML = '';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (isNumber(this.performance?.currentValue)) {
|
|
||||||
this.unit = this.baseCurrency;
|
|
||||||
|
|
||||||
new CountUp('value', this.performance?.currentValue, {
|
|
||||||
decimalPlaces: 2,
|
|
||||||
duration: 1,
|
|
||||||
separator: `'`
|
|
||||||
}).start();
|
|
||||||
} else if (this.performance?.currentValue === null) {
|
|
||||||
this.unit = '%';
|
|
||||||
|
|
||||||
new CountUp(
|
|
||||||
'value',
|
|
||||||
this.performance?.currentNetPerformancePercent * 100,
|
|
||||||
{
|
|
||||||
decimalPlaces: 2,
|
|
||||||
duration: 0.75,
|
|
||||||
separator: `'`
|
|
||||||
}
|
|
||||||
).start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
|
||||||
|
|
||||||
import { GfValueModule } from '../value/value.module';
|
|
||||||
import { PortfolioPerformanceSummaryComponent } from './portfolio-performance-summary.component';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [PortfolioPerformanceSummaryComponent],
|
|
||||||
exports: [PortfolioPerformanceSummaryComponent],
|
|
||||||
imports: [CommonModule, GfValueModule, NgxSkeletonLoaderModule],
|
|
||||||
providers: []
|
|
||||||
})
|
|
||||||
export class GfPortfolioPerformanceSummaryModule {}
|
|
@ -1,59 +1,47 @@
|
|||||||
<div class="container p-0">
|
<div class="container p-0">
|
||||||
<div class="row px-3 py-2">
|
<div class="row no-gutters">
|
||||||
<div class="d-flex flex-grow-1" i18n>Value</div>
|
<div class="flex-grow-1"></div>
|
||||||
<div class="d-flex flex-column flex-wrap justify-content-end">
|
<div *ngIf="isLoading" class="align-items-center d-flex">
|
||||||
<gf-value
|
<ngx-skeleton-loader
|
||||||
class="justify-content-end"
|
animation="pulse"
|
||||||
position="end"
|
class="mb-2"
|
||||||
[currency]="baseCurrency"
|
[theme]="{
|
||||||
[locale]="locale"
|
height: '4rem',
|
||||||
[value]="isLoading ? undefined : performance?.currentValue"
|
width: '15rem'
|
||||||
></gf-value>
|
}"
|
||||||
|
></ngx-skeleton-loader>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
[hidden]="isLoading"
|
||||||
|
class="display-4 font-weight-bold m-0 text-center value-container"
|
||||||
|
>
|
||||||
|
<span #value id="value"></span>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1 px-1">
|
||||||
|
<ngx-skeleton-loader
|
||||||
|
*ngIf="isLoading"
|
||||||
|
animation="pulse"
|
||||||
|
[theme]="{
|
||||||
|
height: '1.3rem',
|
||||||
|
width: '2.5rem'
|
||||||
|
}"
|
||||||
|
></ngx-skeleton-loader>
|
||||||
|
<div *ngIf="!isLoading">
|
||||||
|
{{ unit }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row px-3 py-1">
|
</div>
|
||||||
<div class="d-flex flex-grow-1" i18n>Absolute Performance</div>
|
<div *ngIf="showDetails" class="row">
|
||||||
<div class="d-flex flex-column flex-wrap justify-content-end">
|
<div class="d-flex col justify-content-end">
|
||||||
<gf-value
|
<gf-value
|
||||||
class="justify-content-end"
|
|
||||||
position="end"
|
|
||||||
[colorizeSign]="true"
|
[colorizeSign]="true"
|
||||||
[currency]="baseCurrency"
|
[isCurrency]="true"
|
||||||
[locale]="locale"
|
|
||||||
[value]="isLoading ? undefined : performance?.currentGrossPerformance"
|
|
||||||
></gf-value>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row px-3 py-1">
|
|
||||||
<div class="d-flex flex-grow-1" i18n>Performance (TWR)</div>
|
|
||||||
<div class="d-flex flex-column flex-wrap justify-content-end">
|
|
||||||
<gf-value
|
|
||||||
class="justify-content-end"
|
|
||||||
position="end"
|
|
||||||
[colorizeSign]="true"
|
|
||||||
[isPercent]="true"
|
|
||||||
[locale]="locale"
|
|
||||||
[value]="
|
|
||||||
isLoading ? undefined : performance?.currentGrossPerformancePercent
|
|
||||||
"
|
|
||||||
></gf-value>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!--
|
|
||||||
<div class="row px-3 py-2">
|
|
||||||
<div class="d-flex flex-grow-1" i18n>Net performance</div>
|
|
||||||
<div class="d-flex flex-column flex-wrap justify-content-end">
|
|
||||||
<gf-value
|
|
||||||
class="justify-content-end mb-2"
|
|
||||||
position="end"
|
|
||||||
[colorizeSign]="true"
|
|
||||||
[currency]="baseCurrency"
|
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[value]="isLoading ? undefined : performance?.currentNetPerformance"
|
[value]="isLoading ? undefined : performance?.currentNetPerformance"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
<gf-value
|
<gf-value
|
||||||
class="justify-content-end"
|
|
||||||
position="end"
|
|
||||||
[colorizeSign]="true"
|
[colorizeSign]="true"
|
||||||
[isPercent]="true"
|
[isPercent]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
@ -63,5 +51,4 @@
|
|||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
-->
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,3 +1,9 @@
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
|
.value-container {
|
||||||
|
#value {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
|
ElementRef,
|
||||||
Input,
|
Input,
|
||||||
OnInit
|
OnChanges,
|
||||||
|
OnInit,
|
||||||
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { PortfolioPerformance } from '@ghostfolio/common/interfaces';
|
import { PortfolioPerformance } from '@ghostfolio/common/interfaces';
|
||||||
import { Currency } from '@prisma/client';
|
import { Currency } from '@prisma/client';
|
||||||
|
import { CountUp } from 'countup.js';
|
||||||
|
import { isNumber } from 'lodash';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'gf-portfolio-performance',
|
selector: 'gf-portfolio-performance',
|
||||||
@ -13,13 +18,48 @@ import { Currency } from '@prisma/client';
|
|||||||
templateUrl: './portfolio-performance.component.html',
|
templateUrl: './portfolio-performance.component.html',
|
||||||
styleUrls: ['./portfolio-performance.component.scss']
|
styleUrls: ['./portfolio-performance.component.scss']
|
||||||
})
|
})
|
||||||
export class PortfolioPerformanceComponent implements OnInit {
|
export class PortfolioPerformanceComponent implements OnChanges, OnInit {
|
||||||
@Input() baseCurrency: Currency;
|
@Input() baseCurrency: Currency;
|
||||||
@Input() isLoading: boolean;
|
@Input() isLoading: boolean;
|
||||||
@Input() locale: string;
|
@Input() locale: string;
|
||||||
@Input() performance: PortfolioPerformance;
|
@Input() performance: PortfolioPerformance;
|
||||||
|
@Input() showDetails: boolean;
|
||||||
|
|
||||||
|
@ViewChild('value') value: ElementRef;
|
||||||
|
|
||||||
|
public unit: string;
|
||||||
|
|
||||||
public constructor() {}
|
public constructor() {}
|
||||||
|
|
||||||
public ngOnInit() {}
|
public ngOnInit() {}
|
||||||
|
|
||||||
|
public ngOnChanges() {
|
||||||
|
if (this.isLoading) {
|
||||||
|
if (this.value?.nativeElement) {
|
||||||
|
this.value.nativeElement.innerHTML = '';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isNumber(this.performance?.currentValue)) {
|
||||||
|
this.unit = this.baseCurrency;
|
||||||
|
|
||||||
|
new CountUp('value', this.performance?.currentValue, {
|
||||||
|
decimalPlaces: 2,
|
||||||
|
duration: 1,
|
||||||
|
separator: `'`
|
||||||
|
}).start();
|
||||||
|
} else if (this.performance?.currentValue === null) {
|
||||||
|
this.unit = '%';
|
||||||
|
|
||||||
|
new CountUp(
|
||||||
|
'value',
|
||||||
|
this.performance?.currentNetPerformancePercent * 100,
|
||||||
|
{
|
||||||
|
decimalPlaces: 2,
|
||||||
|
duration: 0.75,
|
||||||
|
separator: `'`
|
||||||
|
}
|
||||||
|
).start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
import { GfValueModule } from '../value/value.module';
|
import { GfValueModule } from '../value/value.module';
|
||||||
import { PortfolioPerformanceComponent } from './portfolio-performance.component';
|
import { PortfolioPerformanceComponent } from './portfolio-performance.component';
|
||||||
@ -7,7 +8,7 @@ import { PortfolioPerformanceComponent } from './portfolio-performance.component
|
|||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [PortfolioPerformanceComponent],
|
declarations: [PortfolioPerformanceComponent],
|
||||||
exports: [PortfolioPerformanceComponent],
|
exports: [PortfolioPerformanceComponent],
|
||||||
imports: [CommonModule, GfValueModule],
|
imports: [CommonModule, GfValueModule, NgxSkeletonLoaderModule],
|
||||||
providers: []
|
providers: []
|
||||||
})
|
})
|
||||||
export class GfPortfolioPerformanceModule {}
|
export class GfPortfolioPerformanceModule {}
|
||||||
|
@ -10,4 +10,4 @@ import { PortfolioProportionChartComponent } from './portfolio-proportion-chart.
|
|||||||
imports: [CommonModule, NgxSkeletonLoaderModule],
|
imports: [CommonModule, NgxSkeletonLoaderModule],
|
||||||
providers: []
|
providers: []
|
||||||
})
|
})
|
||||||
export class PortfolioProportionChartModule {}
|
export class GfPortfolioProportionChartModule {}
|
||||||
|
@ -0,0 +1,134 @@
|
|||||||
|
<div class="container p-0">
|
||||||
|
<div class="row px-3 py-1">
|
||||||
|
<div class="d-flex flex-grow-1" i18n>Time in Market</div>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
{{ timeInMarket }}
|
||||||
|
<gf-value class="justify-content-end" [value]="timeInMarket"></gf-value>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col"><hr /></div>
|
||||||
|
</div>
|
||||||
|
<div class="row px-3">
|
||||||
|
<div class="d-flex flex-grow-1" i18n>
|
||||||
|
Fees for {{ summary?.ordersCount }} {summary?.ordersCount, plural, =1
|
||||||
|
{order} other {orders}}
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<gf-value
|
||||||
|
class="justify-content-end"
|
||||||
|
[currency]="baseCurrency"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="isLoading ? undefined : summary?.fees"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col"><hr /></div>
|
||||||
|
</div>
|
||||||
|
<div class="row px-3 py-1">
|
||||||
|
<div class="d-flex flex-grow-1" i18n>Buy</div>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<gf-value
|
||||||
|
class="justify-content-end"
|
||||||
|
[currency]="baseCurrency"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="isLoading ? undefined : summary?.totalBuy"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row px-3 py-1">
|
||||||
|
<div class="d-flex flex-grow-1" i18n>Sell</div>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<span *ngIf="summary?.totalSell || summary?.totalSell === 0" class="mr-1"
|
||||||
|
>-</span
|
||||||
|
>
|
||||||
|
<gf-value
|
||||||
|
class="justify-content-end"
|
||||||
|
[currency]="baseCurrency"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="isLoading ? undefined : summary?.totalSell"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col"><hr /></div>
|
||||||
|
</div>
|
||||||
|
<div class="row px-3 py-1">
|
||||||
|
<div class="d-flex flex-grow-1" i18n>Investment</div>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<gf-value
|
||||||
|
class="justify-content-end"
|
||||||
|
[currency]="baseCurrency"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="isLoading ? undefined : summary?.committedFunds"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row px-3 py-1">
|
||||||
|
<div class="d-flex flex-grow-1" i18n>Absolute Performance</div>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<gf-value
|
||||||
|
class="justify-content-end"
|
||||||
|
[currency]="baseCurrency"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="isLoading ? undefined : summary?.currentGrossPerformance"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row px-3 py-1">
|
||||||
|
<div class="d-flex flex-grow-1 ml-3" i18n>Performance (TWR)</div>
|
||||||
|
<div class="d-flex flex-column flex-wrap justify-content-end">
|
||||||
|
<gf-value
|
||||||
|
class="justify-content-end"
|
||||||
|
position="end"
|
||||||
|
[colorizeSign]="true"
|
||||||
|
[isPercent]="true"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="
|
||||||
|
isLoading ? undefined : summary?.currentGrossPerformancePercent
|
||||||
|
"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col"><hr /></div>
|
||||||
|
</div>
|
||||||
|
<div class="row px-3 py-1">
|
||||||
|
<div class="d-flex flex-grow-1" i18n>Value</div>
|
||||||
|
<div class="d-flex flex-column flex-wrap justify-content-end">
|
||||||
|
<gf-value
|
||||||
|
class="justify-content-end"
|
||||||
|
position="end"
|
||||||
|
[currency]="baseCurrency"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="isLoading ? undefined : summary?.currentValue"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row px-3 py-1">
|
||||||
|
<div class="d-flex flex-grow-1" i18n>Cash</div>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<gf-value
|
||||||
|
class="justify-content-end"
|
||||||
|
[currency]="baseCurrency"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="isLoading ? undefined : summary?.cash"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col"><hr /></div>
|
||||||
|
</div>
|
||||||
|
<div class="row px-3 py-1">
|
||||||
|
<div class="d-flex flex-grow-1" i18n>Net Worth</div>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<gf-value
|
||||||
|
class="justify-content-end"
|
||||||
|
[currency]="baseCurrency"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="isLoading ? undefined : summary?.netWorth"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,41 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
Input,
|
||||||
|
OnChanges,
|
||||||
|
OnInit
|
||||||
|
} from '@angular/core';
|
||||||
|
import { PortfolioSummary } from '@ghostfolio/common/interfaces';
|
||||||
|
import { Currency } from '@prisma/client';
|
||||||
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'gf-portfolio-summary',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
templateUrl: './portfolio-summary.component.html',
|
||||||
|
styleUrls: ['./portfolio-summary.component.scss']
|
||||||
|
})
|
||||||
|
export class PortfolioSummaryComponent implements OnChanges, OnInit {
|
||||||
|
@Input() baseCurrency: Currency;
|
||||||
|
@Input() isLoading: boolean;
|
||||||
|
@Input() locale: string;
|
||||||
|
@Input() summary: PortfolioSummary;
|
||||||
|
|
||||||
|
public timeInMarket: string;
|
||||||
|
|
||||||
|
public constructor() {}
|
||||||
|
|
||||||
|
public ngOnInit() {}
|
||||||
|
|
||||||
|
public ngOnChanges() {
|
||||||
|
if (this.summary) {
|
||||||
|
if (this.summary.firstOrderDate) {
|
||||||
|
this.timeInMarket = formatDistanceToNow(this.summary.firstOrderDate);
|
||||||
|
} else {
|
||||||
|
this.timeInMarket = '-';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.timeInMarket = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,12 +2,12 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
|
|
||||||
import { GfValueModule } from '../value/value.module';
|
import { GfValueModule } from '../value/value.module';
|
||||||
import { PortfolioOverviewComponent } from './portfolio-overview.component';
|
import { PortfolioSummaryComponent } from './portfolio-summary.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [PortfolioOverviewComponent],
|
declarations: [PortfolioSummaryComponent],
|
||||||
exports: [PortfolioOverviewComponent],
|
exports: [PortfolioSummaryComponent],
|
||||||
imports: [CommonModule, GfValueModule],
|
imports: [CommonModule, GfValueModule],
|
||||||
providers: []
|
providers: []
|
||||||
})
|
})
|
||||||
export class GfPortfolioOverviewModule {}
|
export class GfPortfolioSummaryModule {}
|
@ -1,7 +1,7 @@
|
|||||||
<gf-dialog-header
|
<gf-dialog-header
|
||||||
mat-dialog-title
|
mat-dialog-title
|
||||||
[deviceType]="data.deviceType"
|
[deviceType]="data.deviceType"
|
||||||
[title]="data.title"
|
[title]="data.title ?? data.symbol"
|
||||||
(closeButtonClicked)="onClose()"
|
(closeButtonClicked)="onClose()"
|
||||||
></gf-dialog-header>
|
></gf-dialog-header>
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@
|
|||||||
<gf-value
|
<gf-value
|
||||||
class="mr-3"
|
class="mr-3"
|
||||||
[colorizeSign]="true"
|
[colorizeSign]="true"
|
||||||
[currency]="position?.currency"
|
[currency]="baseCurrency"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[value]="position?.grossPerformance"
|
[value]="position?.grossPerformance"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
|
@ -83,10 +83,10 @@
|
|||||||
*matRowDef="let row; columns: displayedColumns"
|
*matRowDef="let row; columns: displayedColumns"
|
||||||
mat-row
|
mat-row
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
'cursor-pointer': !this.ignoreTypes.includes(row.type)
|
'cursor-pointer': !this.ignoreAssetClasses.includes(row.assetClass)
|
||||||
}"
|
}"
|
||||||
(click)="
|
(click)="
|
||||||
!this.ignoreTypes.includes(row.type) &&
|
!this.ignoreAssetClasses.includes(row.assetClass) &&
|
||||||
onOpenPositionDialog({ symbol: row.symbol, title: row.name })
|
onOpenPositionDialog({ symbol: row.symbol, title: row.name })
|
||||||
"
|
"
|
||||||
></tr>
|
></tr>
|
||||||
@ -111,7 +111,7 @@
|
|||||||
class="my-3 text-center"
|
class="my-3 text-center"
|
||||||
>
|
>
|
||||||
<button i18n mat-stroked-button (click)="onShowAllPositions()">
|
<button i18n mat-stroked-button (click)="onShowAllPositions()">
|
||||||
Show all positions...
|
Show all...
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -14,9 +14,8 @@ import { MatPaginator } from '@angular/material/paginator';
|
|||||||
import { MatSort } from '@angular/material/sort';
|
import { MatSort } from '@angular/material/sort';
|
||||||
import { MatTableDataSource } from '@angular/material/table';
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { Type } from '@ghostfolio/api/services/interfaces/interfaces';
|
|
||||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||||
import { Order as OrderModel } from '@prisma/client';
|
import { AssetClass, Order as OrderModel } from '@prisma/client';
|
||||||
import { Subject, Subscription } from 'rxjs';
|
import { Subject, Subscription } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
@ -43,7 +42,7 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
public dataSource: MatTableDataSource<PortfolioPosition> =
|
public dataSource: MatTableDataSource<PortfolioPosition> =
|
||||||
new MatTableDataSource();
|
new MatTableDataSource();
|
||||||
public displayedColumns = [];
|
public displayedColumns = [];
|
||||||
public ignoreTypes = [Type.Cash];
|
public ignoreAssetClasses = [AssetClass.CASH.toString()];
|
||||||
public isLoading = true;
|
public isLoading = true;
|
||||||
public pageSize = 7;
|
public pageSize = 7;
|
||||||
public routeQueryParams: Subscription;
|
public routeQueryParams: Subscription;
|
||||||
|
@ -5,10 +5,7 @@ import {
|
|||||||
OnChanges,
|
OnChanges,
|
||||||
OnInit
|
OnInit
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {
|
import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
MarketState,
|
|
||||||
Type
|
|
||||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
|
||||||
import { Position } from '@ghostfolio/common/interfaces';
|
import { Position } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -28,8 +25,6 @@ export class PositionsComponent implements OnChanges, OnInit {
|
|||||||
public positionsRest: Position[] = [];
|
public positionsRest: Position[] = [];
|
||||||
public positionsWithPriority: Position[] = [];
|
public positionsWithPriority: Position[] = [];
|
||||||
|
|
||||||
private ignoreTypes = [Type.Cash];
|
|
||||||
|
|
||||||
public constructor() {}
|
public constructor() {}
|
||||||
|
|
||||||
public ngOnInit() {}
|
public ngOnInit() {}
|
||||||
@ -46,10 +41,6 @@ export class PositionsComponent implements OnChanges, OnInit {
|
|||||||
this.positionsWithPriority = [];
|
this.positionsWithPriority = [];
|
||||||
|
|
||||||
for (const portfolioPosition of this.positions) {
|
for (const portfolioPosition of this.positions) {
|
||||||
if (this.ignoreTypes.includes(portfolioPosition.type)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
portfolioPosition.marketState === MarketState.open ||
|
portfolioPosition.marketState === MarketState.open ||
|
||||||
this.range !== '1d'
|
this.range !== '1d'
|
||||||
|
@ -46,9 +46,7 @@
|
|||||||
class="d-none d-lg-table-cell px-1 text-right"
|
class="d-none d-lg-table-cell px-1 text-right"
|
||||||
i18n
|
i18n
|
||||||
mat-header-cell
|
mat-header-cell
|
||||||
>
|
></th>
|
||||||
#
|
|
||||||
</th>
|
|
||||||
<td
|
<td
|
||||||
*matCellDef="let element; let i = index"
|
*matCellDef="let element; let i = index"
|
||||||
class="d-none d-lg-table-cell px-1 text-right"
|
class="d-none d-lg-table-cell px-1 text-right"
|
||||||
@ -96,10 +94,7 @@
|
|||||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
{{ element.symbol | gfSymbol }}
|
{{ element.symbol | gfSymbol }}
|
||||||
<span
|
<span *ngIf="element.isDraft" class="badge badge-secondary ml-1" i18n
|
||||||
*ngIf="isAfter(element.date, endOfToday)"
|
|
||||||
class="badge badge-secondary ml-1"
|
|
||||||
i18n
|
|
||||||
>Draft</span
|
>Draft</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@ -258,7 +253,12 @@
|
|||||||
<tr
|
<tr
|
||||||
*matRowDef="let row; columns: displayedColumns"
|
*matRowDef="let row; columns: displayedColumns"
|
||||||
mat-row
|
mat-row
|
||||||
(click)="onOpenPositionDialog({ symbol: row.symbol, title: '' })"
|
(click)="
|
||||||
|
onOpenPositionDialog({
|
||||||
|
symbol: row.symbol,
|
||||||
|
title: row.SymbolProfile?.name
|
||||||
|
})
|
||||||
|
"
|
||||||
></tr>
|
></tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
@ -85,7 +85,9 @@ export class ValueComponent implements OnChanges, OnInit {
|
|||||||
});
|
});
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
} else if (isDate(new Date(this.value))) {
|
} else {
|
||||||
|
try {
|
||||||
|
if (isDate(new Date(this.value))) {
|
||||||
this.isDate = true;
|
this.isDate = true;
|
||||||
this.isNumber = false;
|
this.isNumber = false;
|
||||||
|
|
||||||
@ -94,6 +96,8 @@ export class ValueComponent implements OnChanges, OnInit {
|
|||||||
DEFAULT_DATE_FORMAT
|
DEFAULT_DATE_FORMAT
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -152,7 +152,7 @@ export class AdminPageComponent implements OnDestroy, OnInit {
|
|||||||
} else if (lastDataGathering === 'IN_PROGRESS') {
|
} else if (lastDataGathering === 'IN_PROGRESS') {
|
||||||
this.dataGatheringInProgress = true;
|
this.dataGatheringInProgress = true;
|
||||||
} else {
|
} else {
|
||||||
this.lastDataGathering = '-';
|
this.lastDataGathering = 'Starting soon...';
|
||||||
}
|
}
|
||||||
|
|
||||||
this.transactionCount = transactionCount;
|
this.transactionCount = transactionCount;
|
||||||
|
@ -65,6 +65,14 @@
|
|||||||
Finanzierung einer Wohnung, Ghostfolio bietet eine solide,
|
Finanzierung einer Wohnung, Ghostfolio bietet eine solide,
|
||||||
datengestützte Entscheidungshilfe.
|
datengestützte Entscheidungshilfe.
|
||||||
</p>
|
</p>
|
||||||
|
<p class="my-5 text-center">
|
||||||
|
<img
|
||||||
|
alt="Ghostfol.io Screenshot"
|
||||||
|
src="./assets/images/screenshot.png"
|
||||||
|
style="max-width: 100%; width: 20rem"
|
||||||
|
title="Ghostfol.io Screenshot"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Ich lege grossen Wert auf Datenschutz. Als
|
Ich lege grossen Wert auf Datenschutz. Als
|
||||||
<a href="https://github.com/ghostfolio/ghostfolio"
|
<a href="https://github.com/ghostfolio/ghostfolio"
|
||||||
|
@ -49,8 +49,8 @@
|
|||||||
<div class="col-md-10 offset-md-1">
|
<div class="col-md-10 offset-md-1">
|
||||||
<blockquote class="blockquote m-0">
|
<blockquote class="blockquote m-0">
|
||||||
<p class="mb-0">
|
<p class="mb-0">
|
||||||
Ghostfolio presents the big picture of assets to make the
|
Ghostfolio presents the big picture of your assets to make
|
||||||
best possible investment decisions.
|
the best possible investment decisions.
|
||||||
</p>
|
</p>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
</div>
|
</div>
|
||||||
@ -63,6 +63,14 @@
|
|||||||
asset classes (stocks, cryptocurrencies, ETFs, etc.) or financing an
|
asset classes (stocks, cryptocurrencies, ETFs, etc.) or financing an
|
||||||
apartment, Ghostfolio offers solid, data-driven decision support.
|
apartment, Ghostfolio offers solid, data-driven decision support.
|
||||||
</p>
|
</p>
|
||||||
|
<p class="my-5 text-center">
|
||||||
|
<img
|
||||||
|
alt="Ghostfol.io Screenshot"
|
||||||
|
src="./assets/images/screenshot.png"
|
||||||
|
style="max-width: 100%; width: 20rem"
|
||||||
|
title="Ghostfol.io Screenshot"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
As I value privacy, data protection is an integral part of
|
As I value privacy, data protection is an integral part of
|
||||||
Ghostfolio. As
|
Ghostfolio. As
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import { ViewportScroller } from '@angular/common';
|
|
||||||
import {
|
import {
|
||||||
AfterViewInit,
|
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
@ -10,6 +8,7 @@ import {
|
|||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { MatTabChangeEvent } from '@angular/material/tabs';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { LineChartItem } from '@ghostfolio/client/components/line-chart/interfaces/line-chart.interface';
|
import { LineChartItem } from '@ghostfolio/client/components/line-chart/interfaces/line-chart.interface';
|
||||||
import { PerformanceChartDialog } from '@ghostfolio/client/components/performance-chart-dialog/performance-chart-dialog.component';
|
import { PerformanceChartDialog } from '@ghostfolio/client/components/performance-chart-dialog/performance-chart-dialog.component';
|
||||||
@ -22,8 +21,8 @@ import {
|
|||||||
} from '@ghostfolio/client/services/settings-storage.service';
|
} from '@ghostfolio/client/services/settings-storage.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
import {
|
import {
|
||||||
PortfolioOverview,
|
|
||||||
PortfolioPerformance,
|
PortfolioPerformance,
|
||||||
|
PortfolioSummary,
|
||||||
Position,
|
Position,
|
||||||
User
|
User
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
@ -31,14 +30,14 @@ import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
|||||||
import { DateRange } from '@ghostfolio/common/types';
|
import { DateRange } from '@ghostfolio/common/types';
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { Subject, Subscription } from 'rxjs';
|
import { Subject, Subscription } from 'rxjs';
|
||||||
import { first, takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'gf-home-page',
|
selector: 'gf-home-page',
|
||||||
templateUrl: './home-page.html',
|
templateUrl: './home-page.html',
|
||||||
styleUrls: ['./home-page.scss']
|
styleUrls: ['./home-page.scss']
|
||||||
})
|
})
|
||||||
export class HomePageComponent implements AfterViewInit, OnDestroy, OnInit {
|
export class HomePageComponent implements OnDestroy, OnInit {
|
||||||
@HostBinding('class.with-create-account-container') get isDemo() {
|
@HostBinding('class.with-create-account-container') get isDemo() {
|
||||||
return this.canCreateAccount;
|
return this.canCreateAccount;
|
||||||
}
|
}
|
||||||
@ -46,6 +45,7 @@ export class HomePageComponent implements AfterViewInit, OnDestroy, OnInit {
|
|||||||
@ViewChild('positionsContainer') positionsContainer: ElementRef;
|
@ViewChild('positionsContainer') positionsContainer: ElementRef;
|
||||||
|
|
||||||
public canCreateAccount: boolean;
|
public canCreateAccount: boolean;
|
||||||
|
public currentTabIndex = 0;
|
||||||
public dateRange: DateRange;
|
public dateRange: DateRange;
|
||||||
public dateRangeOptions: ToggleOption[] = [
|
public dateRangeOptions: ToggleOption[] = [
|
||||||
{ label: 'Today', value: '1d' },
|
{ label: 'Today', value: '1d' },
|
||||||
@ -59,14 +59,14 @@ export class HomePageComponent implements AfterViewInit, OnDestroy, OnInit {
|
|||||||
public hasImpersonationId: boolean;
|
public hasImpersonationId: boolean;
|
||||||
public hasPermissionToAccessFearAndGreedIndex: boolean;
|
public hasPermissionToAccessFearAndGreedIndex: boolean;
|
||||||
public hasPermissionToReadForeignPortfolio: boolean;
|
public hasPermissionToReadForeignPortfolio: boolean;
|
||||||
public hasPositions = false;
|
public hasPositions: boolean;
|
||||||
public historicalDataItems: LineChartItem[];
|
public historicalDataItems: LineChartItem[];
|
||||||
public isLoadingOverview = true;
|
|
||||||
public isLoadingPerformance = true;
|
public isLoadingPerformance = true;
|
||||||
public overview: PortfolioOverview;
|
public isLoadingSummary = true;
|
||||||
public performance: PortfolioPerformance;
|
public performance: PortfolioPerformance;
|
||||||
public positions: Position[];
|
public positions: Position[];
|
||||||
public routeQueryParams: Subscription;
|
public routeQueryParams: Subscription;
|
||||||
|
public summary: PortfolioSummary;
|
||||||
public user: User;
|
public user: User;
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
@ -83,8 +83,7 @@ export class HomePageComponent implements AfterViewInit, OnDestroy, OnInit {
|
|||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private settingsStorageService: SettingsStorageService,
|
private settingsStorageService: SettingsStorageService,
|
||||||
private userService: UserService,
|
private userService: UserService
|
||||||
private viewportScroller: ViewportScroller
|
|
||||||
) {
|
) {
|
||||||
this.routeQueryParams = this.route.queryParams
|
this.routeQueryParams = this.route.queryParams
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
@ -150,19 +149,15 @@ export class HomePageComponent implements AfterViewInit, OnDestroy, OnInit {
|
|||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngAfterViewInit(): void {
|
|
||||||
this.route.fragment
|
|
||||||
.pipe(first())
|
|
||||||
.subscribe((fragment) => this.viewportScroller.scrollToAnchor(fragment));
|
|
||||||
}
|
|
||||||
|
|
||||||
public onChangeDateRange(aDateRange: DateRange) {
|
public onChangeDateRange(aDateRange: DateRange) {
|
||||||
this.dateRange = aDateRange;
|
this.dateRange = aDateRange;
|
||||||
this.settingsStorageService.setSetting(RANGE, this.dateRange);
|
this.settingsStorageService.setSetting(RANGE, this.dateRange);
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
public onTabChanged() {
|
public onTabChanged(event: MatTabChangeEvent) {
|
||||||
|
this.currentTabIndex = event.index;
|
||||||
|
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,10 +186,8 @@ export class HomePageComponent implements AfterViewInit, OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private update() {
|
private update() {
|
||||||
this.hasPositions = undefined;
|
if (this.currentTabIndex === 0) {
|
||||||
this.isLoadingOverview = true;
|
|
||||||
this.isLoadingPerformance = true;
|
this.isLoadingPerformance = true;
|
||||||
this.positions = undefined;
|
|
||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchChart({ range: this.dateRange })
|
.fetchChart({ range: this.dateRange })
|
||||||
@ -219,17 +212,7 @@ export class HomePageComponent implements AfterViewInit, OnDestroy, OnInit {
|
|||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
} else if (this.currentTabIndex === 1) {
|
||||||
this.dataService
|
|
||||||
.fetchPortfolioOverview()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe((response) => {
|
|
||||||
this.overview = response;
|
|
||||||
this.isLoadingOverview = false;
|
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchPositions({ range: this.dateRange })
|
.fetchPositions({ range: this.dateRange })
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
@ -239,6 +222,19 @@ export class HomePageComponent implements AfterViewInit, OnDestroy, OnInit {
|
|||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
} else if (this.currentTabIndex === 2) {
|
||||||
|
this.isLoadingSummary = true;
|
||||||
|
|
||||||
|
this.dataService
|
||||||
|
.fetchPortfolioSummary()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((response) => {
|
||||||
|
this.summary = response;
|
||||||
|
this.isLoadingSummary = false;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,8 @@
|
|||||||
class="position-absolute"
|
class="position-absolute"
|
||||||
headerPosition="below"
|
headerPosition="below"
|
||||||
mat-align-tabs="center"
|
mat-align-tabs="center"
|
||||||
(selectedTabChange)="onTabChanged()"
|
[disablePagination]="true"
|
||||||
|
(selectedTabChange)="onTabChanged($event)"
|
||||||
>
|
>
|
||||||
<mat-tab>
|
<mat-tab>
|
||||||
<ng-template mat-tab-label>
|
<ng-template mat-tab-label>
|
||||||
@ -54,14 +55,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="overview-container row mt-1">
|
<div class="overview-container row mt-1">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<gf-portfolio-performance-summary
|
<gf-portfolio-performance
|
||||||
class="pb-4"
|
class="pb-4"
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
[isLoading]="isLoadingPerformance"
|
[isLoading]="isLoadingPerformance"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[performance]="performance"
|
[performance]="performance"
|
||||||
[showDetails]="!hasImpersonationId || hasPermissionToReadForeignPortfolio"
|
[showDetails]="!hasImpersonationId || hasPermissionToReadForeignPortfolio"
|
||||||
></gf-portfolio-performance-summary>
|
></gf-portfolio-performance>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<gf-toggle
|
<gf-toggle
|
||||||
[defaultValue]="dateRange"
|
[defaultValue]="dateRange"
|
||||||
@ -79,9 +80,9 @@
|
|||||||
<ion-icon name="wallet-outline" size="large"></ion-icon>
|
<ion-icon name="wallet-outline" size="large"></ion-icon>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<div class="container justify-content-center pb-3 px-3 positions">
|
<div class="container justify-content-center pb-3 px-3 positions">
|
||||||
<h3 class="d-flex justify-content-center mb-3" i18n>Positions</h3>
|
<h3 class="d-flex justify-content-center mb-3" i18n>Holdings</h3>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="align-items-center col">
|
<div class="align-items-center col-xs-12 col-md-8 offset-md-2">
|
||||||
<div class="pb-2 text-center">
|
<div class="pb-2 text-center">
|
||||||
<gf-toggle
|
<gf-toggle
|
||||||
[defaultValue]="dateRange"
|
[defaultValue]="dateRange"
|
||||||
@ -118,33 +119,18 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
<div class="container pb-3 px-3 positions">
|
<div class="container pb-3 px-3 positions">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 col-md-6 mb-3">
|
<div class="col-xs-12 col-md-8 offset-md-2">
|
||||||
<mat-card class="h-100">
|
|
||||||
<mat-card-header>
|
|
||||||
<mat-card-title i18n>Performance</mat-card-title>
|
|
||||||
</mat-card-header>
|
|
||||||
<mat-card-content>
|
|
||||||
<gf-portfolio-performance
|
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
|
||||||
[isLoading]="isLoadingPerformance"
|
|
||||||
[locale]="user?.settings?.locale"
|
|
||||||
[performance]="performance"
|
|
||||||
></gf-portfolio-performance>
|
|
||||||
</mat-card-content>
|
|
||||||
</mat-card>
|
|
||||||
</div>
|
|
||||||
<div class="col-xs-12 col-md-6">
|
|
||||||
<mat-card class="h-100">
|
<mat-card class="h-100">
|
||||||
<mat-card-header>
|
<mat-card-header>
|
||||||
<mat-card-title i18n>Summary</mat-card-title>
|
<mat-card-title i18n>Summary</mat-card-title>
|
||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<gf-portfolio-overview
|
<gf-portfolio-summary
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
[isLoading]="isLoadingOverview"
|
[isLoading]="isLoadingSummary"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[overview]="overview"
|
[summary]="summary"
|
||||||
></gf-portfolio-overview>
|
></gf-portfolio-summary>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,9 +7,8 @@ import { RouterModule } from '@angular/router';
|
|||||||
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
|
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
|
||||||
import { GfNoTransactionsInfoModule } from '@ghostfolio/client/components/no-transactions-info/no-transactions-info.module';
|
import { GfNoTransactionsInfoModule } from '@ghostfolio/client/components/no-transactions-info/no-transactions-info.module';
|
||||||
import { GfPerformanceChartDialogModule } from '@ghostfolio/client/components/performance-chart-dialog/performance-chart-dialog.module';
|
import { GfPerformanceChartDialogModule } from '@ghostfolio/client/components/performance-chart-dialog/performance-chart-dialog.module';
|
||||||
import { GfPortfolioOverviewModule } from '@ghostfolio/client/components/portfolio-overview/portfolio-overview.module';
|
|
||||||
import { GfPortfolioPerformanceSummaryModule } from '@ghostfolio/client/components/portfolio-performance-summary/portfolio-performance-summary.module';
|
|
||||||
import { GfPortfolioPerformanceModule } from '@ghostfolio/client/components/portfolio-performance/portfolio-performance.module';
|
import { GfPortfolioPerformanceModule } from '@ghostfolio/client/components/portfolio-performance/portfolio-performance.module';
|
||||||
|
import { GfPortfolioSummaryModule } from '@ghostfolio/client/components/portfolio-summary/portfolio-summary.module';
|
||||||
import { GfPositionsModule } from '@ghostfolio/client/components/positions/positions.module';
|
import { GfPositionsModule } from '@ghostfolio/client/components/positions/positions.module';
|
||||||
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
||||||
|
|
||||||
@ -24,9 +23,8 @@ import { HomePageComponent } from './home-page.component';
|
|||||||
GfLineChartModule,
|
GfLineChartModule,
|
||||||
GfNoTransactionsInfoModule,
|
GfNoTransactionsInfoModule,
|
||||||
GfPerformanceChartDialogModule,
|
GfPerformanceChartDialogModule,
|
||||||
GfPortfolioOverviewModule,
|
|
||||||
GfPortfolioPerformanceModule,
|
GfPortfolioPerformanceModule,
|
||||||
GfPortfolioPerformanceSummaryModule,
|
GfPortfolioSummaryModule,
|
||||||
GfPositionsModule,
|
GfPositionsModule,
|
||||||
GfToggleModule,
|
GfToggleModule,
|
||||||
HomePageRoutingModule,
|
HomePageRoutingModule,
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||||
|
|
||||||
|
import { AllocationsPageComponent } from './allocations-page.component';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{ path: '', component: AllocationsPageComponent, canActivate: [AuthGuard] }
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class AllocationsPageRoutingModule {}
|
@ -4,22 +4,17 @@ import { DataService } from '@ghostfolio/client/services/data.service';
|
|||||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||||
import {
|
import { PortfolioPosition, User } from '@ghostfolio/common/interfaces';
|
||||||
PortfolioItem,
|
|
||||||
PortfolioPosition,
|
|
||||||
User
|
|
||||||
} from '@ghostfolio/common/interfaces';
|
|
||||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'gf-analysis-page',
|
selector: 'gf-allocations-page',
|
||||||
templateUrl: './analysis-page.html',
|
templateUrl: './allocations-page.html',
|
||||||
styleUrls: ['./analysis-page.scss']
|
styleUrls: ['./allocations-page.scss']
|
||||||
})
|
})
|
||||||
export class AnalysisPageComponent implements OnDestroy, OnInit {
|
export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||||
public accounts: {
|
public accounts: {
|
||||||
[symbol: string]: Pick<PortfolioPosition, 'name'> & { value: number };
|
[symbol: string]: Pick<PortfolioPosition, 'name'> & { value: number };
|
||||||
};
|
};
|
||||||
@ -36,7 +31,6 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
{ label: 'Initial', value: 'original' },
|
{ label: 'Initial', value: 'original' },
|
||||||
{ label: 'Current', value: 'current' }
|
{ label: 'Current', value: 'current' }
|
||||||
];
|
];
|
||||||
public investments: InvestmentItem[];
|
|
||||||
public portfolioPositions: { [symbol: string]: PortfolioPosition };
|
public portfolioPositions: { [symbol: string]: PortfolioPosition };
|
||||||
public positions: { [symbol: string]: any };
|
public positions: { [symbol: string]: any };
|
||||||
public positionsArray: PortfolioPosition[];
|
public positionsArray: PortfolioPosition[];
|
||||||
@ -71,15 +65,6 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
this.hasImpersonationId = !!aId;
|
this.hasImpersonationId = !!aId;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.dataService
|
|
||||||
.fetchInvestments()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe((response) => {
|
|
||||||
this.investments = response;
|
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchPortfolioPositions({})
|
.fetchPortfolioPositions({})
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
@ -131,9 +116,9 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
for (const [symbol, position] of Object.entries(aPortfolioPositions)) {
|
for (const [symbol, position] of Object.entries(aPortfolioPositions)) {
|
||||||
this.positions[symbol] = {
|
this.positions[symbol] = {
|
||||||
|
assetClass: position.assetClass,
|
||||||
currency: position.currency,
|
currency: position.currency,
|
||||||
exchange: position.exchange,
|
exchange: position.exchange,
|
||||||
type: position.type,
|
|
||||||
value:
|
value:
|
||||||
aPeriod === 'original'
|
aPeriod === 'original'
|
||||||
? position.allocationInvestment
|
? position.allocationInvestment
|
@ -1,10 +1,9 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="mb-5 row">
|
<div class="mb-5 row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h3 class="d-flex justify-content-center mb-3" i18n>Analysis</h3>
|
<h3 class="d-flex justify-content-center mb-3" i18n>Allocations</h3>
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="m-0" i18n>Positions</h4>
|
|
||||||
<gf-positions-table
|
<gf-positions-table
|
||||||
|
class="mb-4"
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
[deviceType]="deviceType"
|
[deviceType]="deviceType"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
@ -12,30 +11,7 @@
|
|||||||
></gf-positions-table>
|
></gf-positions-table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="proportion-charts row">
|
<div class="proportion-charts row">
|
||||||
<div class="col-md-6">
|
|
||||||
<mat-card class="mb-3">
|
|
||||||
<mat-card-header class="w-100">
|
|
||||||
<mat-card-title i18n>By Type</mat-card-title>
|
|
||||||
<gf-toggle
|
|
||||||
[defaultValue]="period"
|
|
||||||
[isLoading]="false"
|
|
||||||
[options]="periodOptions"
|
|
||||||
(change)="onChangePeriod($event.value)"
|
|
||||||
></gf-toggle>
|
|
||||||
</mat-card-header>
|
|
||||||
<mat-card-content>
|
|
||||||
<gf-portfolio-proportion-chart
|
|
||||||
key="type"
|
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
|
||||||
[isInPercent]="true"
|
|
||||||
[locale]="user?.settings?.locale"
|
|
||||||
[positions]="positions"
|
|
||||||
></gf-portfolio-proportion-chart>
|
|
||||||
</mat-card-content>
|
|
||||||
</mat-card>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
<mat-card-header class="w-100">
|
<mat-card-header class="w-100">
|
||||||
@ -61,7 +37,7 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
<mat-card-header class="w-100">
|
<mat-card-header class="w-100">
|
||||||
<mat-card-title i18n>By Currency</mat-card-title>
|
<mat-card-title i18n>By Asset Class</mat-card-title>
|
||||||
<gf-toggle
|
<gf-toggle
|
||||||
[defaultValue]="period"
|
[defaultValue]="period"
|
||||||
[isLoading]="false"
|
[isLoading]="false"
|
||||||
@ -71,7 +47,7 @@
|
|||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<gf-portfolio-proportion-chart
|
<gf-portfolio-proportion-chart
|
||||||
key="currency"
|
key="assetClass"
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
[isInPercent]="true"
|
[isInPercent]="true"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
@ -83,7 +59,7 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
<mat-card-header class="w-100">
|
<mat-card-header class="w-100">
|
||||||
<mat-card-title i18n>By Exchange</mat-card-title>
|
<mat-card-title i18n>By Currency</mat-card-title>
|
||||||
<gf-toggle
|
<gf-toggle
|
||||||
[defaultValue]="period"
|
[defaultValue]="period"
|
||||||
[isLoading]="false"
|
[isLoading]="false"
|
||||||
@ -93,7 +69,7 @@
|
|||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<gf-portfolio-proportion-chart
|
<gf-portfolio-proportion-chart
|
||||||
key="exchange"
|
key="currency"
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
[isInPercent]="true"
|
[isInPercent]="true"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
@ -192,20 +168,4 @@
|
|||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="investment-chart row">
|
|
||||||
<div class="col-lg">
|
|
||||||
<mat-card class="mb-3">
|
|
||||||
<mat-card-header>
|
|
||||||
<mat-card-title class="align-items-center d-flex" i18n
|
|
||||||
>Timeline</mat-card-title
|
|
||||||
>
|
|
||||||
</mat-card-header>
|
|
||||||
<mat-card-content>
|
|
||||||
<gf-investment-chart
|
|
||||||
[investments]='investments'
|
|
||||||
></gf-investment-chart>
|
|
||||||
</mat-card-content>
|
|
||||||
</mat-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
@ -0,0 +1,27 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
import { GfPortfolioProportionChartModule } from '@ghostfolio/client/components/portfolio-proportion-chart/portfolio-proportion-chart.module';
|
||||||
|
import { GfPositionsTableModule } from '@ghostfolio/client/components/positions-table/positions-table.module';
|
||||||
|
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
||||||
|
import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module';
|
||||||
|
|
||||||
|
import { AllocationsPageRoutingModule } from './allocations-page-routing.module';
|
||||||
|
import { AllocationsPageComponent } from './allocations-page.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [AllocationsPageComponent],
|
||||||
|
exports: [],
|
||||||
|
imports: [
|
||||||
|
AllocationsPageRoutingModule,
|
||||||
|
CommonModule,
|
||||||
|
GfPortfolioProportionChartModule,
|
||||||
|
GfPositionsTableModule,
|
||||||
|
GfToggleModule,
|
||||||
|
GfWorldMapChartModule,
|
||||||
|
MatCardModule
|
||||||
|
],
|
||||||
|
providers: [],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
})
|
||||||
|
export class AllocationsPageModule {}
|
@ -1,16 +1,4 @@
|
|||||||
:host {
|
:host {
|
||||||
.investment-chart {
|
|
||||||
.mat-card {
|
|
||||||
.mat-card-content {
|
|
||||||
aspect-ratio: 16 / 9;
|
|
||||||
|
|
||||||
gf-investment-chart {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.proportion-charts {
|
.proportion-charts {
|
||||||
.mat-card {
|
.mat-card {
|
||||||
.mat-card-content {
|
.mat-card-content {
|
@ -0,0 +1,92 @@
|
|||||||
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { ToggleOption } from '@ghostfolio/client/components/toggle/interfaces/toggle-option.type';
|
||||||
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
|
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||||
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
|
import { PortfolioPosition, User } from '@ghostfolio/common/interfaces';
|
||||||
|
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||||
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'gf-analysis-page',
|
||||||
|
templateUrl: './analysis-page.html',
|
||||||
|
styleUrls: ['./analysis-page.scss']
|
||||||
|
})
|
||||||
|
export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||||
|
public accounts: {
|
||||||
|
[symbol: string]: Pick<PortfolioPosition, 'name'> & { value: number };
|
||||||
|
};
|
||||||
|
public continents: {
|
||||||
|
[code: string]: { name: string; value: number };
|
||||||
|
};
|
||||||
|
public countries: {
|
||||||
|
[code: string]: { name: string; value: number };
|
||||||
|
};
|
||||||
|
public deviceType: string;
|
||||||
|
public hasImpersonationId: boolean;
|
||||||
|
public period = 'current';
|
||||||
|
public periodOptions: ToggleOption[] = [
|
||||||
|
{ label: 'Initial', value: 'original' },
|
||||||
|
{ label: 'Current', value: 'current' }
|
||||||
|
];
|
||||||
|
public investments: InvestmentItem[];
|
||||||
|
public portfolioPositions: { [symbol: string]: PortfolioPosition };
|
||||||
|
public positions: { [symbol: string]: any };
|
||||||
|
public sectors: {
|
||||||
|
[name: string]: { name: string; value: number };
|
||||||
|
};
|
||||||
|
public user: User;
|
||||||
|
|
||||||
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
public constructor(
|
||||||
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
|
private dataService: DataService,
|
||||||
|
private deviceService: DeviceDetectorService,
|
||||||
|
private impersonationStorageService: ImpersonationStorageService,
|
||||||
|
private userService: UserService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the controller
|
||||||
|
*/
|
||||||
|
public ngOnInit() {
|
||||||
|
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||||
|
|
||||||
|
this.impersonationStorageService
|
||||||
|
.onChangeHasImpersonation()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((aId) => {
|
||||||
|
this.hasImpersonationId = !!aId;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.dataService
|
||||||
|
.fetchInvestments()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((response) => {
|
||||||
|
this.investments = response;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.userService.stateChanged
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((state) => {
|
||||||
|
if (state?.user) {
|
||||||
|
this.user = state.user;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="investment-chart row">
|
||||||
|
<div class="col-lg">
|
||||||
|
<h3 class="d-flex justify-content-center mb-3" i18n>Analysis</h3>
|
||||||
|
<mat-card class="mb-3">
|
||||||
|
<mat-card-header>
|
||||||
|
<mat-card-title class="align-items-center d-flex" i18n
|
||||||
|
>Timeline</mat-card-title
|
||||||
|
>
|
||||||
|
</mat-card-header>
|
||||||
|
<mat-card-content>
|
||||||
|
<gf-investment-chart
|
||||||
|
class="h-100"
|
||||||
|
[investments]="investments"
|
||||||
|
></gf-investment-chart>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -2,10 +2,6 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module';
|
import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module';
|
||||||
import { PortfolioProportionChartModule } from '@ghostfolio/client/components/portfolio-proportion-chart/portfolio-proportion-chart.module';
|
|
||||||
import { GfPositionsTableModule } from '@ghostfolio/client/components/positions-table/positions-table.module';
|
|
||||||
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
|
||||||
import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module';
|
|
||||||
|
|
||||||
import { AnalysisPageRoutingModule } from './analysis-page-routing.module';
|
import { AnalysisPageRoutingModule } from './analysis-page-routing.module';
|
||||||
import { AnalysisPageComponent } from './analysis-page.component';
|
import { AnalysisPageComponent } from './analysis-page.component';
|
||||||
@ -17,11 +13,7 @@ import { AnalysisPageComponent } from './analysis-page.component';
|
|||||||
AnalysisPageRoutingModule,
|
AnalysisPageRoutingModule,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
GfInvestmentChartModule,
|
GfInvestmentChartModule,
|
||||||
GfPositionsTableModule,
|
MatCardModule
|
||||||
GfToggleModule,
|
|
||||||
GfWorldMapChartModule,
|
|
||||||
MatCardModule,
|
|
||||||
PortfolioProportionChartModule
|
|
||||||
],
|
],
|
||||||
providers: [],
|
providers: [],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
@ -0,0 +1,9 @@
|
|||||||
|
:host {
|
||||||
|
.investment-chart {
|
||||||
|
.mat-card {
|
||||||
|
.mat-card-content {
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,14 +2,14 @@ import { NgModule } from '@angular/core';
|
|||||||
import { RouterModule, Routes } from '@angular/router';
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||||
|
|
||||||
import { ToolsPageComponent } from './tools-page.component';
|
import { PortfolioPageComponent } from './portfolio-page.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{ path: '', component: ToolsPageComponent, canActivate: [AuthGuard] }
|
{ path: '', component: PortfolioPageComponent, canActivate: [AuthGuard] }
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [RouterModule.forChild(routes)],
|
imports: [RouterModule.forChild(routes)],
|
||||||
exports: [RouterModule]
|
exports: [RouterModule]
|
||||||
})
|
})
|
||||||
export class ToolsPageRoutingModule {}
|
export class PortfolioPageRoutingModule {}
|
@ -0,0 +1,44 @@
|
|||||||
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
|
import { User } from '@ghostfolio/common/interfaces';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'gf-portfolio-page',
|
||||||
|
templateUrl: './portfolio-page.html',
|
||||||
|
styleUrls: ['./portfolio-page.scss']
|
||||||
|
})
|
||||||
|
export class PortfolioPageComponent implements OnDestroy, OnInit {
|
||||||
|
public user: User;
|
||||||
|
|
||||||
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
public constructor(
|
||||||
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
|
private userService: UserService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the controller
|
||||||
|
*/
|
||||||
|
public ngOnInit() {
|
||||||
|
this.userService.stateChanged
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((state) => {
|
||||||
|
if (state?.user) {
|
||||||
|
this.user = state.user;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
|
}
|
||||||
|
}
|
83
apps/client/src/app/pages/portfolio/portfolio-page.html
Normal file
83
apps/client/src/app/pages/portfolio/portfolio-page.html
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<div class="container">
|
||||||
|
<h3 class="d-flex justify-content-center mb-3" i18n>Portfolio</h3>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-12 col-md-6">
|
||||||
|
<mat-card class="mb-3">
|
||||||
|
<h4 i18n>Transactions</h4>
|
||||||
|
<p class="mb-0">Manage your transactions.</p>
|
||||||
|
<p class="text-right">
|
||||||
|
<a
|
||||||
|
color="primary"
|
||||||
|
i18n
|
||||||
|
mat-button
|
||||||
|
[routerLink]="['/portfolio', 'transactions']"
|
||||||
|
>
|
||||||
|
Open Transactions →
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
*ngIf="user?.settings?.viewMode === 'DEFAULT'"
|
||||||
|
class="col-xs-12 col-md-6"
|
||||||
|
>
|
||||||
|
<mat-card class="mb-3">
|
||||||
|
<h4 i18n>Allocations</h4>
|
||||||
|
<p class="mb-0">Check the allocations of your portfolio.</p>
|
||||||
|
<p class="text-right">
|
||||||
|
<a
|
||||||
|
color="primary"
|
||||||
|
i18n
|
||||||
|
mat-button
|
||||||
|
[routerLink]="['/portfolio', 'allocations']"
|
||||||
|
>
|
||||||
|
Open Allocations →
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div
|
||||||
|
*ngIf="user?.settings?.viewMode === 'DEFAULT'"
|
||||||
|
class="col-xs-12 col-md-6"
|
||||||
|
>
|
||||||
|
<mat-card class="mb-3">
|
||||||
|
<h4 i18n>Analysis</h4>
|
||||||
|
<p class="mb-0">Ghostfolio Analysis visualizes your portfolio.</p>
|
||||||
|
<p class="text-right">
|
||||||
|
<a
|
||||||
|
color="primary"
|
||||||
|
i18n
|
||||||
|
mat-button
|
||||||
|
[routerLink]="['/portfolio', 'analysis']"
|
||||||
|
>
|
||||||
|
Open Analysis →
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
*ngIf="user?.settings?.viewMode === 'DEFAULT'"
|
||||||
|
class="col-xs-12 col-md-6"
|
||||||
|
>
|
||||||
|
<mat-card class="mb-3">
|
||||||
|
<h4 i18n>X-ray</h4>
|
||||||
|
<p class="mb-0">
|
||||||
|
Ghostfolio X-ray uses static analysis to identify potential issues and
|
||||||
|
risks in your portfolio.
|
||||||
|
</p>
|
||||||
|
<p class="text-right">
|
||||||
|
<a
|
||||||
|
color="primary"
|
||||||
|
i18n
|
||||||
|
mat-button
|
||||||
|
[routerLink]="['/portfolio', 'report']"
|
||||||
|
>
|
||||||
|
Open X-ray →
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -4,20 +4,20 @@ import { MatButtonModule } from '@angular/material/button';
|
|||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
import { ToolsPageRoutingModule } from './tools-page-routing.module';
|
import { PortfolioPageRoutingModule } from './portfolio-page-routing.module';
|
||||||
import { ToolsPageComponent } from './tools-page.component';
|
import { PortfolioPageComponent } from './portfolio-page.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [ToolsPageComponent],
|
declarations: [PortfolioPageComponent],
|
||||||
exports: [],
|
exports: [],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
RouterModule,
|
PortfolioPageRoutingModule,
|
||||||
ToolsPageRoutingModule
|
RouterModule
|
||||||
],
|
],
|
||||||
providers: [],
|
providers: [],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
export class ToolsPageModule {}
|
export class PortfolioPageModule {}
|
@ -9,6 +9,7 @@ import { FormControl, Validators } from '@angular/forms';
|
|||||||
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||||
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { Currency } from '@prisma/client';
|
import { Currency } from '@prisma/client';
|
||||||
import { Observable, Subject } from 'rxjs';
|
import { Observable, Subject } from 'rxjs';
|
||||||
import {
|
import {
|
||||||
@ -19,7 +20,6 @@ import {
|
|||||||
takeUntil
|
takeUntil
|
||||||
} from 'rxjs/operators';
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
import { DataService } from '../../../services/data.service';
|
|
||||||
import { CreateOrUpdateTransactionDialogParams } from './interfaces/interfaces';
|
import { CreateOrUpdateTransactionDialogParams } from './interfaces/interfaces';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
@ -166,7 +166,7 @@
|
|||||||
color="primary"
|
color="primary"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[disabled]="!(addTransactionForm.form.valid && data.transaction.symbol)"
|
[disabled]="!(addTransactionForm.form.valid && data.transaction.currency && data.transaction.symbol)"
|
||||||
[mat-dialog-close]="data"
|
[mat-dialog-close]="data"
|
||||||
>
|
>
|
||||||
Save
|
Save
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user