Compare commits
47 Commits
Author | SHA1 | Date | |
---|---|---|---|
695c378b48 | |||
fe975945d1 | |||
d8782b0d4c | |||
e14f08a8fb | |||
72c065a59d | |||
98dac4052a | |||
2083d28d02 | |||
addd5c36d9 | |||
aad8f77093 | |||
a904208d06 | |||
2733b78044 | |||
b43b515df1 | |||
70e14b4d3c | |||
0f7d1b7d59 | |||
c2ab6a6c44 | |||
c71a4c078e | |||
e17b217032 | |||
408e08d43c | |||
2fa324702f | |||
05b0efef82 | |||
7c91727eb1 | |||
0ee2258af8 | |||
308b218487 | |||
26694adb8e | |||
77936e3bf3 | |||
13090bf6b5 | |||
b898c0678d | |||
3330ae70b6 | |||
96a615dc5d | |||
98f44323da | |||
8adacd9760 | |||
908aba170d | |||
d599797a65 | |||
8ac1272a9d | |||
0a85a56c67 | |||
4ad5590838 | |||
5b8af68e71 | |||
80d043729d | |||
178166d86b | |||
37358fb480 | |||
616d601cf6 | |||
e88b889fdd | |||
f6cdc4ff47 | |||
818c40fc61 | |||
3589e72aea | |||
e68aa1fa68 | |||
bb76ace95d |
131
CHANGELOG.md
131
CHANGELOG.md
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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'
|
||||||
|
@ -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 {}
|
||||||
|
@ -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
|
||||||
});
|
});
|
||||||
|
@ -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 {}
|
||||||
|
@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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' }]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {}
|
||||||
|
@ -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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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()
|
||||||
|
@ -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 }
|
||||||
|
@ -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
|
||||||
|
7
apps/api/src/app/cache/cache.controller.ts
vendored
7
apps/api/src/app/cache/cache.controller.ts
vendored
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
23
apps/api/src/app/cache/cache.module.ts
vendored
23
apps/api/src/app/cache/cache.module.ts
vendored
@ -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 {}
|
||||||
|
14
apps/api/src/app/cache/cache.service.ts
vendored
14
apps/api/src/app/cache/cache.service.ts
vendored
@ -1,16 +1,14 @@
|
|||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CacheService {
|
export class CacheService {
|
||||||
public constructor(private prisma: PrismaService) {}
|
public constructor(
|
||||||
|
private readonly dataGaterhingService: DataGatheringService
|
||||||
|
) {}
|
||||||
|
|
||||||
public async flush(aUserId: string): Promise<void> {
|
public async flush(): Promise<void> {
|
||||||
await this.prisma.property.deleteMany({
|
await this.dataGaterhingService.reset();
|
||||||
where: {
|
|
||||||
OR: [{ key: 'LAST_DATA_GATHERING' }, { key: 'LOCKED_DATA_GATHERING' }]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -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 {}
|
|
@ -1,6 +0,0 @@
|
|||||||
import { TransactionPointSymbol } from '@ghostfolio/api/app/core/interfaces/transaction-point-symbol.interface';
|
|
||||||
|
|
||||||
export interface TransactionPoint {
|
|
||||||
date: string;
|
|
||||||
items: TransactionPointSymbol[];
|
|
||||||
}
|
|
@ -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 {}
|
||||||
|
@ -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 }
|
||||||
|
@ -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 {}
|
||||||
|
@ -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,
|
||||||
|
@ -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 {}
|
||||||
|
@ -24,8 +24,7 @@ export class ImportService {
|
|||||||
type,
|
type,
|
||||||
unitPrice
|
unitPrice
|
||||||
} of orders) {
|
} of orders) {
|
||||||
await this.orderService.createOrder(
|
await this.orderService.createOrder({
|
||||||
{
|
|
||||||
currency,
|
currency,
|
||||||
dataSource,
|
dataSource,
|
||||||
fee,
|
fee,
|
||||||
@ -35,9 +34,7 @@ export class ImportService {
|
|||||||
unitPrice,
|
unitPrice,
|
||||||
date: parseISO(<string>(<unknown>date)),
|
date: parseISO(<string>(<unknown>date)),
|
||||||
User: { connect: { id: userId } }
|
User: { connect: { id: userId } }
|
||||||
},
|
});
|
||||||
userId
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,10 @@
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||||
|
import { 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 {}
|
||||||
|
@ -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' }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {}
|
||||||
|
@ -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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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', () => {
|
@ -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()
|
@ -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;
|
@ -0,0 +1,6 @@
|
|||||||
|
import { TransactionPointSymbol } from './transaction-point-symbol.interface';
|
||||||
|
|
||||||
|
export interface TransactionPoint {
|
||||||
|
date: string;
|
||||||
|
items: TransactionPointSymbol[];
|
||||||
|
}
|
@ -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'
|
@ -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(() => {
|
@ -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[];
|
||||||
|
|
@ -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;
|
||||||
|
@ -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 {}
|
||||||
|
@ -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) {
|
||||||
|
@ -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() {}
|
@ -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 {}
|
||||||
|
@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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 {}
|
||||||
|
@ -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);
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
export interface UserSettings {
|
||||||
|
isRestrictedView?: boolean;
|
||||||
|
}
|
6
apps/api/src/app/user/update-user-setting.dto.ts
Normal file
6
apps/api/src/app/user/update-user-setting.dto.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { IsBoolean } from 'class-validator';
|
||||||
|
|
||||||
|
export class UpdateUserSettingDto {
|
||||||
|
@IsBoolean()
|
||||||
|
isRestrictedView?: boolean;
|
||||||
|
}
|
@ -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) {
|
||||||
|
@ -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 {}
|
||||||
|
@ -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: {
|
||||||
|
@ -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() {
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
@ -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'
|
||||||
|
@ -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'
|
||||||
|
@ -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'
|
||||||
|
@ -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';
|
||||||
|
@ -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';
|
||||||
|
@ -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';
|
||||||
|
@ -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';
|
||||||
|
8
apps/api/src/services/configuration.module.ts
Normal file
8
apps/api/src/services/configuration.module.ts
Normal 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 {}
|
12
apps/api/src/services/data-gathering.module.ts
Normal file
12
apps/api/src/services/data-gathering.module.ts
Normal 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 {}
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
22
apps/api/src/services/data-provider/data-provider.module.ts
Normal file
22
apps/api/src/services/data-provider/data-provider.module.ts
Normal 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 {}
|
@ -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) {
|
@ -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: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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<{
|
||||||
|
@ -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;
|
||||||
|
@ -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) => {
|
||||||
|
10
apps/api/src/services/exchange-rate-data.module.ts
Normal file
10
apps/api/src/services/exchange-rate-data.module.ts
Normal 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 {}
|
@ -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}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
10
apps/api/src/services/impersonation.module.ts
Normal file
10
apps/api/src/services/impersonation.module.ts
Normal 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 {}
|
@ -4,10 +4,10 @@ import { PrismaService } from './prisma.service';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ImpersonationService {
|
export class ImpersonationService {
|
||||||
public constructor(private prisma: PrismaService) {}
|
public constructor(private readonly prismaService: PrismaService) {}
|
||||||
|
|
||||||
public async validateImpersonationId(aId = '', aUserId: string) {
|
public async validateImpersonationId(aId = '', aUserId: string) {
|
||||||
const accessObject = await this.prisma.access.findFirst({
|
const accessObject = await this.prismaService.access.findFirst({
|
||||||
where: { GranteeUser: { id: aUserId }, id: aId }
|
where: { GranteeUser: { id: aUserId }, id: aId }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,5 +1,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];
|
|
||||||
|
@ -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;
|
||||||
|
8
apps/api/src/services/prisma.module.ts
Normal file
8
apps/api/src/services/prisma.module.ts
Normal 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 {}
|
@ -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: {
|
||||||
|
@ -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
|
||||||
>
|
>
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -1,74 +0,0 @@
|
|||||||
<div class="container p-0">
|
|
||||||
<div class="row px-3 py-1">
|
|
||||||
<div class="d-flex flex-grow-1" i18n>Cash</div>
|
|
||||||
<div class="d-flex justify-content-end">
|
|
||||||
<gf-value
|
|
||||||
class="justify-content-end"
|
|
||||||
[currency]="baseCurrency"
|
|
||||||
[locale]="locale"
|
|
||||||
[value]="isLoading ? undefined : overview?.cash"
|
|
||||||
></gf-value>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col"><hr /></div>
|
|
||||||
</div>
|
|
||||||
<div class="row px-3 py-1">
|
|
||||||
<div class="d-flex flex-grow-1" i18n>Buy</div>
|
|
||||||
<div class="d-flex justify-content-end">
|
|
||||||
<gf-value
|
|
||||||
class="justify-content-end"
|
|
||||||
[currency]="baseCurrency"
|
|
||||||
[locale]="locale"
|
|
||||||
[value]="isLoading ? undefined : overview?.totalBuy"
|
|
||||||
></gf-value>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row px-3 py-1">
|
|
||||||
<div class="d-flex flex-grow-1" i18n>Sell</div>
|
|
||||||
<div class="d-flex justify-content-end">
|
|
||||||
<span
|
|
||||||
*ngIf="overview?.totalSell || overview?.totalSell === 0"
|
|
||||||
class="mr-1"
|
|
||||||
>-</span
|
|
||||||
>
|
|
||||||
<gf-value
|
|
||||||
class="justify-content-end"
|
|
||||||
[currency]="baseCurrency"
|
|
||||||
[locale]="locale"
|
|
||||||
[value]="isLoading ? undefined : overview?.totalSell"
|
|
||||||
></gf-value>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col"><hr /></div>
|
|
||||||
</div>
|
|
||||||
<div class="row px-3">
|
|
||||||
<div class="d-flex flex-grow-1" i18n>Investment</div>
|
|
||||||
<div class="d-flex justify-content-end">
|
|
||||||
<gf-value
|
|
||||||
class="justify-content-end"
|
|
||||||
[currency]="baseCurrency"
|
|
||||||
[locale]="locale"
|
|
||||||
[value]="isLoading ? undefined : overview?.committedFunds"
|
|
||||||
></gf-value>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col"><hr /></div>
|
|
||||||
</div>
|
|
||||||
<div class="row px-3">
|
|
||||||
<div class="d-flex flex-grow-1" i18n>
|
|
||||||
Fees for {{ overview?.ordersCount }} {overview?.ordersCount, plural, =1
|
|
||||||
{order} other {orders}}
|
|
||||||
</div>
|
|
||||||
<div class="d-flex justify-content-end">
|
|
||||||
<gf-value
|
|
||||||
class="justify-content-end"
|
|
||||||
[currency]="baseCurrency"
|
|
||||||
[locale]="locale"
|
|
||||||
[value]="isLoading ? undefined : overview?.fees"
|
|
||||||
></gf-value>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,28 +0,0 @@
|
|||||||
import {
|
|
||||||
ChangeDetectionStrategy,
|
|
||||||
Component,
|
|
||||||
Input,
|
|
||||||
OnChanges,
|
|
||||||
OnInit
|
|
||||||
} from '@angular/core';
|
|
||||||
import { PortfolioOverview } from '@ghostfolio/common/interfaces';
|
|
||||||
import { Currency } from '@prisma/client';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'gf-portfolio-overview',
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
templateUrl: './portfolio-overview.component.html',
|
|
||||||
styleUrls: ['./portfolio-overview.component.scss']
|
|
||||||
})
|
|
||||||
export class PortfolioOverviewComponent implements OnChanges, OnInit {
|
|
||||||
@Input() baseCurrency: Currency;
|
|
||||||
@Input() isLoading: boolean;
|
|
||||||
@Input() locale: string;
|
|
||||||
@Input() overview: PortfolioOverview;
|
|
||||||
|
|
||||||
public constructor() {}
|
|
||||||
|
|
||||||
public ngOnInit() {}
|
|
||||||
|
|
||||||
public ngOnChanges() {}
|
|
||||||
}
|
|
@ -1,54 +0,0 @@
|
|||||||
<div class="container p-0">
|
|
||||||
<div class="row no-gutters">
|
|
||||||
<div class="flex-grow-1"></div>
|
|
||||||
<div *ngIf="isLoading" class="align-items-center d-flex">
|
|
||||||
<ngx-skeleton-loader
|
|
||||||
animation="pulse"
|
|
||||||
class="mb-2"
|
|
||||||
[theme]="{
|
|
||||||
height: '4rem',
|
|
||||||
width: '15rem'
|
|
||||||
}"
|
|
||||||
></ngx-skeleton-loader>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
[hidden]="isLoading"
|
|
||||||
class="display-4 font-weight-bold m-0 text-center value-container"
|
|
||||||
>
|
|
||||||
<span #value id="value"></span>
|
|
||||||
</div>
|
|
||||||
<div class="flex-grow-1 px-1">
|
|
||||||
<ngx-skeleton-loader
|
|
||||||
*ngIf="isLoading"
|
|
||||||
animation="pulse"
|
|
||||||
[theme]="{
|
|
||||||
height: '1.3rem',
|
|
||||||
width: '2.5rem'
|
|
||||||
}"
|
|
||||||
></ngx-skeleton-loader>
|
|
||||||
<div *ngIf="!isLoading">
|
|
||||||
{{ unit }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div *ngIf="showDetails" class="row">
|
|
||||||
<div class="d-flex col justify-content-end">
|
|
||||||
<gf-value
|
|
||||||
[colorizeSign]="true"
|
|
||||||
[isCurrency]="true"
|
|
||||||
[locale]="locale"
|
|
||||||
[value]="isLoading ? undefined : performance?.currentNetPerformance"
|
|
||||||
></gf-value>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<gf-value
|
|
||||||
[colorizeSign]="true"
|
|
||||||
[isPercent]="true"
|
|
||||||
[locale]="locale"
|
|
||||||
[value]="
|
|
||||||
isLoading ? undefined : performance?.currentNetPerformancePercent
|
|
||||||
"
|
|
||||||
></gf-value>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,9 +0,0 @@
|
|||||||
:host {
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
.value-container {
|
|
||||||
#value {
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,65 +0,0 @@
|
|||||||
import {
|
|
||||||
ChangeDetectionStrategy,
|
|
||||||
Component,
|
|
||||||
ElementRef,
|
|
||||||
Input,
|
|
||||||
OnChanges,
|
|
||||||
OnInit,
|
|
||||||
ViewChild
|
|
||||||
} from '@angular/core';
|
|
||||||
import { PortfolioPerformance } from '@ghostfolio/common/interfaces';
|
|
||||||
import { Currency } from '@prisma/client';
|
|
||||||
import { CountUp } from 'countup.js';
|
|
||||||
import { isNumber } from 'lodash';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'gf-portfolio-performance-summary',
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
templateUrl: './portfolio-performance-summary.component.html',
|
|
||||||
styleUrls: ['./portfolio-performance-summary.component.scss']
|
|
||||||
})
|
|
||||||
export class PortfolioPerformanceSummaryComponent implements OnChanges, OnInit {
|
|
||||||
@Input() baseCurrency: Currency;
|
|
||||||
@Input() isLoading: boolean;
|
|
||||||
@Input() locale: string;
|
|
||||||
@Input() performance: PortfolioPerformance;
|
|
||||||
@Input() showDetails: boolean;
|
|
||||||
|
|
||||||
@ViewChild('value') value: ElementRef;
|
|
||||||
|
|
||||||
public unit: string;
|
|
||||||
|
|
||||||
public constructor() {}
|
|
||||||
|
|
||||||
public ngOnInit() {}
|
|
||||||
|
|
||||||
public ngOnChanges() {
|
|
||||||
if (this.isLoading) {
|
|
||||||
if (this.value?.nativeElement) {
|
|
||||||
this.value.nativeElement.innerHTML = '';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (isNumber(this.performance?.currentValue)) {
|
|
||||||
this.unit = this.baseCurrency;
|
|
||||||
|
|
||||||
new CountUp('value', this.performance?.currentValue, {
|
|
||||||
decimalPlaces: 2,
|
|
||||||
duration: 1,
|
|
||||||
separator: `'`
|
|
||||||
}).start();
|
|
||||||
} else if (this.performance?.currentValue === null) {
|
|
||||||
this.unit = '%';
|
|
||||||
|
|
||||||
new CountUp(
|
|
||||||
'value',
|
|
||||||
this.performance?.currentNetPerformancePercent * 100,
|
|
||||||
{
|
|
||||||
decimalPlaces: 2,
|
|
||||||
duration: 0.75,
|
|
||||||
separator: `'`
|
|
||||||
}
|
|
||||||
).start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
|
||||||
|
|
||||||
import { GfValueModule } from '../value/value.module';
|
|
||||||
import { PortfolioPerformanceSummaryComponent } from './portfolio-performance-summary.component';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [PortfolioPerformanceSummaryComponent],
|
|
||||||
exports: [PortfolioPerformanceSummaryComponent],
|
|
||||||
imports: [CommonModule, GfValueModule, NgxSkeletonLoaderModule],
|
|
||||||
providers: []
|
|
||||||
})
|
|
||||||
export class GfPortfolioPerformanceSummaryModule {}
|
|
@ -1,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>
|
||||||
|
@ -1,3 +1,9 @@
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
|
.value-container {
|
||||||
|
#value {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
|
ElementRef,
|
||||||
Input,
|
Input,
|
||||||
OnInit
|
OnChanges,
|
||||||
|
OnInit,
|
||||||
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { PortfolioPerformance } from '@ghostfolio/common/interfaces';
|
import { PortfolioPerformance } from '@ghostfolio/common/interfaces';
|
||||||
import { Currency } from '@prisma/client';
|
import { Currency } from '@prisma/client';
|
||||||
|
import { CountUp } from 'countup.js';
|
||||||
|
import { isNumber } from 'lodash';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'gf-portfolio-performance',
|
selector: 'gf-portfolio-performance',
|
||||||
@ -13,13 +18,48 @@ import { Currency } from '@prisma/client';
|
|||||||
templateUrl: './portfolio-performance.component.html',
|
templateUrl: './portfolio-performance.component.html',
|
||||||
styleUrls: ['./portfolio-performance.component.scss']
|
styleUrls: ['./portfolio-performance.component.scss']
|
||||||
})
|
})
|
||||||
export class PortfolioPerformanceComponent implements OnInit {
|
export class PortfolioPerformanceComponent implements OnChanges, OnInit {
|
||||||
@Input() baseCurrency: Currency;
|
@Input() baseCurrency: Currency;
|
||||||
@Input() isLoading: boolean;
|
@Input() isLoading: boolean;
|
||||||
@Input() locale: string;
|
@Input() locale: string;
|
||||||
@Input() performance: PortfolioPerformance;
|
@Input() performance: PortfolioPerformance;
|
||||||
|
@Input() showDetails: boolean;
|
||||||
|
|
||||||
|
@ViewChild('value') value: ElementRef;
|
||||||
|
|
||||||
|
public unit: string;
|
||||||
|
|
||||||
public constructor() {}
|
public constructor() {}
|
||||||
|
|
||||||
public ngOnInit() {}
|
public ngOnInit() {}
|
||||||
|
|
||||||
|
public ngOnChanges() {
|
||||||
|
if (this.isLoading) {
|
||||||
|
if (this.value?.nativeElement) {
|
||||||
|
this.value.nativeElement.innerHTML = '';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isNumber(this.performance?.currentValue)) {
|
||||||
|
this.unit = this.baseCurrency;
|
||||||
|
|
||||||
|
new CountUp('value', this.performance?.currentValue, {
|
||||||
|
decimalPlaces: 2,
|
||||||
|
duration: 1,
|
||||||
|
separator: `'`
|
||||||
|
}).start();
|
||||||
|
} else if (this.performance?.currentValue === null) {
|
||||||
|
this.unit = '%';
|
||||||
|
|
||||||
|
new CountUp(
|
||||||
|
'value',
|
||||||
|
this.performance?.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
Reference in New Issue
Block a user