Compare commits

...

47 Commits

Author SHA1 Message Date
695c378b48 Release 1.43.0 (#316) 2021-08-24 21:31:19 +02:00
fe975945d1 Feature/add fallback for loading currencies (#315)
* Add fallback for loading currencies

* Update changelog
2021-08-24 21:09:02 +02:00
d8782b0d4c Feature/automate countries for stocks in symbol profile data (#314)
* Automate countries for stocks in symbol profile data

* Update changelog
2021-08-24 20:24:18 +02:00
e14f08a8fb Release 1.42.0 (#313) 2021-08-22 22:37:44 +02:00
72c065a59d Feature/introduce asset sub class (#312)
* Introduce asset sub class

* Update changelog
2021-08-22 22:19:10 +02:00
98dac4052a Feature/add subscription type to the admin user table (#311)
* Add the subscription type to the user table in the admin control panel

* Update changelog
2021-08-22 22:11:05 +02:00
2083d28d02 Feature/minor improvements in the page components (#310)
* Move permissions to constructor

* Sort imports
2021-08-22 10:25:34 +02:00
addd5c36d9 Release 1.41.0 (#309) 2021-08-21 15:35:59 +02:00
aad8f77093 Feature/improve allocations by account (#308)
* Improve allocations by account

* Eliminate accounts from PortfolioPosition

* Ignore cash assets in the allocation chart by sector, continent and country

* Add missing accounts to portfolio details

* Update changelog
2021-08-21 15:03:55 +02:00
a904208d06 Feature/improve table styling (#307)
* Improve table styling

* Update changelog
2021-08-21 14:56:50 +02:00
2733b78044 Minor improvement (#306) 2021-08-21 14:56:11 +02:00
b43b515df1 Feature/add link to system status page (#305)
* Add link to system status page

* Update changelog
2021-08-21 08:57:12 +02:00
70e14b4d3c Feature/improve restricted view mode (#304)
* Improve wording and padding

* Update changelog
2021-08-20 20:58:33 +02:00
0f7d1b7d59 Release 1.40.0 (#303) 2021-08-19 21:56:21 +02:00
c2ab6a6c44 Feature/improve portfolio details endpoint (#302)
* Make details endpoint fault tolerant (do not throw error)

* Update changelog
2021-08-19 21:44:10 +02:00
c71a4c078e Bugfix/convert g bp to gbp in yahoo finance service (#301)
* Fix currency inconsistency in the yahoo finance service (GBp to GBP)

* Update changelog
2021-08-18 18:22:01 +02:00
e17b217032 Bugfix/fix issue on buy date in position detail dialog (#297)
* Fix issue on buy date

* Update changelog
2021-08-17 21:31:32 +02:00
408e08d43c Bugfix/fix node engine version mismatch (#299)
* Fix node engine version mismatch

* Update changelog
2021-08-17 20:32:12 +02:00
2fa324702f Release 1.39.0 (#296) 2021-08-16 21:57:51 +02:00
05b0efef82 Feature/add restricted view (#295)
* Add restricted view

* Update changelog
2021-08-16 21:40:29 +02:00
7c91727eb1 Feature/restructure allocations page (#294)
* Restructure allocations page

* Update changelog
2021-08-15 09:55:46 +02:00
0ee2258af8 Feature/improve impersonation mode (#293)
* Improve the impersonation mode

* Update changelog
2021-08-14 19:15:26 +02:00
308b218487 introduce basic module structure for data provider (#278)
* introduce basic module structure for data provider

* introduce DataGatheringModule

* introduce ExchangeRateDataModule

* introduce ImpersonationModule

* move RulesService

* cleanup portfolio module

* Sort imports

Co-authored-by: Valentin Zickner <github@zickner.ch>
Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2021-08-14 16:55:40 +02:00
26694adb8e Release 1.38.0 (#292) 2021-08-14 11:25:50 +02:00
77936e3bf3 Feature/improve users table (#291)
* Improve users table
  * Engagement / Day
  * Registration

* Update changelog
2021-08-14 11:12:08 +02:00
13090bf6b5 Feature/add overview menu item on mobile (#290)
* Add overview menu item

* Update changelog
2021-08-14 11:09:53 +02:00
b898c0678d Feature/refactor exchange rate service (#289)
* Refactor exchange rate service

* Update changelog
2021-08-14 11:06:21 +02:00
3330ae70b6 Release 1.37.0 (#288) 2021-08-13 20:49:05 +02:00
96a615dc5d Bugfix/only allow supported currencies in symbol search (#287)
* Only allow supported currencies in symbol search

* Update changelog
2021-08-13 20:29:57 +02:00
98f44323da Feature/improve usability of tabs on home page (#283)
* Improve usability: lazy load endpoints on tab change

* Feature/improve portfolio summary (#285)

* Update changelog
2021-08-13 19:26:48 +02:00
8adacd9760 Feature/upgrade angular material css vars to version 2.1.2 (#282)
* Upgrade angular-material-css-vars from version 2.1.0 to 2.1.2

* Update changelog
2021-08-13 19:05:22 +02:00
908aba170d Fix position chart for missing historical data (#284)
* Fix position chart for missing historical data

* Update changelog
2021-08-12 23:30:04 +02:00
d599797a65 Release 1.36.0 (#281) 2021-08-09 22:06:19 +02:00
8ac1272a9d Feature/eliminate name from scraper config (#277)
* Eliminate name from scraper config

* Update changelog
2021-08-09 21:33:58 +02:00
0a85a56c67 Respect cash balance in allocations, do not hide cryptocurrency holdings (#280)
* Respect cash balance in allocations, do not hide cryptocurrency holdings

* Update changelog
2021-08-09 21:26:41 +02:00
4ad5590838 Feature/improve data gathering (#276)
* Improve data gathering
  * Refactoring
  * On server restart, only reset if hanging in LOCKED_DATA_GATHERING state

* Update changelog
2021-08-09 21:11:35 +02:00
5b8af68e71 Release 1.35.0 (#279) 2021-08-08 21:23:48 +02:00
80d043729d Feature/replace type with asset class (#274)
* Improved the asset classification
  * Add assetClass to symbolProfile
  * Remove type from position

* Update changelog
2021-08-08 19:27:58 +02:00
178166d86b Extend investment chart by three months (#273) 2021-08-08 19:25:38 +02:00
37358fb480 Bugfix/add fallback if exchange service is not initialized (#264)
* Add fallback and log error

* Update changelog
2021-08-08 19:24:51 +02:00
616d601cf6 Improve wording (#275) 2021-08-08 12:20:30 +02:00
e88b889fdd Feature/optimize accounts table for mobile (#271)
* Optimize accounts table

* Update changelog
2021-08-08 09:24:47 +02:00
f6cdc4ff47 Feature/disable pagination of tabs (#272)
* Disable pagination

* Update changelog
2021-08-08 09:24:30 +02:00
818c40fc61 Feature/upgrade chart.js to version 3.5.0 (#268)
* Upgrade chart.js

* Update changelog
2021-08-08 08:33:55 +02:00
3589e72aea Harmonize prisma service (#266) 2021-08-07 22:38:07 +02:00
e68aa1fa68 Clean up imports (#267) 2021-08-07 22:37:39 +02:00
bb76ace95d Feature/improve support for draft transactions (#265)
* Improve support for draft transactions

* Update changelog
2021-08-07 20:52:55 +02:00
153 changed files with 2400 additions and 1710 deletions

View File

@ -5,6 +5,137 @@ 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.43.0 - 24.08.2021
### Added
- Extended the data management of symbol profile data by countries (automated for stocks)
- Added a fallback for initially loading currencies if historical data is not yet available
## 1.42.0 - 22.08.2021
### Added
- Added the subscription type to the users table of the admin control panel
- Introduced the sub classification of assets
### Todo
- Apply data migration (`yarn database:push`)
## 1.41.0 - 21.08.2021
### Added
- Added a link to the system status page
### Changed
- Improved the wording for the _Restricted View_: _Presenter View_
- Improved the styling of the tables
- Ignored cash assets in the allocation chart by sector, continent and country
### Fixed
- Fixed an issue in the allocation chart by account (wrong calculation)
- Fixed an issue in the allocation chart by account (missing cash accounts)
## 1.40.0 - 19.08.2021
### Changed
- Improved the fault tolerance of the portfolio details endpoint
### Fixed
- Fixed the node engine version mismatch in `package.json`
- Fixed an issue on the buy date in the position detail dialog
- Fixed an issue with the currency inconsistency in the _Yahoo Finance_ service (convert from `GBp` to `GBP`)
## 1.39.0 - 16.08.2021
### Added
- Added an option to hide absolute values like performances and quantities (_Restricted View_)
### Changed
- Restructured the allocations page
### Fixed
- Fixed an issue with the performance in the portfolio summary tab on the home page (impersonation mode)
- Fixed various values in the impersonation mode which have not been nullified
### Removed
- Removed the current net performance
- Removed the read foreign portfolio permission
### Todo
- Apply data migration (`yarn database:push`)
## 1.38.0 - 14.08.2021
### Added
- Added the overview menu item on mobile
### Changed
- Refactored the exchange rate service
- Improved the users table in the admin control panel
## 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 ## 1.34.0 - 07.08.2021
### Changed ### Changed

View File

@ -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,

View File

@ -1,3 +1,4 @@
import { UserService } from '@ghostfolio/api/app/user/user.service';
import { nullifyValuesInObjects } from '@ghostfolio/api/helper/object.helper'; import { nullifyValuesInObjects } from '@ghostfolio/api/helper/object.helper';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
import { import {
@ -33,7 +34,8 @@ export class AccountController {
public constructor( public constructor(
private readonly accountService: AccountService, private readonly accountService: AccountService,
private readonly impersonationService: ImpersonationService, private readonly impersonationService: ImpersonationService,
@Inject(REQUEST) private readonly request: RequestWithUser @Inject(REQUEST) private readonly request: RequestWithUser,
private readonly userService: UserService
) {} ) {}
@Delete(':id') @Delete(':id')
@ -84,25 +86,22 @@ export class AccountController {
public async getAllAccounts( public async getAllAccounts(
@Headers('impersonation-id') impersonationId @Headers('impersonation-id') impersonationId
): Promise<AccountModel[]> { ): Promise<AccountModel[]> {
const impersonationUserId = await this.impersonationService.validateImpersonationId( const impersonationUserId =
await this.impersonationService.validateImpersonationId(
impersonationId, impersonationId,
this.request.user.id this.request.user.id
); );
let accounts = await this.accountService.accounts({ let accounts = await this.accountService.getAccounts(
include: { Order: true, Platform: true }, impersonationUserId || this.request.user.id
orderBy: { name: 'asc' }, );
where: { userId: impersonationUserId || this.request.user.id }
});
if ( if (
impersonationUserId && impersonationUserId ||
!hasPermission( this.userService.isRestrictedView(this.request.user)
getPermissions(this.request.user.role),
permissions.readForeignPortfolio
)
) { ) {
accounts = nullifyValuesInObjects(accounts, [ accounts = nullifyValuesInObjects(accounts, [
'balance',
'fee', 'fee',
'quantity', 'quantity',
'unitPrice' 'unitPrice'

View File

@ -1,32 +1,26 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service'; import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module';
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service'; import { ImpersonationModule } from '@ghostfolio/api/services/impersonation.module';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { RedisCacheModule } from '../redis-cache/redis-cache.module';
import { AccountController } from './account.controller'; import { AccountController } from './account.controller';
import { AccountService } from './account.service'; import { AccountService } from './account.service';
@Module({ @Module({
imports: [RedisCacheModule], imports: [
ConfigurationModule,
DataProviderModule,
ExchangeRateDataModule,
ImpersonationModule,
RedisCacheModule,
PrismaModule,
UserModule
],
controllers: [AccountController], controllers: [AccountController],
providers: [ providers: [AccountService]
AccountService,
AlphaVantageService,
ConfigurationService,
DataProviderService,
ExchangeRateDataService,
GhostfolioScraperApiService,
ImpersonationService,
PrismaService,
RakutenRapidApiService,
YahooFinanceService
]
}) })
export class AccountModule {} export class AccountModule {}

View File

@ -1,23 +1,21 @@
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 { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Account, Currency, Order, Prisma } from '@prisma/client'; import { Account, Currency, Order, Platform, 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
}); });
@ -43,10 +41,15 @@ export class AccountService {
cursor?: Prisma.AccountWhereUniqueInput; cursor?: Prisma.AccountWhereUniqueInput;
where?: Prisma.AccountWhereInput; where?: Prisma.AccountWhereInput;
orderBy?: Prisma.AccountOrderByInput; orderBy?: Prisma.AccountOrderByInput;
}): Promise<Account[]> { }): Promise<
(Account & {
Order?: Order[];
Platform?: Platform;
})[]
> {
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 +63,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,13 +72,27 @@ 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
}); });
} }
public async getAccounts(aUserId: string) {
const accounts = await this.accounts({
include: { Order: true, Platform: true },
orderBy: { name: 'asc' },
where: { userId: aUserId }
});
return accounts.map((account) => {
const result = { ...account, transactionCount: account.Order.length };
delete result.Order;
return result;
});
}
public async getCashDetails( public async getCashDetails(
aUserId: string, aUserId: string,
aCurrency: Currency aCurrency: Currency
@ -105,7 +122,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
}); });

View File

@ -1,31 +1,25 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service'; import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module';
import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service'; import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AdminController } from './admin.controller'; import { AdminController } from './admin.controller';
import { AdminService } from './admin.service'; import { AdminService } from './admin.service';
@Module({ @Module({
imports: [], imports: [
ConfigurationModule,
DataGatheringModule,
DataProviderModule,
ExchangeRateDataModule,
PrismaModule,
SubscriptionModule
],
controllers: [AdminController], controllers: [AdminController],
providers: [ providers: [AdminService],
AdminService, exports: [AdminService]
AlphaVantageService,
ConfigurationService,
DataGatheringService,
DataProviderService,
ExchangeRateDataService,
GhostfolioScraperApiService,
PrismaService,
RakutenRapidApiService,
YahooFinanceService
]
}) })
export class AdminModule {} export class AdminModule {}

View File

@ -1,14 +1,21 @@
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
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';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Currency } from '@prisma/client'; import { Currency } from '@prisma/client';
import { differenceInDays } from 'date-fns';
@Injectable() @Injectable()
export class AdminService { export class AdminService {
public constructor( public constructor(
private exchangeRateDataService: ExchangeRateDataService, private readonly configurationService: ConfigurationService,
private prisma: PrismaService private readonly dataGatheringService: DataGatheringService,
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly prismaService: PrismaService,
private readonly subscriptionService: SubscriptionService
) {} ) {}
public async get(): Promise<AdminData> { public async get(): Promise<AdminData> {
@ -61,24 +68,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';
@ -87,8 +92,8 @@ export class AdminService {
return null; return null;
} }
private async getUsersWithAnalytics() { private async getUsersWithAnalytics(): Promise<AdminData['users']> {
return await this.prisma.user.findMany({ const usersWithAnalytics = await this.prismaService.user.findMany({
orderBy: { orderBy: {
Analytics: { Analytics: {
updatedAt: 'desc' updatedAt: 'desc'
@ -106,7 +111,8 @@ export class AdminService {
} }
}, },
createdAt: true, createdAt: true,
id: true id: true,
Subscription: true
}, },
take: 30, take: 30,
where: { where: {
@ -115,5 +121,30 @@ export class AdminService {
} }
} }
}); });
return usersWithAnalytics.map(
({ _count, alias, Analytics, createdAt, id, Subscription }) => {
const daysSinceRegistration =
differenceInDays(new Date(), createdAt) + 1;
const engagement = Analytics.activityCount / daysSinceRegistration;
const subscription = this.configurationService.get(
'ENABLE_FEATURE_SUBSCRIPTION'
)
? this.subscriptionService.getSubscription(Subscription)
: undefined;
return {
alias,
createdAt,
engagement,
id,
subscription,
accountCount: _count.Account || 0,
lastActivity: Analytics.updatedAt,
transactionCount: _count.Order || 0
};
}
);
} }
} }

View File

@ -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' }]
}
});
} }
} }
} }

View File

@ -1,35 +1,30 @@
import { join } from 'path'; import { join } from 'path';
import { AuthDeviceModule } from '@ghostfolio/api/app/auth-device/auth-device.module'; import { AuthDeviceModule } from '@ghostfolio/api/app/auth-device/auth-device.module';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
import { CronService } from '@ghostfolio/api/services/cron.service';
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { ScheduleModule } from '@nestjs/schedule'; import { ScheduleModule } from '@nestjs/schedule';
import { ServeStaticModule } from '@nestjs/serve-static'; import { ServeStaticModule } from '@nestjs/serve-static';
import { ConfigurationService } from '../services/configuration.service';
import { CronService } from '../services/cron.service';
import { DataGatheringService } from '../services/data-gathering.service';
import { DataProviderService } from '../services/data-provider.service';
import { AlphaVantageService } from '../services/data-provider/alpha-vantage/alpha-vantage.service';
import { GhostfolioScraperApiService } from '../services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
import { RakutenRapidApiService } from '../services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
import { YahooFinanceService } from '../services/data-provider/yahoo-finance/yahoo-finance.service';
import { ExchangeRateDataService } from '../services/exchange-rate-data.service';
import { PrismaService } from '../services/prisma.service';
import { AccessModule } from './access/access.module'; import { AccessModule } from './access/access.module';
import { AccountModule } from './account/account.module'; import { AccountModule } from './account/account.module';
import { AdminModule } from './admin/admin.module'; import { AdminModule } from './admin/admin.module';
import { AppController } from './app.controller'; import { AppController } from './app.controller';
import { AuthModule } from './auth/auth.module'; import { AuthModule } from './auth/auth.module';
import { CacheModule } from './cache/cache.module'; import { CacheModule } from './cache/cache.module';
import { CoreModule } from './core/core.module';
import { ExperimentalModule } from './experimental/experimental.module'; import { ExperimentalModule } from './experimental/experimental.module';
import { ExportModule } from './export/export.module'; import { ExportModule } from './export/export.module';
import { ImportModule } from './import/import.module'; import { ImportModule } from './import/import.module';
import { InfoModule } from './info/info.module'; import { InfoModule } from './info/info.module';
import { OrderModule } from './order/order.module'; import { OrderModule } from './order/order.module';
import { PortfolioModule } from './portfolio/portfolio.module'; import { PortfolioModule } from './portfolio/portfolio.module';
import { RedisCacheModule } from './redis-cache/redis-cache.module';
import { SubscriptionModule } from './subscription/subscription.module'; import { SubscriptionModule } from './subscription/subscription.module';
import { SymbolModule } from './symbol/symbol.module'; import { SymbolModule } from './symbol/symbol.module';
import { UserModule } from './user/user.module'; import { UserModule } from './user/user.module';
@ -43,13 +38,17 @@ import { UserModule } from './user/user.module';
AuthModule, AuthModule,
CacheModule, CacheModule,
ConfigModule.forRoot(), ConfigModule.forRoot(),
CoreModule, ConfigurationModule,
DataGatheringModule,
DataProviderModule,
ExchangeRateDataModule,
ExperimentalModule, ExperimentalModule,
ExportModule, ExportModule,
ImportModule, ImportModule,
InfoModule, InfoModule,
OrderModule, OrderModule,
PortfolioModule, PortfolioModule,
PrismaModule,
RedisCacheModule, RedisCacheModule,
ScheduleModule.forRoot(), ScheduleModule.forRoot(),
ServeStaticModule.forRoot({ ServeStaticModule.forRoot({
@ -71,17 +70,6 @@ import { UserModule } from './user/user.module';
UserModule UserModule
], ],
controllers: [AppController], controllers: [AppController],
providers: [ providers: [CronService]
AlphaVantageService,
ConfigurationService,
CronService,
DataGatheringService,
DataProviderService,
ExchangeRateDataService,
GhostfolioScraperApiService,
PrismaService,
RakutenRapidApiService,
YahooFinanceService
]
}) })
export class AppModule {} export class AppModule {}

View File

@ -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
}); });
} }

View File

@ -1,11 +1,12 @@
import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service'; import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service';
import { WebAuthService } from '@ghostfolio/api/app/auth/web-auth.service'; import { WebAuthService } from '@ghostfolio/api/app/auth/web-auth.service';
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
import { UserService } from '@ghostfolio/api/app/user/user.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.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';
import { UserService } from '../user/user.service';
import { AuthController } from './auth.controller'; import { AuthController } from './auth.controller';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { GoogleStrategy } from './google.strategy'; import { GoogleStrategy } from './google.strategy';
@ -17,7 +18,8 @@ import { JwtStrategy } from './jwt.strategy';
JwtModule.register({ JwtModule.register({
secret: process.env.JWT_SECRET_KEY, secret: process.env.JWT_SECRET_KEY,
signOptions: { expiresIn: '180 days' } signOptions: { expiresIn: '180 days' }
}) }),
SubscriptionModule
], ],
providers: [ providers: [
AuthDeviceService, AuthDeviceService,

View File

@ -1,8 +1,8 @@
import { UserService } from '@ghostfolio/api/app/user/user.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
import { UserService } from '../user/user.service';
import { ValidateOAuthLoginParams } from './interfaces/interfaces'; import { ValidateOAuthLoginParams } from './interfaces/interfaces';
@Injectable() @Injectable()

View File

@ -1,16 +1,15 @@
import { UserService } from '@ghostfolio/api/app/user/user.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { Injectable, UnauthorizedException } from '@nestjs/common'; import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport'; import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt'; import { ExtractJwt, Strategy } from 'passport-jwt';
import { UserService } from '../user/user.service';
@Injectable() @Injectable()
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 +23,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 }

View File

@ -1,5 +1,6 @@
import { AuthDeviceDto } from '@ghostfolio/api/app/auth-device/auth-device.dto'; import { AuthDeviceDto } from '@ghostfolio/api/app/auth-device/auth-device.dto';
import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service'; import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service';
import { UserService } from '@ghostfolio/api/app/user/user.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { RequestWithUser } from '@ghostfolio/common/types'; import { RequestWithUser } from '@ghostfolio/common/types';
import { import {
@ -22,7 +23,6 @@ import {
verifyAttestationResponse verifyAttestationResponse
} from '@simplewebauthn/server'; } from '@simplewebauthn/server';
import { UserService } from '../user/user.service';
import { import {
AssertionCredentialJSON, AssertionCredentialJSON,
AttestationCredentialJSON AttestationCredentialJSON

View File

@ -1,11 +1,10 @@
import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { RequestWithUser } from '@ghostfolio/common/types'; import { RequestWithUser } from '@ghostfolio/common/types';
import { Controller, Inject, Post, UseGuards } from '@nestjs/common'; import { Controller, Inject, Post, UseGuards } from '@nestjs/common';
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { RedisCacheService } from '../redis-cache/redis-cache.service';
import { CacheService } from './cache.service';
@Controller('cache') @Controller('cache')
export class CacheController { export class CacheController {
public constructor( public constructor(
@ -21,6 +20,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();
} }
} }

View File

@ -1,13 +1,30 @@
import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.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 { RedisCacheModule } from '../redis-cache/redis-cache.module';
import { CacheController } from './cache.controller'; import { CacheController } from './cache.controller';
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 {}

View File

@ -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;
} }

View File

@ -1,30 +0,0 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.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 { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { Module } from '@nestjs/common';
import { CurrentRateService } from './current-rate.service';
import { MarketDataService } from './market-data.service';
@Module({
imports: [],
controllers: [],
providers: [
AlphaVantageService,
ConfigurationService,
CurrentRateService,
DataProviderService,
ExchangeRateDataService,
GhostfolioScraperApiService,
MarketDataService,
PrismaService,
RakutenRapidApiService,
YahooFinanceService
]
})
export class CoreModule {}

View File

@ -1,6 +0,0 @@
import { TransactionPointSymbol } from '@ghostfolio/api/app/core/interfaces/transaction-point-symbol.interface';
export interface TransactionPoint {
date: string;
items: TransactionPointSymbol[];
}

View File

@ -1,34 +1,23 @@
import { AccountService } from '@ghostfolio/api/app/account/account.service'; import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module';
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service'; import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
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 { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { RulesService } from '@ghostfolio/api/services/rules.service';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ExperimentalController } from './experimental.controller'; import { ExperimentalController } from './experimental.controller';
import { ExperimentalService } from './experimental.service'; import { ExperimentalService } from './experimental.service';
@Module({ @Module({
imports: [RedisCacheModule], imports: [
ConfigurationModule,
DataProviderModule,
ExchangeRateDataModule,
RedisCacheModule,
PrismaModule
],
controllers: [ExperimentalController], controllers: [ExperimentalController],
providers: [ providers: [AccountService, ExperimentalService]
AccountService,
AlphaVantageService,
ConfigurationService,
DataProviderService,
ExchangeRateDataService,
ExperimentalService,
GhostfolioScraperApiService,
PrismaService,
RakutenRapidApiService,
RulesService,
YahooFinanceService
]
}) })
export class ExperimentalModule {} export class ExperimentalModule {}

View File

@ -1,8 +1,7 @@
import { AccountService } from '@ghostfolio/api/app/account/account.service'; import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.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 { RulesService } from '@ghostfolio/api/services/rules.service';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
@Injectable() @Injectable()
@ -11,12 +10,11 @@ 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
) {} ) {}
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 }

View File

@ -1,32 +1,23 @@
import { CacheService } from '@ghostfolio/api/app/cache/cache.service'; import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service'; import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service'; import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
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 { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ExportController } from './export.controller'; import { ExportController } from './export.controller';
import { ExportService } from './export.service'; import { ExportService } from './export.service';
@Module({ @Module({
imports: [RedisCacheModule], imports: [
ConfigurationModule,
DataGatheringModule,
DataProviderModule,
PrismaModule,
RedisCacheModule
],
controllers: [ExportController], controllers: [ExportController],
providers: [ providers: [CacheService, ExportService]
AlphaVantageService,
CacheService,
ConfigurationService,
DataGatheringService,
DataProviderService,
ExportService,
GhostfolioScraperApiService,
PrismaService,
RakutenRapidApiService,
YahooFinanceService
]
}) })
export class ExportModule {} export class ExportModule {}

View File

@ -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,

View File

@ -1,34 +1,24 @@
import { CacheService } from '@ghostfolio/api/app/cache/cache.service'; import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
import { OrderService } from '@ghostfolio/api/app/order/order.service'; import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service'; import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service'; import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
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 { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ImportController } from './import.controller'; import { ImportController } from './import.controller';
import { ImportService } from './import.service'; import { ImportService } from './import.service';
@Module({ @Module({
imports: [RedisCacheModule], imports: [
ConfigurationModule,
DataGatheringModule,
DataProviderModule,
PrismaModule,
RedisCacheModule
],
controllers: [ImportController], controllers: [ImportController],
providers: [ providers: [CacheService, ImportService, OrderService]
AlphaVantageService,
CacheService,
ConfigurationService,
DataGatheringService,
DataProviderService,
GhostfolioScraperApiService,
ImportService,
OrderService,
PrismaService,
RakutenRapidApiService,
YahooFinanceService
]
}) })
export class ImportModule {} export class ImportModule {}

View File

@ -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
);
} }
} }
} }

View File

@ -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 { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.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 {}

View File

@ -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' }
}); });

View File

@ -1,3 +1,4 @@
import { UserService } from '@ghostfolio/api/app/user/user.service';
import { nullifyValuesInObjects } from '@ghostfolio/api/helper/object.helper'; import { nullifyValuesInObjects } from '@ghostfolio/api/helper/object.helper';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
import { import {
@ -34,7 +35,8 @@ export class OrderController {
public constructor( public constructor(
private readonly impersonationService: ImpersonationService, private readonly impersonationService: ImpersonationService,
private readonly orderService: OrderService, private readonly orderService: OrderService,
@Inject(REQUEST) private readonly request: RequestWithUser @Inject(REQUEST) private readonly request: RequestWithUser,
private readonly userService: UserService
) {} ) {}
@Delete(':id') @Delete(':id')
@ -52,15 +54,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()
@ -92,11 +91,8 @@ export class OrderController {
}); });
if ( if (
impersonationUserId && impersonationUserId ||
!hasPermission( this.userService.isRestrictedView(this.request.user)
getPermissions(this.request.user.role),
permissions.readForeignPortfolio
)
) { ) {
orders = nullifyValuesInObjects(orders, ['fee', 'quantity', 'unitPrice']); orders = nullifyValuesInObjects(orders, ['fee', 'quantity', 'unitPrice']);
} }
@ -135,8 +131,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: {
@ -159,9 +154,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')
@ -198,8 +191,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,
@ -216,8 +208,6 @@ export class OrderController {
userId: this.request.user.id userId: this.request.user.id
} }
} }
}, });
this.request.user.id
);
} }
} }

View File

@ -1,34 +1,28 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service'; import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service'; import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service'; import { ImpersonationModule } from '@ghostfolio/api/services/impersonation.module';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service'; import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { CacheService } from '../cache/cache.service';
import { RedisCacheModule } from '../redis-cache/redis-cache.module';
import { OrderController } from './order.controller'; import { OrderController } from './order.controller';
import { OrderService } from './order.service'; import { OrderService } from './order.service';
@Module({ @Module({
imports: [RedisCacheModule], imports: [
ConfigurationModule,
DataGatheringModule,
DataProviderModule,
ImpersonationModule,
PrismaModule,
RedisCacheModule,
UserModule
],
controllers: [OrderController], controllers: [OrderController],
providers: [ providers: [CacheService, OrderService],
AlphaVantageService, exports: [OrderService]
CacheService,
ConfigurationService,
DataGatheringService,
DataProviderService,
GhostfolioScraperApiService,
ImpersonationService,
OrderService,
PrismaService,
RakutenRapidApiService,
YahooFinanceService
]
}) })
export class OrderModule {} export class OrderModule {}

View File

@ -1,3 +1,4 @@
import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.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 { OrderWithAccount } from '@ghostfolio/common/types'; import { OrderWithAccount } from '@ghostfolio/common/types';
@ -5,22 +6,18 @@ import { Injectable } from '@nestjs/common';
import { DataSource, Order, Prisma } from '@prisma/client'; 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 { 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 +32,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 +42,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 +58,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 +118,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
}); });
} }

View File

@ -1,8 +1,8 @@
import { CurrentRateService } from '@ghostfolio/api/app/core/current-rate.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/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 { Currency, MarketData } from '@prisma/client'; import { Currency, MarketData } from '@prisma/client';
import { CurrentRateService } from './current-rate.service';
import { MarketDataService } from './market-data.service'; import { MarketDataService } from './market-data.service';
jest.mock('./market-data.service', () => { jest.mock('./market-data.service', () => {

View File

@ -1,13 +1,13 @@
import { GetValueObject } from '@ghostfolio/api/app/core/interfaces/get-value-object.interface'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { GetValueParams } from '@ghostfolio/api/app/core/interfaces/get-value-params.interface';
import { GetValuesParams } from '@ghostfolio/api/app/core/interfaces/get-values-params.interface';
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 { resetHours } from '@ghostfolio/common/helper'; import { resetHours } from '@ghostfolio/common/helper';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { isBefore, isToday } from 'date-fns'; import { isBefore, isToday } from 'date-fns';
import { flatten } from 'lodash'; import { flatten } from 'lodash';
import { GetValueObject } from './interfaces/get-value-object.interface';
import { GetValueParams } from './interfaces/get-value-params.interface';
import { GetValuesParams } from './interfaces/get-values-params.interface';
import { MarketDataService } from './market-data.service'; import { MarketDataService } from './market-data.service';
@Injectable() @Injectable()

View File

@ -1,6 +1,7 @@
import { DateQuery } from '@ghostfolio/api/app/core/interfaces/date-query.interface';
import { Currency } from '@prisma/client'; import { Currency } from '@prisma/client';
import { DateQuery } from './date-query.interface';
export interface GetValuesParams { export interface GetValuesParams {
currencies: { [symbol: string]: Currency }; currencies: { [symbol: string]: Currency };
dateQuery: DateQuery; dateQuery: DateQuery;

View File

@ -0,0 +1,6 @@
import { TransactionPointSymbol } from './transaction-point-symbol.interface';
export interface TransactionPoint {
date: string;
items: TransactionPointSymbol[];
}

View File

@ -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'

View File

@ -1,11 +1,3 @@
import { CurrentRateService } from '@ghostfolio/api/app/core/current-rate.service';
import { GetValueParams } from '@ghostfolio/api/app/core/interfaces/get-value-params.interface';
import { GetValuesParams } from '@ghostfolio/api/app/core/interfaces/get-values-params.interface';
import { PortfolioOrder } from '@ghostfolio/api/app/core/interfaces/portfolio-order.interface';
import { TimelinePeriod } from '@ghostfolio/api/app/core/interfaces/timeline-period.interface';
import { TimelineSpecification } from '@ghostfolio/api/app/core/interfaces/timeline-specification.interface';
import { TransactionPoint } from '@ghostfolio/api/app/core/interfaces/transaction-point.interface';
import { PortfolioCalculator } from '@ghostfolio/api/app/core/portfolio-calculator';
import { OrderType } from '@ghostfolio/api/models/order-type'; import { OrderType } from '@ghostfolio/api/models/order-type';
import { parseDate, resetHours } from '@ghostfolio/common/helper'; import { parseDate, resetHours } from '@ghostfolio/common/helper';
import { Currency } from '@prisma/client'; import { Currency } from '@prisma/client';
@ -18,6 +10,15 @@ import {
isSameDay isSameDay
} from 'date-fns'; } from 'date-fns';
import { CurrentRateService } from './current-rate.service';
import { GetValueParams } from './interfaces/get-value-params.interface';
import { GetValuesParams } from './interfaces/get-values-params.interface';
import { PortfolioOrder } from './interfaces/portfolio-order.interface';
import { TimelinePeriod } from './interfaces/timeline-period.interface';
import { TimelineSpecification } from './interfaces/timeline-specification.interface';
import { TransactionPoint } from './interfaces/transaction-point.interface';
import { PortfolioCalculator } from './portfolio-calculator';
function mockGetValue(symbol: string, date: Date) { function mockGetValue(symbol: string, date: Date) {
switch (symbol) { switch (symbol) {
case 'AMZN': case 'AMZN':
@ -67,7 +68,7 @@ function mockGetValue(symbol: string, date: Date) {
} }
} }
jest.mock('@ghostfolio/api/app/core/current-rate.service', () => { jest.mock('./current-rate.service', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => { CurrentRateService: jest.fn().mockImplementation(() => {

View File

@ -1,14 +1,3 @@
import { CurrentRateService } from '@ghostfolio/api/app/core/current-rate.service';
import { CurrentPositions } from '@ghostfolio/api/app/core/interfaces/current-positions.interface';
import { GetValueObject } from '@ghostfolio/api/app/core/interfaces/get-value-object.interface';
import { PortfolioOrder } from '@ghostfolio/api/app/core/interfaces/portfolio-order.interface';
import { TimelinePeriod } from '@ghostfolio/api/app/core/interfaces/timeline-period.interface';
import {
Accuracy,
TimelineSpecification
} from '@ghostfolio/api/app/core/interfaces/timeline-specification.interface';
import { TransactionPointSymbol } from '@ghostfolio/api/app/core/interfaces/transaction-point-symbol.interface';
import { TransactionPoint } from '@ghostfolio/api/app/core/interfaces/transaction-point.interface';
import { OrderType } from '@ghostfolio/api/models/order-type'; import { OrderType } from '@ghostfolio/api/models/order-type';
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
import { TimelinePosition } from '@ghostfolio/common/interfaces'; import { TimelinePosition } from '@ghostfolio/common/interfaces';
@ -27,6 +16,18 @@ import {
} from 'date-fns'; } from 'date-fns';
import { flatten } from 'lodash'; import { flatten } from 'lodash';
import { CurrentRateService } from './current-rate.service';
import { CurrentPositions } from './interfaces/current-positions.interface';
import { GetValueObject } from './interfaces/get-value-object.interface';
import { PortfolioOrder } from './interfaces/portfolio-order.interface';
import { TimelinePeriod } from './interfaces/timeline-period.interface';
import {
Accuracy,
TimelineSpecification
} from './interfaces/timeline-specification.interface';
import { TransactionPointSymbol } from './interfaces/transaction-point-symbol.interface';
import { TransactionPoint } from './interfaces/transaction-point.interface';
export class PortfolioCalculator { export class PortfolioCalculator {
private transactionPoints: TransactionPoint[]; private transactionPoints: TransactionPoint[];

View File

@ -1,22 +1,16 @@
import { UserService } from '@ghostfolio/api/app/user/user.service';
import { import {
hasNotDefinedValuesInObject, hasNotDefinedValuesInObject,
nullifyValuesInObject nullifyValuesInObject
} from '@ghostfolio/api/helper/object.helper'; } from '@ghostfolio/api/helper/object.helper';
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 { import {
PortfolioItem, PortfolioDetails,
PortfolioOverview,
PortfolioPerformance, PortfolioPerformance,
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 {
getPermissions,
hasPermission,
permissions
} from '@ghostfolio/common/permissions';
import { RequestWithUser } from '@ghostfolio/common/types'; import { RequestWithUser } from '@ghostfolio/common/types';
import { import {
Controller, Controller,
@ -45,12 +39,12 @@ import { PortfolioService } from './portfolio.service';
export class PortfolioController { export class PortfolioController {
public constructor( public constructor(
private readonly exchangeRateDataService: ExchangeRateDataService, private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly impersonationService: ImpersonationService, private readonly portfolioService: PortfolioService,
private portfolioService: PortfolioService, @Inject(REQUEST) private readonly request: RequestWithUser,
@Inject(REQUEST) private readonly request: RequestWithUser private readonly userService: UserService
) {} ) {}
@Get('/investments') @Get('investments')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async findAll( public async findAll(
@Headers('impersonation-id') impersonationId @Headers('impersonation-id') impersonationId
@ -60,11 +54,8 @@ export class PortfolioController {
); );
if ( if (
impersonationId && impersonationId ||
!hasPermission( this.userService.isRestrictedView(this.request.user)
getPermissions(this.request.user.role),
permissions.readForeignPortfolio
)
) { ) {
const maxInvestment = investments.reduce( const maxInvestment = investments.reduce(
(investment, item) => Math.max(investment, item.investment), (investment, item) => Math.max(investment, item.investment),
@ -105,11 +96,8 @@ export class PortfolioController {
} }
if ( if (
impersonationId && impersonationId ||
!hasPermission( this.userService.isRestrictedView(this.request.user)
getPermissions(this.request.user.role),
permissions.readForeignPortfolio
)
) { ) {
let maxValue = 0; let maxValue = 0;
@ -136,44 +124,25 @@ export class PortfolioController {
@Headers('impersonation-id') impersonationId, @Headers('impersonation-id') impersonationId,
@Query('range') range, @Query('range') range,
@Res() res: Response @Res() res: Response
): Promise<{ [symbol: string]: PortfolioPosition }> { ): Promise<PortfolioDetails> {
let details: { [symbol: string]: PortfolioPosition } = {}; const { accounts, holdings, hasErrors } =
await this.portfolioService.getDetails(impersonationId, range);
const impersonationUserId = if (hasErrors || hasNotDefinedValuesInObject(holdings)) {
await this.impersonationService.validateImpersonationId(
impersonationId,
this.request.user.id
);
try {
details = await this.portfolioService.getDetails(
impersonationUserId,
range
);
} catch (error) {
console.error(error);
res.status(StatusCodes.ACCEPTED);
}
if (hasNotDefinedValuesInObject(details)) {
res.status(StatusCodes.ACCEPTED); res.status(StatusCodes.ACCEPTED);
} }
if ( if (
impersonationId && impersonationId ||
!hasPermission( this.userService.isRestrictedView(this.request.user)
getPermissions(this.request.user.role),
permissions.readForeignPortfolio
)
) { ) {
const totalInvestment = Object.values(details) const totalInvestment = Object.values(holdings)
.map((portfolioPosition) => { .map((portfolioPosition) => {
return portfolioPosition.investment; return portfolioPosition.investment;
}) })
.reduce((a, b) => a + b, 0); .reduce((a, b) => a + b, 0);
const totalValue = Object.values(details) const totalValue = Object.values(holdings)
.map((portfolioPosition) => { .map((portfolioPosition) => {
return this.exchangeRateDataService.toCurrency( return this.exchangeRateDataService.toCurrency(
portfolioPosition.quantity * portfolioPosition.marketPrice, portfolioPosition.quantity * portfolioPosition.marketPrice,
@ -183,50 +152,21 @@ export class PortfolioController {
}) })
.reduce((a, b) => a + b, 0); .reduce((a, b) => a + b, 0);
for (const [symbol, portfolioPosition] of Object.entries(details)) { for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
portfolioPosition.grossPerformance = null; portfolioPosition.grossPerformance = null;
portfolioPosition.investment = portfolioPosition.investment =
portfolioPosition.investment / totalInvestment; portfolioPosition.investment / totalInvestment;
for (const [account, { current, original }] of Object.entries(
portfolioPosition.accounts
)) {
portfolioPosition.accounts[account].current = current / totalValue;
portfolioPosition.accounts[account].original =
original / totalInvestment;
}
portfolioPosition.quantity = null; portfolioPosition.quantity = null;
} }
for (const [name, { current, original }] of Object.entries(accounts)) {
accounts[name].current = current / totalValue;
accounts[name].original = original / totalInvestment;
}
} }
return <any>res.json(details); return <any>res.json({ accounts, holdings });
}
@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')
@ -247,15 +187,11 @@ export class PortfolioController {
let performance = performanceInformation.performance; let performance = performanceInformation.performance;
if ( if (
impersonationId && impersonationId ||
!hasPermission( this.userService.isRestrictedView(this.request.user)
getPermissions(this.request.user.role),
permissions.readForeignPortfolio
)
) { ) {
performance = nullifyValuesInObject(performance, [ performance = nullifyValuesInObject(performance, [
'currentGrossPerformance', 'currentGrossPerformance',
'currentNetPerformance',
'currentValue' 'currentValue'
]); ]);
} }
@ -279,9 +215,48 @@ export class PortfolioController {
res.status(StatusCodes.ACCEPTED); res.status(StatusCodes.ACCEPTED);
} }
if (
impersonationId ||
this.userService.isRestrictedView(this.request.user)
) {
result.positions = result.positions.map((position) => {
return nullifyValuesInObject(position, [
'grossPerformance',
'investment',
'quantity'
]);
});
}
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 ||
this.userService.isRestrictedView(this.request.user)
) {
summary = nullifyValuesInObject(summary, [
'cash',
'committedFunds',
'currentGrossPerformance',
'currentValue',
'fees',
'netWorth',
'totalBuy',
'totalSell'
]);
}
return summary;
}
@Get('position/:symbol') @Get('position/:symbol')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async getPosition( public async getPosition(
@ -295,13 +270,14 @@ export class PortfolioController {
if (position) { if (position) {
if ( if (
impersonationId && impersonationId ||
!hasPermission( this.userService.isRestrictedView(this.request.user)
getPermissions(this.request.user.role),
permissions.readForeignPortfolio
)
) { ) {
position = nullifyValuesInObject(position, ['grossPerformance']); position = nullifyValuesInObject(position, [
'grossPerformance',
'investment',
'quantity'
]);
} }
return position; return position;

View File

@ -1,50 +1,40 @@
import { AccountService } from '@ghostfolio/api/app/account/account.service'; import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { CacheService } from '@ghostfolio/api/app/cache/cache.service'; import { OrderModule } from '@ghostfolio/api/app/order/order.module';
import { CurrentRateService } from '@ghostfolio/api/app/core/current-rate.service'; import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { MarketDataService } from '@ghostfolio/api/app/core/market-data.service'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
import { OrderService } from '@ghostfolio/api/app/order/order.service'; import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { UserService } from '@ghostfolio/api/app/user/user.service'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ImpersonationModule } from '@ghostfolio/api/services/impersonation.module';
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service'; import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
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 { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.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 { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { CurrentRateService } from './current-rate.service';
import { MarketDataService } from './market-data.service';
import { PortfolioController } from './portfolio.controller'; import { PortfolioController } from './portfolio.controller';
import { PortfolioService } from './portfolio.service'; import { PortfolioService } from './portfolio.service';
import { RulesService } from './rules.service';
@Module({ @Module({
imports: [RedisCacheModule], imports: [
ConfigurationModule,
DataGatheringModule,
DataProviderModule,
ExchangeRateDataModule,
ImpersonationModule,
OrderModule,
PrismaModule,
UserModule
],
controllers: [PortfolioController], controllers: [PortfolioController],
providers: [ providers: [
AccountService, AccountService,
AlphaVantageService,
CacheService,
CurrentRateService, CurrentRateService,
ConfigurationService,
DataGatheringService,
DataProviderService,
ExchangeRateDataService,
GhostfolioScraperApiService,
ImpersonationService,
MarketDataService, MarketDataService,
OrderService,
PortfolioService, PortfolioService,
PrismaService,
RakutenRapidApiService,
RulesService, RulesService,
SymbolProfileService, SymbolProfileService
UserService,
YahooFinanceService
] ]
}) })
export class PortfolioModule {} export class PortfolioModule {}

View File

@ -1,10 +1,11 @@
import { AccountService } from '@ghostfolio/api/app/account/account.service'; import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { CurrentRateService } from '@ghostfolio/api/app/core/current-rate.service'; import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details.interface';
import { PortfolioOrder } from '@ghostfolio/api/app/core/interfaces/portfolio-order.interface';
import { TimelineSpecification } from '@ghostfolio/api/app/core/interfaces/timeline-specification.interface';
import { TransactionPoint } from '@ghostfolio/api/app/core/interfaces/transaction-point.interface';
import { PortfolioCalculator } from '@ghostfolio/api/app/core/portfolio-calculator';
import { OrderService } from '@ghostfolio/api/app/order/order.service'; import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface';
import { TimelineSpecification } from '@ghostfolio/api/app/portfolio/interfaces/timeline-specification.interface';
import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface';
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/portfolio-calculator';
import { OrderType } from '@ghostfolio/api/models/order-type'; import { OrderType } from '@ghostfolio/api/models/order-type';
import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment'; import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment';
import { AccountClusterRiskInitialInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/initial-investment'; import { AccountClusterRiskInitialInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/initial-investment';
@ -14,19 +15,20 @@ import { CurrencyClusterRiskBaseCurrencyInitialInvestment } from '@ghostfolio/ap
import { CurrencyClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/current-investment'; import { CurrencyClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/current-investment';
import { CurrencyClusterRiskInitialInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/initial-investment'; import { CurrencyClusterRiskInitialInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/initial-investment';
import { FeeRatioInitialInvestment } from '@ghostfolio/api/models/rules/fees/fee-ratio-initial-investment'; import { FeeRatioInitialInvestment } from '@ghostfolio/api/models/rules/fees/fee-ratio-initial-investment';
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/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 { 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, PortfolioDetails,
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,
@ -58,6 +65,7 @@ import {
HistoricalDataItem, HistoricalDataItem,
PortfolioPositionDetail PortfolioPositionDetail
} from './interfaces/portfolio-position-detail.interface'; } from './interfaces/portfolio-position-detail.interface';
import { RulesService } from './rules.service';
@Injectable() @Injectable()
export class PortfolioService { export class PortfolioService {
@ -83,7 +91,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 +119,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,35 +152,10 @@ 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'
): Promise<{ [symbol: string]: PortfolioPosition }> { ): Promise<PortfolioDetails & { hasErrors: boolean }> {
const userId = await this.getUserId(aImpersonationId); const userId = await this.getUserId(aImpersonationId);
const userCurrency = this.request.user.Settings.currency; const userCurrency = this.request.user.Settings.currency;
@ -178,12 +164,12 @@ 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 { accounts: {}, holdings: {}, hasErrors: false };
} }
portfolioCalculator.setTransactionPoints(transactionPoints); portfolioCalculator.setTransactionPoints(transactionPoints);
@ -194,12 +180,16 @@ export class PortfolioService {
startDate startDate
); );
if (currentPositions.hasErrors) { const cashDetails = await this.accountService.getCashDetails(
throw new Error('Missing information'); userId,
} userCurrency
);
const result: { [symbol: string]: PortfolioPosition } = {}; const holdings: PortfolioDetails['holdings'] = {};
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
@ -219,23 +209,22 @@ export class PortfolioService {
for (const position of currentPositions.positions) { for (const position of currentPositions.positions) {
portfolioItemsNow[position.symbol] = position; portfolioItemsNow[position.symbol] = position;
} }
const accounts = this.getAccounts(orders, portfolioItemsNow, userCurrency);
for (const item of currentPositions.positions) { for (const item of currentPositions.positions) {
const value = item.quantity.mul(item.marketPrice); const value = item.quantity.mul(item.marketPrice);
const symbolProfile = symbolProfileMap[item.symbol]; const symbolProfile = symbolProfileMap[item.symbol];
const dataProviderResponse = dataProviderResponses[item.symbol]; const dataProviderResponse = dataProviderResponses[item.symbol];
result[item.symbol] = { holdings[item.symbol] = {
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(), assetSubClass: symbolProfile.assetSubClass,
countries: symbolProfile.countries, countries: symbolProfile.countries,
currency: item.currency, currency: item.currency,
exchange: dataProviderResponse.exchange, exchange: dataProviderResponse.exchange,
grossPerformance: item.grossPerformance.toNumber(), grossPerformance: item.grossPerformance?.toNumber() ?? 0,
grossPerformancePercent: item.grossPerformancePercentage.toNumber(), grossPerformancePercent:
item.grossPerformancePercentage?.toNumber() ?? 0,
investment: item.investment.toNumber(), investment: item.investment.toNumber(),
marketPrice: item.marketPrice, marketPrice: item.marketPrice,
marketState: dataProviderResponse.marketState, marketState: dataProviderResponse.marketState,
@ -244,12 +233,25 @@ export class PortfolioService {
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()
}; };
} }
return result; // TODO: Add a cash position for each currency
holdings[ghostfolioCashSymbol] = await this.getCashPosition({
cashDetails,
investment: totalInvestment,
value: totalValue
});
const accounts = await this.getAccounts(
orders,
portfolioItemsNow,
userCurrency,
userId
);
return { accounts, holdings, hasErrors: currentPositions.hasErrors };
} }
public async getPosition( public async getPosition(
@ -258,7 +260,7 @@ export class PortfolioService {
): Promise<PortfolioPositionDetail> { ): Promise<PortfolioPositionDetail> {
const userId = await this.getUserId(aImpersonationId); const userId = await this.getUserId(aImpersonationId);
const orders = (await this.getOrders(userId)).filter( const orders = (await this.orderService.getOrders({ userId })).filter(
(order) => order.symbol === aSymbol (order) => order.symbol === aSymbol
); );
@ -339,8 +341,17 @@ export class PortfolioService {
); );
const historicalDataArray: HistoricalDataItem[] = []; const historicalDataArray: HistoricalDataItem[] = [];
let maxPrice = marketPrice; let maxPrice = Math.max(orders[0].unitPrice, marketPrice);
let minPrice = marketPrice; let minPrice = Math.min(orders[0].unitPrice, marketPrice);
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;
@ -453,7 +464,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 {
@ -490,6 +501,7 @@ export class PortfolioService {
positions: 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:
@ -514,7 +526,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 {
@ -522,8 +534,6 @@ export class PortfolioService {
performance: { performance: {
currentGrossPerformance: 0, currentGrossPerformance: 0,
currentGrossPerformancePercent: 0, currentGrossPerformancePercent: 0,
currentNetPerformance: 0,
currentNetPerformancePercent: 0,
currentValue: 0 currentValue: 0
} }
}; };
@ -548,9 +558,6 @@ export class PortfolioService {
performance: { performance: {
currentGrossPerformance, currentGrossPerformance,
currentGrossPerformancePercent, currentGrossPerformancePercent,
// TODO: the next two should include fees
currentNetPerformance: currentGrossPerformance,
currentNetPerformancePercent: currentGrossPerformancePercent,
currentValue: currentValue currentValue: currentValue
} }
}; };
@ -576,9 +583,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 {
@ -601,7 +608,12 @@ export class PortfolioService {
for (const position of currentPositions.positions) { for (const position of currentPositions.positions) {
portfolioItemsNow[position.symbol] = position; portfolioItemsNow[position.symbol] = position;
} }
const accounts = this.getAccounts(orders, portfolioItemsNow, baseCurrency); const accounts = await this.getAccounts(
orders,
portfolioItemsNow,
baseCurrency,
userId
);
return { return {
rules: { rules: {
accountClusterRisk: await this.rulesService.evaluate( accountClusterRisk: await this.rulesService.evaluate(
@ -656,6 +668,73 @@ 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(aImpersonationId);
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 cashValue = new Big(cashDetails.balance);
return {
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':
@ -674,11 +753,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: [] };
@ -712,13 +797,37 @@ export class PortfolioService {
}; };
} }
private getAccounts( private async getAccounts(
orders: OrderWithAccount[], orders: OrderWithAccount[],
portfolioItemsNow: { [p: string]: TimelinePosition }, portfolioItemsNow: { [p: string]: TimelinePosition },
userCurrency userCurrency: Currency,
userId: string
) { ) {
const accounts: PortfolioPosition['accounts'] = {}; const accounts: PortfolioDetails['accounts'] = {};
for (const order of orders) {
const currentAccounts = await this.accountService.getAccounts(userId);
for (const account of currentAccounts) {
const ordersByAccount = orders.filter(({ accountId }) => {
return accountId === account.id;
});
if (ordersByAccount.length <= 0) {
// Add account without orders
const balance = this.exchangeRateDataService.toCurrency(
account.balance,
account.currency,
userCurrency
);
accounts[account.name] = {
current: balance,
original: balance
};
continue;
}
for (const order of ordersByAccount) {
let currentValueOfSymbol = this.exchangeRateDataService.toCurrency( let currentValueOfSymbol = this.exchangeRateDataService.toCurrency(
order.quantity * portfolioItemsNow[order.symbol].marketPrice, order.quantity * portfolioItemsNow[order.symbol].marketPrice,
order.currency, order.currency,
@ -747,20 +856,9 @@ export class PortfolioService {
}; };
} }
} }
return accounts;
} }
private getOrders(aUserId: string) { return accounts;
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) {

View File

@ -1,9 +1,8 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { Rule } from '@ghostfolio/api/models/rule';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Currency } from '@prisma/client'; import { Currency } from '@prisma/client';
import { Rule } from '../models/rule';
@Injectable() @Injectable()
export class RulesService { export class RulesService {
public constructor() {} public constructor() {}

View File

@ -8,6 +8,7 @@ import { SubscriptionService } from './subscription.service';
@Module({ @Module({
imports: [], imports: [],
controllers: [SubscriptionController], controllers: [SubscriptionController],
providers: [ConfigurationService, PrismaService, SubscriptionService] providers: [ConfigurationService, PrismaService, SubscriptionService],
exports: [SubscriptionService]
}) })
export class SubscriptionModule {} export class SubscriptionModule {}

View File

@ -1,7 +1,9 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { SubscriptionType } from '@ghostfolio/common/types/subscription.type';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { addDays } from 'date-fns'; import { Subscription } from '@prisma/client';
import { addDays, isBefore } from 'date-fns';
import Stripe from 'stripe'; import Stripe from 'stripe';
@Injectable() @Injectable()
@ -10,7 +12,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 +70,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: {
@ -86,4 +88,23 @@ export class SubscriptionService {
console.error(error); console.error(error);
} }
} }
public getSubscription(aSubscriptions: Subscription[]) {
if (aSubscriptions.length > 0) {
const latestSubscription = aSubscriptions.reduce((a, b) => {
return new Date(a.expiresAt) > new Date(b.expiresAt) ? a : b;
});
return {
expiresAt: latestSubscription.expiresAt,
type: isBefore(new Date(), latestSubscription.expiresAt)
? SubscriptionType.Premium
: SubscriptionType.Basic
};
} else {
return {
type: SubscriptionType.Basic
};
}
}
} }

View File

@ -1,6 +1,7 @@
import { DataSource } from '@prisma/client'; import { Currency, DataSource } from '@prisma/client';
export interface LookupItem { export interface LookupItem {
currency: Currency;
dataSource: DataSource; dataSource: DataSource;
name: string; name: string;
symbol: string; symbol: string;

View File

@ -1,27 +1,14 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service'; import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
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 { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { SymbolController } from './symbol.controller'; import { SymbolController } from './symbol.controller';
import { SymbolService } from './symbol.service'; import { SymbolService } from './symbol.service';
@Module({ @Module({
imports: [], imports: [ConfigurationModule, DataProviderModule, PrismaModule],
controllers: [SymbolController], controllers: [SymbolController],
providers: [ providers: [SymbolService]
AlphaVantageService,
ConfigurationService,
DataProviderService,
GhostfolioScraperApiService,
PrismaService,
RakutenRapidApiService,
SymbolService,
YahooFinanceService
]
}) })
export class SymbolModule {} export class SymbolModule {}

View File

@ -1,6 +1,5 @@
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/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,30 @@ 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({ currency: true,
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);

View File

@ -0,0 +1,3 @@
export interface UserSettings {
isRestrictedView?: boolean;
}

View File

@ -0,0 +1,6 @@
import { IsBoolean } from 'class-validator';
export class UpdateUserSettingDto {
@IsBoolean()
isRestrictedView?: boolean;
}

View File

@ -26,6 +26,8 @@ import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { UserItem } from './interfaces/user-item.interface'; import { UserItem } from './interfaces/user-item.interface';
import { UserSettingsParams } from './interfaces/user-settings-params.interface'; import { UserSettingsParams } from './interfaces/user-settings-params.interface';
import { UserSettings } from './interfaces/user-settings.interface';
import { UpdateUserSettingDto } from './update-user-setting.dto';
import { UpdateUserSettingsDto } from './update-user-settings.dto'; import { UpdateUserSettingsDto } from './update-user-settings.dto';
import { UserService } from './user.service'; import { UserService } from './user.service';
@ -78,6 +80,32 @@ export class UserController {
}; };
} }
@Put('setting')
@UseGuards(AuthGuard('jwt'))
public async updateUserSetting(@Body() data: UpdateUserSettingDto) {
if (
!hasPermission(
getPermissions(this.request.user.role),
permissions.updateUserSettings
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const userSettings: UserSettings = {
...(<UserSettings>this.request.user.Settings.settings),
...data
};
return await this.userService.updateUserSetting({
userSettings,
userId: this.request.user.id
});
}
@Put('settings') @Put('settings')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async updateUserSettings(@Body() data: UpdateUserSettingsDto) { public async updateUserSettings(@Body() data: UpdateUserSettingsDto) {

View File

@ -1,3 +1,4 @@
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.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';
@ -11,9 +12,11 @@ import { UserService } from './user.service';
JwtModule.register({ JwtModule.register({
secret: process.env.JWT_SECRET_KEY, secret: process.env.JWT_SECRET_KEY,
signOptions: { expiresIn: '30 days' } signOptions: { expiresIn: '30 days' }
}) }),
SubscriptionModule
], ],
controllers: [UserController], controllers: [UserController],
providers: [ConfigurationService, PrismaService, UserService] providers: [ConfigurationService, PrismaService, UserService],
exports: [UserService]
}) })
export class UserModule {} export class UserModule {}

View File

@ -1,3 +1,4 @@
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { locale } from '@ghostfolio/common/config'; import { locale } from '@ghostfolio/common/config';
@ -6,9 +7,9 @@ import { getPermissions, permissions } from '@ghostfolio/common/permissions';
import { SubscriptionType } from '@ghostfolio/common/types/subscription.type'; import { SubscriptionType } from '@ghostfolio/common/types/subscription.type';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Currency, Prisma, Provider, User, ViewMode } from '@prisma/client'; import { Currency, Prisma, Provider, User, ViewMode } from '@prisma/client';
import { isBefore } from 'date-fns';
import { UserSettingsParams } from './interfaces/user-settings-params.interface'; import { UserSettingsParams } from './interfaces/user-settings-params.interface';
import { UserSettings } from './interfaces/user-settings.interface';
const crypto = require('crypto'); const crypto = require('crypto');
@ -18,7 +19,8 @@ export class UserService {
public constructor( public constructor(
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private prisma: PrismaService private readonly prismaService: PrismaService,
private readonly subscriptionService: SubscriptionService
) {} ) {}
public async getUser({ public async getUser({
@ -29,7 +31,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
}, },
@ -50,6 +52,7 @@ export class UserService {
}), }),
accounts: Account, accounts: Account,
settings: { settings: {
...(<UserSettings>Settings.settings),
locale, locale,
baseCurrency: Settings?.currency ?? UserService.DEFAULT_CURRENCY, baseCurrency: Settings?.currency ?? UserService.DEFAULT_CURRENCY,
viewMode: Settings?.viewMode ?? ViewMode.DEFAULT viewMode: Settings?.viewMode ?? ViewMode.DEFAULT
@ -57,10 +60,14 @@ export class UserService {
}; };
} }
public isRestrictedView(aUser: UserWithSettings) {
return (aUser.Settings.settings as UserSettings)?.isRestrictedView ?? false;
}
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
}); });
@ -84,6 +91,7 @@ export class UserService {
// Set default settings if needed // Set default settings if needed
userFromDatabase.Settings = { userFromDatabase.Settings = {
currency: UserService.DEFAULT_CURRENCY, currency: UserService.DEFAULT_CURRENCY,
settings: null,
updatedAt: new Date(), updatedAt: new Date(),
userId: userFromDatabase?.id, userId: userFromDatabase?.id,
viewMode: ViewMode.DEFAULT viewMode: ViewMode.DEFAULT
@ -91,25 +99,10 @@ export class UserService {
} }
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
if (userFromDatabase?.Subscription?.length > 0) { user.subscription = this.subscriptionService.getSubscription(
const latestSubscription = userFromDatabase.Subscription.reduce( userFromDatabase?.Subscription
(a, b) => {
return new Date(a.expiresAt) > new Date(b.expiresAt) ? a : b;
}
); );
user.subscription = {
expiresAt: latestSubscription.expiresAt,
type: isBefore(new Date(), latestSubscription.expiresAt)
? SubscriptionType.Premium
: SubscriptionType.Basic
};
} else {
user.subscription = {
type: SubscriptionType.Basic
};
}
if (user.subscription.type === SubscriptionType.Basic) { if (user.subscription.type === SubscriptionType.Basic) {
user.permissions = user.permissions.filter((permission) => { user.permissions = user.permissions.filter((permission) => {
return permission !== permissions.updateViewMode; return permission !== permissions.updateViewMode;
@ -129,7 +122,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 +139,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 +162,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,46 +178,75 @@ 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
}); });
} }
public async updateUserSetting({
userId,
userSettings
}: {
userId: string;
userSettings: UserSettings;
}) {
const settings = userSettings as Prisma.JsonObject;
await this.prismaService.settings.upsert({
create: {
settings,
User: {
connect: {
id: userId
}
}
},
update: {
settings
},
where: {
userId: userId
}
});
return;
}
public async updateUserSettings({ public async updateUserSettings({
currency, currency,
userId, userId,
viewMode viewMode
}: UserSettingsParams) { }: UserSettingsParams) {
await this.prisma.settings.upsert({ await this.prismaService.settings.upsert({
create: { create: {
currency, currency,
User: { User: {

View File

@ -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() {

View File

@ -1,10 +1,10 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface'; import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { groupBy } from '@ghostfolio/common/helper'; import { groupBy } from '@ghostfolio/common/helper';
import { TimelinePosition } from '@ghostfolio/common/interfaces'; import { TimelinePosition } from '@ghostfolio/common/interfaces';
import { Currency } from '@prisma/client'; import { Currency } from '@prisma/client';
import { ExchangeRateDataService } from '../services/exchange-rate-data.service';
import { EvaluationResult } from './interfaces/evaluation-result.interface'; import { EvaluationResult } from './interfaces/evaluation-result.interface';
import { RuleInterface } from './interfaces/rule.interface'; import { RuleInterface } from './interfaces/rule.interface';

View File

@ -1,16 +1,17 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface'; import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { PortfolioPosition } from '@ghostfolio/common/interfaces'; import {
PortfolioDetails,
PortfolioPosition
} from '@ghostfolio/common/interfaces';
import { Rule } from '../../rule'; import { Rule } from '../../rule';
export class AccountClusterRiskCurrentInvestment extends Rule<Settings> { export class AccountClusterRiskCurrentInvestment extends Rule<Settings> {
public constructor( public constructor(
protected exchangeRateDataService: ExchangeRateDataService, protected exchangeRateDataService: ExchangeRateDataService,
private accounts: { private accounts: PortfolioDetails['accounts']
[account: string]: { current: number; original: number };
}
) { ) {
super(exchangeRateDataService, { super(exchangeRateDataService, {
name: 'Current Investment' name: 'Current Investment'

View File

@ -1,6 +1,9 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface'; import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
import { PortfolioPosition } from '@ghostfolio/common/interfaces'; import {
PortfolioDetails,
PortfolioPosition
} from '@ghostfolio/common/interfaces';
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service'; import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
import { Rule } from '../../rule'; import { Rule } from '../../rule';
@ -8,9 +11,7 @@ import { Rule } from '../../rule';
export class AccountClusterRiskInitialInvestment extends Rule<Settings> { export class AccountClusterRiskInitialInvestment extends Rule<Settings> {
public constructor( public constructor(
protected exchangeRateDataService: ExchangeRateDataService, protected exchangeRateDataService: ExchangeRateDataService,
private accounts: { private accounts: PortfolioDetails['accounts']
[account: string]: { current: number; original: number };
}
) { ) {
super(exchangeRateDataService, { super(exchangeRateDataService, {
name: 'Initial Investment' name: 'Initial Investment'

View File

@ -1,5 +1,6 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface'; import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
import { PortfolioDetails } from '@ghostfolio/common/interfaces';
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service'; import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
import { Rule } from '../../rule'; import { Rule } from '../../rule';
@ -7,9 +8,7 @@ import { Rule } from '../../rule';
export class AccountClusterRiskSingleAccount extends Rule<RuleSettings> { export class AccountClusterRiskSingleAccount extends Rule<RuleSettings> {
public constructor( public constructor(
protected exchangeRateDataService: ExchangeRateDataService, protected exchangeRateDataService: ExchangeRateDataService,
private accounts: { private accounts: PortfolioDetails['accounts']
[account: string]: { current: number; original: number };
}
) { ) {
super(exchangeRateDataService, { super(exchangeRateDataService, {
name: 'Single Account' name: 'Single Account'

View File

@ -1,4 +1,4 @@
import { CurrentPositions } from '@ghostfolio/api/app/core/interfaces/current-positions.interface'; import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface'; import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';

View File

@ -1,4 +1,4 @@
import { CurrentPositions } from '@ghostfolio/api/app/core/interfaces/current-positions.interface'; import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface'; import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
import { Currency } from '@prisma/client'; import { Currency } from '@prisma/client';

View File

@ -1,4 +1,4 @@
import { CurrentPositions } from '@ghostfolio/api/app/core/interfaces/current-positions.interface'; import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface'; import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
import { Currency } from '@prisma/client'; import { Currency } from '@prisma/client';

View File

@ -1,4 +1,4 @@
import { CurrentPositions } from '@ghostfolio/api/app/core/interfaces/current-positions.interface'; import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface'; import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
import { Currency } from '@prisma/client'; import { Currency } from '@prisma/client';

View File

@ -0,0 +1,8 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { Module } from '@nestjs/common';
@Module({
providers: [ConfigurationService],
exports: [ConfigurationService]
})
export class ConfigurationModule {}

View File

@ -0,0 +1,12 @@
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
import { Module } from '@nestjs/common';
@Module({
imports: [ConfigurationModule, DataProviderModule, PrismaModule],
providers: [DataGatheringService],
exports: [DataGatheringService]
})
export class DataGatheringModule {}

View File

@ -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';
@ -20,7 +19,7 @@ import {
} from 'date-fns'; } from 'date-fns';
import { ConfigurationService } from './configuration.service'; import { ConfigurationService } from './configuration.service';
import { DataProviderService } from './data-provider.service'; import { DataProviderService } from './data-provider/data-provider.service';
import { GhostfolioScraperApiService } from './data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service'; import { GhostfolioScraperApiService } from './data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
import { IDataGatheringItem } from './interfaces/interfaces'; import { IDataGatheringItem } from './interfaces/interfaces';
import { PrismaService } from './prisma.service'; import { PrismaService } from './prisma.service';
@ -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() {
@ -39,9 +38,9 @@ export class DataGatheringService {
if (isDataGatheringNeeded) { if (isDataGatheringNeeded) {
console.log('7d data gathering has been started.'); console.log('7d data gathering has been started.');
console.time('7d-data-gathering'); console.time('data-gathering-7d');
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,27 +64,27 @@ 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'
} }
}); });
console.log('7d data gathering has been completed.'); console.log('7d data gathering has been completed.');
console.timeEnd('7d-data-gathering'); console.timeEnd('data-gathering-7d');
} }
} }
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' }
}); });
if (!isDataGatheringLocked) { if (!isDataGatheringLocked) {
console.log('Max data gathering has been started.'); console.log('Max data gathering has been started.');
console.time('max-data-gathering'); console.time('data-gathering-max');
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,20 +108,20 @@ 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'
} }
}); });
console.log('Max data gathering has been completed.'); console.log('Max data gathering has been completed.');
console.timeEnd('max-data-gathering'); console.timeEnd('data-gathering-max');
} }
} }
public async gatherProfileData(aSymbols?: string[]) { public async gatherProfileData(aSymbols?: string[]) {
console.log('Profile data gathering has been started.'); console.log('Profile data gathering has been started.');
console.time('profile-data-gathering'); console.time('data-gathering-profile');
let symbols = aSymbols; let symbols = aSymbols;
@ -135,18 +134,25 @@ 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, assetSubClass, countries, currency, dataSource, name }
] of Object.entries(currentData)) {
try { try {
await this.prisma.symbolProfile.upsert({ await this.prismaService.symbolProfile.upsert({
create: { create: {
assetClass,
assetSubClass,
countries,
currency, currency,
dataSource, dataSource,
name, name,
symbol symbol
}, },
update: { update: {
assetClass,
assetSubClass,
countries,
currency, currency,
name name
}, },
@ -163,7 +169,7 @@ export class DataGatheringService {
} }
console.log('Profile data gathering has been completed.'); console.log('Profile data gathering has been completed.');
console.timeEnd('profile-data-gathering'); console.timeEnd('data-gathering-profile');
} }
public async gatherSymbols(aSymbolsWithStartDate: IDataGatheringItem[]) { public async gatherSymbols(aSymbolsWithStartDate: IDataGatheringItem[]) {
@ -203,7 +209,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 +254,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 +305,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 +337,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 +351,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 +364,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 +386,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 +403,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;
} }

View File

@ -1,11 +1,11 @@
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } 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 { DataSource } from '@prisma/client';
import { isAfter, isBefore, parse } from 'date-fns'; import { isAfter, isBefore, parse } from 'date-fns';
import { ConfigurationService } from '../../configuration.service';
import { DataProviderInterface } from '../../interfaces/data-provider.interface'; import { DataProviderInterface } from '../../interfaces/data-provider.interface';
import { import {
IDataProviderHistoricalResponse, IDataProviderHistoricalResponse,

View File

@ -0,0 +1,22 @@
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
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 { PrismaModule } from '@ghostfolio/api/services/prisma.module';
import { Module } from '@nestjs/common';
import { AlphaVantageService } from './alpha-vantage/alpha-vantage.service';
import { DataProviderService } from './data-provider.service';
@Module({
imports: [ConfigurationModule, PrismaModule],
providers: [
AlphaVantageService,
DataProviderService,
GhostfolioScraperApiService,
RakutenRapidApiService,
YahooFinanceService
],
exports: [DataProviderService, GhostfolioScraperApiService]
})
export class DataProviderModule {}

View File

@ -1,3 +1,11 @@
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import {
IDataGatheringItem,
IDataProviderHistoricalResponse,
IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { import {
DATE_FORMAT, DATE_FORMAT,
isGhostfolioScraperApiSymbol, isGhostfolioScraperApiSymbol,
@ -8,17 +16,10 @@ import { Injectable } from '@nestjs/common';
import { DataSource, MarketData } from '@prisma/client'; import { DataSource, MarketData } from '@prisma/client';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { ConfigurationService } from './configuration.service'; import { AlphaVantageService } from './alpha-vantage/alpha-vantage.service';
import { AlphaVantageService } from './data-provider/alpha-vantage/alpha-vantage.service'; import { GhostfolioScraperApiService } from './ghostfolio-scraper-api/ghostfolio-scraper-api.service';
import { GhostfolioScraperApiService } from './data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service'; import { RakutenRapidApiService } from './rakuten-rapid-api/rakuten-rapid-api.service';
import { RakutenRapidApiService } from './data-provider/rakuten-rapid-api/rakuten-rapid-api.service'; import { YahooFinanceService } from './yahoo-finance/yahoo-finance.service';
import { YahooFinanceService } from './data-provider/yahoo-finance/yahoo-finance.service';
import {
IDataGatheringItem,
IDataProviderHistoricalResponse,
IDataProviderResponse
} from './interfaces/interfaces';
import { PrismaService } from './prisma.service';
@Injectable() @Injectable()
export class DataProviderService { export class DataProviderService {
@ -26,11 +27,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 +113,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;
@ -167,10 +167,19 @@ export class DataProviderService {
return result; return result;
} }
public async search(aSymbol: string) { public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
return this.getDataProvider( const { items } = await this.getDataProvider(
<DataSource>this.configurationService.get('DATA_SOURCES')[0] <DataSource>this.configurationService.get('DATA_SOURCES')[0]
).search(aSymbol); ).search(aSymbol);
const filteredItems = items.filter((item) => {
// Only allow symbols with supported currency
return item.currency ? true : false;
});
return {
items: filteredItems
};
} }
private getDataProvider(providerName: DataSource) { private getDataProvider(providerName: DataSource) {

View File

@ -1,3 +1,5 @@
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { import {
DATE_FORMAT, DATE_FORMAT,
getYesterday, getYesterday,
@ -12,18 +14,18 @@ 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
} from '../../interfaces/interfaces'; } from '../../interfaces/interfaces';
import { PrismaService } from '../../prisma.service';
import { ScraperConfig } from './interfaces/scraper-config.interface'; import { ScraperConfig } from './interfaces/scraper-config.interface';
@Injectable() @Injectable()
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 +43,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 +57,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 +67,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 +131,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
}, },
@ -124,7 +144,7 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
return []; return [];
} }
public async search(aSymbol: string) { public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
return { items: [] }; return { items: [] };
} }

View File

@ -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;

View File

@ -1,3 +1,6 @@
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { import {
DATE_FORMAT, DATE_FORMAT,
getToday, getToday,
@ -10,20 +13,18 @@ import { DataSource } from '@prisma/client';
import * as bent from 'bent'; import * as bent from 'bent';
import { format, subMonths, subWeeks, subYears } from 'date-fns'; import { format, subMonths, subWeeks, subYears } from 'date-fns';
import { ConfigurationService } from '../../configuration.service';
import { DataProviderInterface } from '../../interfaces/data-provider.interface'; import { DataProviderInterface } from '../../interfaces/data-provider.interface';
import { import {
IDataProviderHistoricalResponse, IDataProviderHistoricalResponse,
IDataProviderResponse, IDataProviderResponse,
MarketState MarketState
} from '../../interfaces/interfaces'; } from '../../interfaces/interfaces';
import { PrismaService } from '../../prisma.service';
@Injectable() @Injectable()
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 +90,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 +98,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 +106,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),
@ -129,12 +130,12 @@ export class RakutenRapidApiService implements DataProviderInterface {
return {}; return {};
} }
public async search(aSymbol: string) { public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
return { items: [] }; return { items: [] };
} }
public setPrisma(aPrismaService: PrismaService) { public setPrisma(aPrismaService: PrismaService) {
this.prisma = aPrismaService; this.prismaService = aPrismaService;
} }
private async getFearAndGreedIndex(): Promise<{ private async getFearAndGreedIndex(): Promise<{

View File

@ -25,6 +25,7 @@ export interface IYahooFinancePrice {
} }
export interface IYahooFinanceSummaryProfile { export interface IYahooFinanceSummaryProfile {
country?: string;
industry?: string; industry?: string;
sector?: string; sector?: string;
website?: string; website?: string;

View File

@ -8,8 +8,15 @@ 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,
AssetSubClass,
Currency,
DataSource
} from '@prisma/client';
import * as bent from 'bent'; import * as bent from 'bent';
import Big from 'big.js';
import { countries } from 'countries-list';
import { format } from 'date-fns'; import { format } from 'date-fns';
import * as yahooFinance from 'yahoo-finance'; import * as yahooFinance from 'yahoo-finance';
@ -17,11 +24,11 @@ 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,
IYahooFinancePrice,
IYahooFinanceQuoteResponse IYahooFinanceQuoteResponse
} from './interfaces/interfaces'; } from './interfaces/interfaces';
@ -60,7 +67,11 @@ export class YahooFinanceService implements DataProviderInterface {
// Convert symbols back // Convert symbols back
const symbol = convertFromYahooSymbol(yahooSymbol); const symbol = convertFromYahooSymbol(yahooSymbol);
const { assetClass, assetSubClass } = this.parseAssetClass(value.price);
response[symbol] = { response[symbol] = {
assetClass,
assetSubClass,
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,10 +80,36 @@ 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))
}; };
if (value.price?.currency === 'GBp') {
// Convert GBp (pence) to GBP
response[symbol].currency = Currency.GBP;
response[symbol].marketPrice = new Big(
value.price?.regularMarketPrice ?? 0
)
.div(100)
.toNumber();
}
// Add country if stock and available
if (
assetSubClass === AssetSubClass.STOCK &&
value.summaryProfile?.country
) {
try {
const [code] = Object.entries(countries).find(([, country]) => {
return country.name === value.summaryProfile?.country;
});
if (code) {
response[symbol].countries = [{ code, weight: 1 }];
}
} catch {}
}
// Add url if available
const url = value.summaryProfile?.website; const url = value.summaryProfile?.website;
if (url) { if (url) {
response[symbol].url = url; response[symbol].url = url;
@ -138,7 +175,7 @@ export class YahooFinanceService implements DataProviderInterface {
} }
public async search(aSymbol: string): Promise<{ items: LookupItem[] }> { public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
let items = []; let items: LookupItem[] = [];
try { try {
const get = bent( const get = bent(
@ -148,8 +185,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,13 +215,14 @@ 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);
} }
return true; return true;
}) })
.map(({ longname, shortname, symbol }) => { .map(({ longname, shortname, symbol }) => {
return { return {
currency: marketData[symbol]?.currency,
dataSource: DataSource.YAHOO, dataSource: DataSource.YAHOO,
name: longname || shortname, name: longname || shortname,
symbol: convertFromYahooSymbol(symbol) symbol: convertFromYahooSymbol(symbol)
@ -203,14 +256,29 @@ export class YahooFinanceService implements DataProviderInterface {
return aSymbol; return aSymbol;
} }
private getType(aSymbol: string, aValue: IYahooFinanceQuoteResponse): Type { private parseAssetClass(aPrice: IYahooFinancePrice): {
if (isCrypto(aSymbol)) { assetClass: AssetClass;
return Type.Cryptocurrency; assetSubClass: AssetSubClass;
} else if (aValue.price?.quoteType.toLowerCase() === 'equity') { } {
return Type.Stock; let assetClass: AssetClass;
let assetSubClass: AssetSubClass;
switch (aPrice?.quoteType?.toLowerCase()) {
case 'cryptocurrency':
assetClass = AssetClass.CASH;
assetSubClass = AssetSubClass.CRYPTOCURRENCY;
break;
case 'equity':
assetClass = AssetClass.EQUITY;
assetSubClass = AssetSubClass.STOCK;
break;
case 'etf':
assetClass = AssetClass.EQUITY;
assetSubClass = AssetSubClass.ETF;
break;
} }
return aValue.price?.quoteType.toLowerCase(); return { assetClass, assetSubClass };
} }
private parseExchange(aString: string): string { private parseExchange(aString: string): string {
@ -220,18 +288,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) => {

View File

@ -0,0 +1,10 @@
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { Module } from '@nestjs/common';
@Module({
imports: [DataProviderModule],
providers: [ExchangeRateDataService],
exports: [ExchangeRateDataService]
})
export class ExchangeRateDataModule {}

View File

@ -1,40 +1,58 @@
import { currencyPairs } from '@ghostfolio/common/config';
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper'; 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 { isEmpty, isNumber } from 'lodash';
import { DataProviderService } from './data-provider.service'; import { DataProviderService } from './data-provider/data-provider.service';
@Injectable() @Injectable()
export class ExchangeRateDataService { export class ExchangeRateDataService {
private currencies = {}; private currencyPairs: string[] = [];
private pairs: string[] = []; private exchangeRates: { [currencyPair: string]: number } = {};
public constructor(private dataProviderService: DataProviderService) { public constructor(private dataProviderService: DataProviderService) {
this.initialize(); this.initialize();
} }
public async initialize() { public async initialize() {
this.pairs = []; this.currencyPairs = [];
this.exchangeRates = {};
this.addPairs(Currency.CHF, Currency.EUR); for (const { currency1, currency2 } of currencyPairs) {
this.addPairs(Currency.CHF, Currency.GBP); this.addCurrencyPairs(currency1, currency2);
this.addPairs(Currency.CHF, Currency.USD); }
this.addPairs(Currency.EUR, Currency.GBP);
this.addPairs(Currency.EUR, Currency.USD);
this.addPairs(Currency.GBP, Currency.USD);
await this.loadCurrencies(); await this.loadCurrencies();
} }
public async loadCurrencies() { public async loadCurrencies() {
const result = await this.dataProviderService.getHistorical( const result = await this.dataProviderService.getHistorical(
this.pairs, this.currencyPairs,
'day', 'day',
getYesterday(), getYesterday(),
getYesterday() getYesterday()
); );
if (isEmpty(result)) {
// Load currencies directly from data provider as a fallback
// if historical data is not yet available
const historicalData = await this.dataProviderService.get(
this.currencyPairs.map((currencyPair) => {
return currencyPair;
})
);
Object.keys(historicalData).forEach((key) => {
result[key] = {
[format(getYesterday(), DATE_FORMAT)]: {
marketPrice: historicalData[key].marketPrice
}
};
});
}
const resultExtended = result; const resultExtended = result;
Object.keys(result).forEach((pair) => { Object.keys(result).forEach((pair) => {
@ -49,20 +67,21 @@ export class ExchangeRateDataService {
}; };
}); });
this.pairs.forEach((pair) => { this.currencyPairs.forEach((pair) => {
const [currency1, currency2] = pair.match(/.{1,3}/g); const [currency1, currency2] = pair.match(/.{1,3}/g);
const date = format(getYesterday(), DATE_FORMAT); const date = format(getYesterday(), DATE_FORMAT);
this.currencies[pair] = resultExtended[pair]?.[date]?.marketPrice; this.exchangeRates[pair] = resultExtended[pair]?.[date]?.marketPrice;
if (!this.currencies[pair]) { if (!this.exchangeRates[pair]) {
// Not found, calculate indirectly via USD // Not found, calculate indirectly via USD
this.currencies[pair] = this.exchangeRates[pair] =
resultExtended[`${currency1}${Currency.USD}`]?.[date]?.marketPrice * resultExtended[`${currency1}${Currency.USD}`]?.[date]?.marketPrice *
resultExtended[`${Currency.USD}${currency2}`]?.[date]?.marketPrice; resultExtended[`${Currency.USD}${currency2}`]?.[date]?.marketPrice;
// Calculate the opposite direction // Calculate the opposite direction
this.currencies[`${currency2}${currency1}`] = 1 / this.currencies[pair]; this.exchangeRates[`${currency2}${currency1}`] =
1 / this.exchangeRates[pair];
} }
}); });
} }
@ -72,7 +91,7 @@ export class ExchangeRateDataService {
aFromCurrency: Currency, aFromCurrency: Currency,
aToCurrency: Currency aToCurrency: Currency
) { ) {
if (isNaN(this.currencies[`${Currency.USD}${Currency.CHF}`])) { if (isNaN(this.exchangeRates[`${Currency.USD}${Currency.CHF}`])) {
// Reinitialize if data is not loaded correctly // Reinitialize if data is not loaded correctly
this.initialize(); this.initialize();
} }
@ -80,14 +99,32 @@ export class ExchangeRateDataService {
let factor = 1; let factor = 1;
if (aFromCurrency !== aToCurrency) { if (aFromCurrency !== aToCurrency) {
factor = this.currencies[`${aFromCurrency}${aToCurrency}`]; if (this.exchangeRates[`${aFromCurrency}${aToCurrency}`]) {
factor = this.exchangeRates[`${aFromCurrency}${aToCurrency}`];
} else {
// Calculate indirectly via USD
const factor1 = this.exchangeRates[`${aFromCurrency}${Currency.USD}`];
const factor2 = this.exchangeRates[`${Currency.USD}${aToCurrency}`];
factor = factor1 * factor2;
this.exchangeRates[`${aFromCurrency}${aToCurrency}`] = factor;
}
} }
if (isNumber(factor)) {
return factor * aValue; return factor * aValue;
} }
private addPairs(aCurrency1: Currency, aCurrency2: Currency) { // Fallback with error, if currencies are not available
this.pairs.push(`${aCurrency1}${aCurrency2}`); console.error(
this.pairs.push(`${aCurrency2}${aCurrency1}`); `No exchange rate has been found for ${aFromCurrency}${aToCurrency}`
);
return aValue;
}
private addCurrencyPairs(aCurrency1: Currency, aCurrency2: Currency) {
this.currencyPairs.push(`${aCurrency1}${aCurrency2}`);
this.currencyPairs.push(`${aCurrency2}${aCurrency1}`);
} }
} }

View File

@ -0,0 +1,10 @@
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
import { Module } from '@nestjs/common';
@Module({
imports: [PrismaModule],
providers: [ImpersonationService],
exports: [ImpersonationService]
})
export class ImpersonationModule {}

View File

@ -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 }
}); });

View File

@ -1,5 +1,11 @@
import { UNKNOWN_KEY } from '@ghostfolio/common/config'; import {
import { Account, Currency, DataSource, SymbolProfile } from '@prisma/client'; Account,
AssetClass,
AssetSubClass,
Currency,
DataSource,
SymbolProfile
} from '@prisma/client';
import { OrderType } from '../../models/order-type'; import { OrderType } from '../../models/order-type';
@ -9,20 +15,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 +35,9 @@ export interface IDataProviderHistoricalResponse {
} }
export interface IDataProviderResponse { export interface IDataProviderResponse {
assetClass?: AssetClass;
assetSubClass?: AssetSubClass;
countries?: { code: string; weight: number }[];
currency: Currency; currency: Currency;
dataSource: DataSource; dataSource: DataSource;
exchange?: string; exchange?: string;
@ -43,8 +45,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 +56,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];

View File

@ -1,8 +1,15 @@
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,
AssetSubClass,
Currency,
DataSource
} from '@prisma/client';
export interface EnhancedSymbolProfile { export interface EnhancedSymbolProfile {
assetClass: AssetClass;
assetSubClass: AssetSubClass;
createdAt: Date; createdAt: Date;
currency: Currency | null; currency: Currency | null;
dataSource: DataSource; dataSource: DataSource;

View File

@ -0,0 +1,8 @@
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { Module } from '@nestjs/common';
@Module({
providers: [PrismaService],
exports: [PrismaService]
})
export class PrismaModule {}

View File

@ -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: {

View File

@ -10,7 +10,7 @@
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Type</th> <th *matHeaderCellDef class="px-1" i18n mat-header-cell>Type</th>
<td *matCellDef="let element" class="px-1" mat-cell> <td *matCellDef="let element" class="px-1" mat-cell>
<ion-icon class="mr-1" name="lock-closed-outline"></ion-icon> <ion-icon class="mr-1" name="lock-closed-outline"></ion-icon>
Restricted Access Restricted View
</td></ng-container </td></ng-container
> >

View File

@ -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.transactionCount }}
</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"

View File

@ -155,6 +155,16 @@
</button> </button>
<hr class="m-0" /> <hr class="m-0" />
</ng-container> </ng-container>
<a
class="d-block d-sm-none"
i18n
mat-menu-item
[ngClass]="{
'font-weight-bold': currentRoute === 'home' || currentRoute === 'zen'
}"
[routerLink]="['/']"
>Overview</a
>
<a <a
*ngIf="user?.settings?.viewMode === 'DEFAULT'" *ngIf="user?.settings?.viewMode === 'DEFAULT'"
class="d-block d-sm-none" class="d-block d-sm-none"

View File

@ -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;
} }

View File

@ -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>

View File

@ -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() {}
}

View File

@ -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>

View File

@ -1,9 +0,0 @@
:host {
display: block;
.value-container {
#value {
font-variant-numeric: tabular-nums;
}
}
}

View File

@ -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();
}
}
}
}

View File

@ -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 {}

View File

@ -1,35 +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" [locale]="locale"
[value]="isLoading ? undefined : performance?.currentGrossPerformance" [value]="isLoading ? undefined : performance?.currentGrossPerformance"
></gf-value> ></gf-value>
</div> </div>
</div> <div class="col">
<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 <gf-value
class="justify-content-end"
position="end"
[colorizeSign]="true" [colorizeSign]="true"
[isPercent]="true" [isPercent]="true"
[locale]="locale" [locale]="locale"
@ -39,29 +51,4 @@
></gf-value> ></gf-value>
</div> </div>
</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"
[value]="isLoading ? undefined : performance?.currentNetPerformance"
></gf-value>
<gf-value
class="justify-content-end"
position="end"
[colorizeSign]="true"
[isPercent]="true"
[locale]="locale"
[value]="
isLoading ? undefined : performance?.currentNetPerformancePercent
"
></gf-value>
</div>
</div>
-->
</div> </div>

View File

@ -1,3 +1,9 @@
:host { :host {
display: block; display: block;
.value-container {
#value {
font-variant-numeric: tabular-nums;
}
}
} }

View File

@ -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?.currentGrossPerformancePercent * 100,
{
decimalPlaces: 2,
duration: 0.75,
separator: `'`
}
).start();
}
}
}
} }

Some files were not shown because too many files have changed in this diff Show More