Compare commits
44 Commits
Author | SHA1 | Date | |
---|---|---|---|
993a491d24 | |||
631efff7ae | |||
a3d1ac2ce4 | |||
4484c21757 | |||
87cd3ef33f | |||
163f4a3d3f | |||
a84256dc03 | |||
cf82066976 | |||
e248c9cedd | |||
90a2fea7d6 | |||
d17b02092e | |||
c70eb7793e | |||
e3a1d2b9cf | |||
e68ebdf76d | |||
949cf58eef | |||
0816defb95 | |||
a076a1c933 | |||
40c95a541d | |||
2d04d7b8d5 | |||
94e0feac68 | |||
cd9e974c40 | |||
11700f75d9 | |||
0439a7beaa | |||
608b195ba9 | |||
8cb5fd64dd | |||
ef317a86ed | |||
19ada83d0b | |||
6ecf66ea2a | |||
65ba16c07c | |||
aee7c12c44 | |||
125956eb3e | |||
954224401d | |||
d268de3e12 | |||
39cfb4603b | |||
15a70abf67 | |||
5cb69291f5 | |||
cf582b2e98 | |||
82e159a083 | |||
2aff139982 | |||
6c7adb6193 | |||
bb2cd1c85a | |||
9d92c48ab7 | |||
dbed4ea527 | |||
8d149b5e2b |
123
CHANGELOG.md
123
CHANGELOG.md
@ -5,6 +5,113 @@ 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/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## 1.0.0 - 05.05.2021
|
||||
|
||||
### Added
|
||||
|
||||
- Added the functionality to clone a transaction
|
||||
- Added a _Google Play_ badge on the landing page
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed to maskable icons
|
||||
|
||||
## 0.99.0 - 03.05.2021
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for deleting users in the admin control panel
|
||||
|
||||
### Changed
|
||||
|
||||
- Eliminated the platform attribute from the transaction model
|
||||
|
||||
## 0.98.0 - 02.05.2021
|
||||
|
||||
### Added
|
||||
|
||||
- Added the logic to create and update accounts
|
||||
|
||||
## 0.97.0 - 01.05.2021
|
||||
|
||||
### Added
|
||||
|
||||
- Added an account page as a preparation for the multi accounts support
|
||||
|
||||
## 0.96.0 - 30.04.2021
|
||||
|
||||
### Added
|
||||
|
||||
- Added the absolute change to the position detail dialog
|
||||
- Added the number of transactions to the position detail dialog
|
||||
|
||||
### Changed
|
||||
|
||||
- Harmonized the slogan to "Open Source Portfolio Tracker"
|
||||
|
||||
## 0.95.0 - 28.04.2021
|
||||
|
||||
### Added
|
||||
|
||||
- Added a data source attribute to the transactions model
|
||||
|
||||
## 0.94.0 - 27.04.2021
|
||||
|
||||
### Added
|
||||
|
||||
- Added the generic scraper symbols to the symbol lookup results
|
||||
|
||||
## 0.93.0 - 26.04.2021
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the user table styling of the admin control panel
|
||||
- Improved the background colors in the dark mode
|
||||
|
||||
## 0.92.0 - 25.04.2021
|
||||
|
||||
### Added
|
||||
|
||||
- Prepared further for multi accounts support: store account for new transactions
|
||||
- Added a horizontal scrollbar to the user table of the admin control panel
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue in the header with outdated data
|
||||
- Fixed an issue on the about page with outdated data
|
||||
|
||||
## 0.91.0 - 25.04.2021
|
||||
|
||||
### Added
|
||||
|
||||
- Extended the support for feature flags to simplify the initial project setup
|
||||
- Prepared for multi accounts support
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the styling of the rules in the _X-ray_ section
|
||||
|
||||
## 0.90.0 - 22.04.2021
|
||||
|
||||
### Added
|
||||
|
||||
- Added the symbol logo to the position detail dialog
|
||||
- Introduced a third option for the market state: `delayed` (besides `open` and `closed`)
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the user table of the admin control panel
|
||||
|
||||
## 0.89.0 - 21.04.2021
|
||||
|
||||
### Added
|
||||
|
||||
- Added a prettifier (pipe) for generic scraper symbols
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the text truncation in buttons of the admin control panel
|
||||
|
||||
## 0.88.0 - 20.04.2021
|
||||
|
||||
### Changed
|
||||
@ -74,7 +181,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue in the portfolio update on deleting a transaction
|
||||
- Fixed an issue in the _X-Ray_ section (missing redirection on logout)
|
||||
- Fixed an issue in the _X-ray_ section (missing redirection on logout)
|
||||
|
||||
## 0.82.0 - 10.04.2021
|
||||
|
||||
@ -147,7 +254,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Changed
|
||||
|
||||
- Grouped the _X-Ray_ section visually in _Currency Cluster Risk_ and _Platform Cluster Risk_
|
||||
- Grouped the _X-ray_ section visually in _Currency Cluster Risk_ and _Platform Cluster Risk_
|
||||
|
||||
## 0.76.0 - 02.04.2021
|
||||
|
||||
@ -159,7 +266,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue in the _X-Ray_ section (empty portfolio)
|
||||
- Fixed an issue in the _X-ray_ section (empty portfolio)
|
||||
|
||||
## 0.75.0 - 01.04.2021
|
||||
|
||||
@ -172,7 +279,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Added
|
||||
|
||||
- Added a _Create Account_ message in the _Live Demo_
|
||||
- Added skeleton loaders to the _X-Ray_ section
|
||||
- Added skeleton loaders to the _X-ray_ section
|
||||
|
||||
### Changed
|
||||
|
||||
@ -188,7 +295,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the intro text in the _X-Ray_ section
|
||||
- Improved the intro text in the _X-ray_ section
|
||||
|
||||
### Fixed
|
||||
|
||||
@ -204,7 +311,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- Added an intro text to the _X-Ray_ section
|
||||
- Added an intro text to the _X-ray_ section
|
||||
|
||||
### Changed
|
||||
|
||||
@ -223,7 +330,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the styling in the _X-Ray_ section
|
||||
- Improved the styling in the _X-ray_ section
|
||||
|
||||
## 0.70.0 - 27.03.2021
|
||||
|
||||
@ -231,7 +338,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
- Added the current _Fear & Greed Index_ as text
|
||||
- Extended the landing page text: _Ghostfolio_ empowers busy folks...
|
||||
- Added the first static portfolio analysis rule in the brand new _X-Ray_ section
|
||||
- Added the first static portfolio analysis rule in the brand new _X-ray_ section
|
||||
|
||||
### Changed
|
||||
|
||||
|
13
README.md
13
README.md
@ -61,7 +61,7 @@ The frontend is built with [Angular](https://angular.io).
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Node.js](https://nodejs.org/en/download)
|
||||
- [Node.js](https://nodejs.org/en/download) (version 14+)
|
||||
- [Yarn](https://yarnpkg.com/en/docs/install)
|
||||
- [Docker](https://www.docker.com/products/docker-desktop)
|
||||
|
||||
@ -71,12 +71,17 @@ The frontend is built with [Angular](https://angular.io).
|
||||
2. Run `cd docker`
|
||||
3. Run `docker compose build`
|
||||
4. Run `docker compose up -d` to start [PostgreSQL](https://www.postgresql.org) and [Redis](https://redis.io)
|
||||
5. Run `yarn setup:database` to initialize the database schema and populate your database with (example) data
|
||||
6. Start server and client (see _Development_)
|
||||
7. Login as _Admin_ with the following _Security Token_: `ae76872ae8f3419c6d6f64bf51888ecbcc703927a342d815fafe486acdb938da07d0cf44fca211a0be74a423238f535362d390a41e81e633a9ce668a6e31cdf9`
|
||||
5. Run `cd -` to go back to the project root directory
|
||||
6. Run `yarn setup:database` to initialize the database schema and populate your database with (example) data
|
||||
7. Start server and client (see _Development_)
|
||||
8. Login as _Admin_ with the following _Security Token_: `ae76872ae8f3419c6d6f64bf51888ecbcc703927a342d815fafe486acdb938da07d0cf44fca211a0be74a423238f535362d390a41e81e633a9ce668a6e31cdf9`
|
||||
9. Go to the _Admin Control Panel_ and press _Gather All Data_ to fetch historical data
|
||||
10. Press _Sign out_ and check out the _Live Demo_
|
||||
|
||||
## Development
|
||||
|
||||
Please make sure you have completed the instructions from _Setup_
|
||||
|
||||
### Start server
|
||||
|
||||
- Debug: Run `yarn watch:server` and click "Launch Program" in _Visual Studio Code_
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { RequestWithUser } from '@ghostfolio/api/app/interfaces/request-with-user.type';
|
||||
import { Controller, Get, Inject, UseGuards } from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { RequestWithUser } from 'apps/api/src/app/interfaces/request-with-user.type';
|
||||
|
||||
import { AccessService } from './access.service';
|
||||
import { Access } from './interfaces/access.interface';
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { PrismaService } from '../../services/prisma.service';
|
||||
import { AccessController } from './access.controller';
|
||||
import { AccessService } from './access.service';
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
import { PrismaService } from '../../services/prisma.service';
|
||||
import { AccessWithGranteeUser } from './interfaces/access-with-grantee-user.type';
|
||||
|
||||
@Injectable()
|
||||
|
237
apps/api/src/app/account/account.controller.ts
Normal file
237
apps/api/src/app/account/account.controller.ts
Normal file
@ -0,0 +1,237 @@
|
||||
import { RequestWithUser } from '@ghostfolio/api/app/interfaces/request-with-user.type';
|
||||
import { nullifyValuesInObjects } from '@ghostfolio/api/helper/object.helper';
|
||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
||||
import { getPermissions, hasPermission, permissions } from '@ghostfolio/helper';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Headers,
|
||||
HttpException,
|
||||
Inject,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
UseGuards
|
||||
} from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { Account as AccountModel, Order } from '@prisma/client';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
|
||||
import { AccountService } from './account.service';
|
||||
import { CreateAccountDto } from './create-account.dto';
|
||||
import { UpdateAccountDto } from './update-account.dto';
|
||||
|
||||
@Controller('account')
|
||||
export class AccountController {
|
||||
public constructor(
|
||||
private readonly accountService: AccountService,
|
||||
private readonly impersonationService: ImpersonationService,
|
||||
@Inject(REQUEST) private readonly request: RequestWithUser
|
||||
) {}
|
||||
|
||||
@Delete(':id')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async deleteAccount(@Param('id') id: string): Promise<AccountModel> {
|
||||
if (
|
||||
!hasPermission(
|
||||
getPermissions(this.request.user.role),
|
||||
permissions.deleteAccount
|
||||
)
|
||||
) {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||
StatusCodes.FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
const account = await this.accountService.accountWithOrders(
|
||||
{
|
||||
id_userId: {
|
||||
id,
|
||||
userId: this.request.user.id
|
||||
}
|
||||
},
|
||||
{ Order: true }
|
||||
);
|
||||
|
||||
if (account?.isDefault || account?.Order.length > 0) {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||
StatusCodes.FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
return this.accountService.deleteAccount(
|
||||
{
|
||||
id_userId: {
|
||||
id,
|
||||
userId: this.request.user.id
|
||||
}
|
||||
},
|
||||
this.request.user.id
|
||||
);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async getAllAccounts(
|
||||
@Headers('impersonation-id') impersonationId
|
||||
): Promise<AccountModel[]> {
|
||||
const impersonationUserId = await this.impersonationService.validateImpersonationId(
|
||||
impersonationId,
|
||||
this.request.user.id
|
||||
);
|
||||
|
||||
let accounts = await this.accountService.accounts({
|
||||
include: { Order: true, Platform: true },
|
||||
orderBy: { name: 'asc' },
|
||||
where: { userId: impersonationUserId || this.request.user.id }
|
||||
});
|
||||
|
||||
if (
|
||||
impersonationUserId &&
|
||||
!hasPermission(
|
||||
getPermissions(this.request.user.role),
|
||||
permissions.readForeignPortfolio
|
||||
)
|
||||
) {
|
||||
accounts = nullifyValuesInObjects(accounts, [
|
||||
'fee',
|
||||
'quantity',
|
||||
'unitPrice'
|
||||
]);
|
||||
}
|
||||
|
||||
return accounts;
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async getAccountById(@Param('id') id: string): Promise<AccountModel> {
|
||||
return this.accountService.account({
|
||||
id_userId: {
|
||||
id,
|
||||
userId: this.request.user.id
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Post()
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async createAccount(
|
||||
@Body() data: CreateAccountDto
|
||||
): Promise<AccountModel> {
|
||||
if (
|
||||
!hasPermission(
|
||||
getPermissions(this.request.user.role),
|
||||
permissions.createAccount
|
||||
)
|
||||
) {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||
StatusCodes.FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
if (data.platformId) {
|
||||
const platformId = data.platformId;
|
||||
delete data.platformId;
|
||||
|
||||
return this.accountService.createAccount(
|
||||
{
|
||||
...data,
|
||||
Platform: { connect: { id: platformId } },
|
||||
User: { connect: { id: this.request.user.id } }
|
||||
},
|
||||
this.request.user.id
|
||||
);
|
||||
} else {
|
||||
delete data.platformId;
|
||||
|
||||
return this.accountService.createAccount(
|
||||
{
|
||||
...data,
|
||||
User: { connect: { id: this.request.user.id } }
|
||||
},
|
||||
this.request.user.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async update(@Param('id') id: string, @Body() data: UpdateAccountDto) {
|
||||
if (
|
||||
!hasPermission(
|
||||
getPermissions(this.request.user.role),
|
||||
permissions.updateAccount
|
||||
)
|
||||
) {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||
StatusCodes.FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
const originalAccount = await this.accountService.account({
|
||||
id_userId: {
|
||||
id,
|
||||
userId: this.request.user.id
|
||||
}
|
||||
});
|
||||
|
||||
if (!originalAccount) {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||
StatusCodes.FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
if (data.platformId) {
|
||||
const platformId = data.platformId;
|
||||
delete data.platformId;
|
||||
|
||||
return this.accountService.updateAccount(
|
||||
{
|
||||
data: {
|
||||
...data,
|
||||
Platform: { connect: { id: platformId } },
|
||||
User: { connect: { id: this.request.user.id } }
|
||||
},
|
||||
where: {
|
||||
id_userId: {
|
||||
id,
|
||||
userId: this.request.user.id
|
||||
}
|
||||
}
|
||||
},
|
||||
this.request.user.id
|
||||
);
|
||||
} else {
|
||||
// platformId is null, remove it
|
||||
delete data.platformId;
|
||||
|
||||
return this.accountService.updateAccount(
|
||||
{
|
||||
data: {
|
||||
...data,
|
||||
Platform: originalAccount.platformId
|
||||
? { disconnect: true }
|
||||
: undefined,
|
||||
User: { connect: { id: this.request.user.id } }
|
||||
},
|
||||
where: {
|
||||
id_userId: {
|
||||
id,
|
||||
userId: this.request.user.id
|
||||
}
|
||||
}
|
||||
},
|
||||
this.request.user.id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
30
apps/api/src/app/account/account.module.ts
Normal file
30
apps/api/src/app/account/account.module.ts
Normal file
@ -0,0 +1,30 @@
|
||||
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 { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { RedisCacheModule } from '../redis-cache/redis-cache.module';
|
||||
import { AccountController } from './account.controller';
|
||||
import { AccountService } from './account.service';
|
||||
|
||||
@Module({
|
||||
imports: [RedisCacheModule],
|
||||
controllers: [AccountController],
|
||||
providers: [
|
||||
AccountService,
|
||||
AlphaVantageService,
|
||||
ConfigurationService,
|
||||
DataProviderService,
|
||||
GhostfolioScraperApiService,
|
||||
ImpersonationService,
|
||||
PrismaService,
|
||||
RakutenRapidApiService,
|
||||
YahooFinanceService
|
||||
]
|
||||
})
|
||||
export class AccountModule {}
|
89
apps/api/src/app/account/account.service.ts
Normal file
89
apps/api/src/app/account/account.service.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Account, Order, Prisma } from '@prisma/client';
|
||||
|
||||
import { RedisCacheService } from '../redis-cache/redis-cache.service';
|
||||
|
||||
@Injectable()
|
||||
export class AccountService {
|
||||
public constructor(
|
||||
private readonly redisCacheService: RedisCacheService,
|
||||
private prisma: PrismaService
|
||||
) {}
|
||||
|
||||
public async account(
|
||||
accountWhereUniqueInput: Prisma.AccountWhereUniqueInput
|
||||
): Promise<Account | null> {
|
||||
return this.prisma.account.findUnique({
|
||||
where: accountWhereUniqueInput
|
||||
});
|
||||
}
|
||||
|
||||
public async accountWithOrders(
|
||||
accountWhereUniqueInput: Prisma.AccountWhereUniqueInput,
|
||||
accountInclude: Prisma.AccountInclude
|
||||
): Promise<
|
||||
Account & {
|
||||
Order?: Order[];
|
||||
}
|
||||
> {
|
||||
return this.prisma.account.findUnique({
|
||||
include: accountInclude,
|
||||
where: accountWhereUniqueInput
|
||||
});
|
||||
}
|
||||
|
||||
public async accounts(params: {
|
||||
include?: Prisma.AccountInclude;
|
||||
skip?: number;
|
||||
take?: number;
|
||||
cursor?: Prisma.AccountWhereUniqueInput;
|
||||
where?: Prisma.AccountWhereInput;
|
||||
orderBy?: Prisma.AccountOrderByInput;
|
||||
}): Promise<Account[]> {
|
||||
const { include, skip, take, cursor, where, orderBy } = params;
|
||||
|
||||
return this.prisma.account.findMany({
|
||||
cursor,
|
||||
include,
|
||||
orderBy,
|
||||
skip,
|
||||
take,
|
||||
where
|
||||
});
|
||||
}
|
||||
|
||||
public async createAccount(
|
||||
data: Prisma.AccountCreateInput,
|
||||
aUserId: string
|
||||
): Promise<Account> {
|
||||
return this.prisma.account.create({
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
public async deleteAccount(
|
||||
where: Prisma.AccountWhereUniqueInput,
|
||||
aUserId: string
|
||||
): Promise<Account> {
|
||||
this.redisCacheService.remove(`${aUserId}.portfolio`);
|
||||
|
||||
return this.prisma.account.delete({
|
||||
where
|
||||
});
|
||||
}
|
||||
|
||||
public async updateAccount(
|
||||
params: {
|
||||
where: Prisma.AccountWhereUniqueInput;
|
||||
data: Prisma.AccountUpdateInput;
|
||||
},
|
||||
aUserId: string
|
||||
): Promise<Account> {
|
||||
const { data, where } = params;
|
||||
return this.prisma.account.update({
|
||||
data,
|
||||
where
|
||||
});
|
||||
}
|
||||
}
|
14
apps/api/src/app/account/create-account.dto.ts
Normal file
14
apps/api/src/app/account/create-account.dto.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { AccountType } from '@prisma/client';
|
||||
import { IsString, ValidateIf } from 'class-validator';
|
||||
|
||||
export class CreateAccountDto {
|
||||
@IsString()
|
||||
accountType: AccountType;
|
||||
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
@ValidateIf((object, value) => value !== null)
|
||||
platformId: string | null;
|
||||
}
|
17
apps/api/src/app/account/update-account.dto.ts
Normal file
17
apps/api/src/app/account/update-account.dto.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { AccountType } from '@prisma/client';
|
||||
import { IsString, ValidateIf } from 'class-validator';
|
||||
|
||||
export class UpdateAccountDto {
|
||||
@IsString()
|
||||
accountType: AccountType;
|
||||
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
@ValidateIf((object, value) => value !== null)
|
||||
platformId: string | null;
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
import { RequestWithUser } from '@ghostfolio/api/app/interfaces/request-with-user.type';
|
||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||
import { getPermissions, hasPermission, permissions } from '@ghostfolio/helper';
|
||||
import {
|
||||
Controller,
|
||||
@ -9,10 +11,8 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { RequestWithUser } from 'apps/api/src/app/interfaces/request-with-user.type';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
|
||||
import { DataGatheringService } from '../../services/data-gathering.service';
|
||||
import { AdminService } from './admin.service';
|
||||
import { AdminData } from './interfaces/admin-data.interface';
|
||||
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
||||
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
|
||||
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
||||
import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
|
||||
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { ConfigurationService } from '../../services/configuration.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 { AdminController } from './admin.controller';
|
||||
import { AdminService } from './admin.service';
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Currency } from '@prisma/client';
|
||||
|
||||
import { ExchangeRateDataService } from '../../services/exchange-rate-data.service';
|
||||
import { PrismaService } from '../../services/prisma.service';
|
||||
import { AdminData } from './interfaces/admin-data.interface';
|
||||
|
||||
@Injectable()
|
||||
@ -97,7 +97,7 @@ export class AdminService {
|
||||
},
|
||||
select: {
|
||||
_count: {
|
||||
select: { Order: true }
|
||||
select: { Account: true, Order: true }
|
||||
},
|
||||
alias: true,
|
||||
Analytics: {
|
||||
@ -109,7 +109,12 @@ export class AdminService {
|
||||
createdAt: true,
|
||||
id: true
|
||||
},
|
||||
take: 20
|
||||
take: 20,
|
||||
where: {
|
||||
NOT: {
|
||||
Analytics: null
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import { YahooFinanceService } from '../services/data-provider/yahoo-finance/yah
|
||||
import { ExchangeRateDataService } from '../services/exchange-rate-data.service';
|
||||
import { PrismaService } from '../services/prisma.service';
|
||||
import { AccessModule } from './access/access.module';
|
||||
import { AccountModule } from './account/account.module';
|
||||
import { AdminModule } from './admin/admin.module';
|
||||
import { AppController } from './app.controller';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
@ -32,6 +33,7 @@ import { UserModule } from './user/user.module';
|
||||
imports: [
|
||||
AdminModule,
|
||||
AccessModule,
|
||||
AccountModule,
|
||||
AuthModule,
|
||||
CacheModule,
|
||||
ConfigModule.forRoot(),
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
@ -10,7 +11,6 @@ import {
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
|
||||
import { ConfigurationService } from '../../services/configuration.service';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
@Controller('auth')
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
|
||||
import { ConfigurationService } from '../../services/configuration.service';
|
||||
import { PrismaService } from '../../services/prisma.service';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { AuthService } from './auth.service';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { Injectable, InternalServerErrorException } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
|
||||
import { ConfigurationService } from '../../services/configuration.service';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { ValidateOAuthLoginParams } from './interfaces/interfaces';
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Provider } from '@prisma/client';
|
||||
import { Strategy } from 'passport-google-oauth20';
|
||||
|
||||
import { ConfigurationService } from '../../services/configuration.service';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
@Injectable()
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
|
||||
import { ConfigurationService } from '../../services/configuration.service';
|
||||
import { PrismaService } from '../../services/prisma.service';
|
||||
import { UserService } from '../user/user.service';
|
||||
|
||||
@Injectable()
|
||||
|
2
apps/api/src/app/cache/cache.controller.ts
vendored
2
apps/api/src/app/cache/cache.controller.ts
vendored
@ -1,7 +1,7 @@
|
||||
import { RequestWithUser } from '@ghostfolio/api/app/interfaces/request-with-user.type';
|
||||
import { Controller, Inject, Param, Post, UseGuards } from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { RequestWithUser } from 'apps/api/src/app/interfaces/request-with-user.type';
|
||||
|
||||
import { RedisCacheService } from '../redis-cache/redis-cache.service';
|
||||
import { CacheService } from './cache.service';
|
||||
|
2
apps/api/src/app/cache/cache.module.ts
vendored
2
apps/api/src/app/cache/cache.module.ts
vendored
@ -1,6 +1,6 @@
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { PrismaService } from '../../services/prisma.service';
|
||||
import { RedisCacheModule } from '../redis-cache/redis-cache.module';
|
||||
import { CacheController } from './cache.controller';
|
||||
import { CacheService } from './cache.service';
|
||||
|
4
apps/api/src/app/cache/cache.service.ts
vendored
4
apps/api/src/app/cache/cache.service.ts
vendored
@ -1,7 +1,5 @@
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma, User } from '@prisma/client';
|
||||
|
||||
import { PrismaService } from '../../services/prisma.service';
|
||||
|
||||
@Injectable()
|
||||
export class CacheService {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { RequestWithUser } from '@ghostfolio/api/app/interfaces/request-with-user.type';
|
||||
import {
|
||||
baseCurrency,
|
||||
benchmarks,
|
||||
@ -14,7 +15,6 @@ import {
|
||||
Post
|
||||
} from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { RequestWithUser } from 'apps/api/src/app/interfaces/request-with-user.type';
|
||||
import { parse } from 'date-fns';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
|
||||
|
@ -1,14 +1,14 @@
|
||||
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 { RulesService } from '@ghostfolio/api/services/rules.service';
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { ConfigurationService } from '../../services/configuration.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 { RulesService } from '../../services/rules.service';
|
||||
import { ExperimentalController } from './experimental.controller';
|
||||
import { ExperimentalService } from './experimental.service';
|
||||
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { Portfolio } from '@ghostfolio/api/models/portfolio';
|
||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider.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 { Injectable } from '@nestjs/common';
|
||||
import { Currency, Type } from '@prisma/client';
|
||||
import { parseISO } from 'date-fns';
|
||||
|
||||
import { Portfolio } from '../../models/portfolio';
|
||||
import { DataProviderService } from '../../services/data-provider.service';
|
||||
import { ExchangeRateDataService } from '../../services/exchange-rate-data.service';
|
||||
import { PrismaService } from '../../services/prisma.service';
|
||||
import { RulesService } from '../../services/rules.service';
|
||||
import { OrderWithPlatform } from '../order/interfaces/order-with-platform.type';
|
||||
import { OrderWithAccount } from '../order/interfaces/order-with-account.type';
|
||||
import { CreateOrderDto } from './create-order.dto';
|
||||
import { Data } from './interfaces/data.interface';
|
||||
|
||||
@ -33,10 +33,13 @@ export class ExperimentalService {
|
||||
aDate: Date,
|
||||
aBaseCurrency: Currency
|
||||
): Promise<Data> {
|
||||
const ordersWithPlatform: OrderWithPlatform[] = aOrders.map((order) => {
|
||||
const ordersWithPlatform: OrderWithAccount[] = aOrders.map((order) => {
|
||||
return {
|
||||
...order,
|
||||
accountId: undefined,
|
||||
accountUserId: undefined,
|
||||
createdAt: new Date(),
|
||||
dataSource: undefined,
|
||||
date: parseISO(order.date),
|
||||
fee: 0,
|
||||
id: undefined,
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
|
||||
import { ConfigurationService } from '../../services/configuration.service';
|
||||
import { PrismaService } from '../../services/prisma.service';
|
||||
import { InfoController } from './info.controller';
|
||||
import { InfoService } from './info.service';
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { permissions } from '@ghostfolio/helper';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { Currency } from '@prisma/client';
|
||||
|
||||
import { ConfigurationService } from '../../services/configuration.service';
|
||||
import { PrismaService } from '../../services/prisma.service';
|
||||
import { InfoItem } from './interfaces/info-item.interface';
|
||||
|
||||
@Injectable()
|
||||
@ -26,7 +26,11 @@ export class InfoService {
|
||||
const globalPermissions: string[] = [];
|
||||
|
||||
if (this.configurationService.get('ENABLE_FEATURE_SOCIAL_LOGIN')) {
|
||||
globalPermissions.push(permissions.useSocialLogin);
|
||||
globalPermissions.push(permissions.enableSocialLogin);
|
||||
}
|
||||
|
||||
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
||||
globalPermissions.push(permissions.enableSubscription);
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -1,3 +1,6 @@
|
||||
import { Settings, User } from '@prisma/client';
|
||||
import { Account, Settings, User } from '@prisma/client';
|
||||
|
||||
export type UserWithSettings = User & { Settings: Settings };
|
||||
export type UserWithSettings = User & {
|
||||
Account: Account[];
|
||||
Settings: Settings;
|
||||
};
|
||||
|
@ -1,20 +1,22 @@
|
||||
import { Currency, Type } from '@prisma/client';
|
||||
import { Currency, DataSource, Type } from '@prisma/client';
|
||||
import { IsISO8601, IsNumber, IsString, ValidateIf } from 'class-validator';
|
||||
|
||||
export class CreateOrderDto {
|
||||
@IsString()
|
||||
accountId: string;
|
||||
|
||||
@IsString()
|
||||
currency: Currency;
|
||||
|
||||
@IsString()
|
||||
dataSource: DataSource;
|
||||
|
||||
@IsISO8601()
|
||||
date: string;
|
||||
|
||||
@IsNumber()
|
||||
fee: number;
|
||||
|
||||
@IsString()
|
||||
@ValidateIf((object, value) => value !== null)
|
||||
platformId: string | null;
|
||||
|
||||
@IsNumber()
|
||||
quantity: number;
|
||||
|
||||
|
@ -0,0 +1,3 @@
|
||||
import { Account, Order } from '@prisma/client';
|
||||
|
||||
export type OrderWithAccount = Order & { Account?: Account };
|
@ -1,3 +0,0 @@
|
||||
import { Order, Platform } from '@prisma/client';
|
||||
|
||||
export type OrderWithPlatform = Order & { Platform?: Platform };
|
@ -1,3 +1,6 @@
|
||||
import { RequestWithUser } from '@ghostfolio/api/app/interfaces/request-with-user.type';
|
||||
import { nullifyValuesInObjects } from '@ghostfolio/api/helper/object.helper';
|
||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
||||
import { getPermissions, hasPermission, permissions } from '@ghostfolio/helper';
|
||||
import {
|
||||
Body,
|
||||
@ -15,12 +18,9 @@ import {
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { Order as OrderModel } from '@prisma/client';
|
||||
import { RequestWithUser } from 'apps/api/src/app/interfaces/request-with-user.type';
|
||||
import { parseISO } from 'date-fns';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
|
||||
import { nullifyValuesInObjects } from '../../helper/object.helper';
|
||||
import { ImpersonationService } from '../../services/impersonation.service';
|
||||
import { CreateOrderDto } from './create-order.dto';
|
||||
import { OrderService } from './order.service';
|
||||
import { UpdateOrderDto } from './update-order.dto';
|
||||
@ -71,7 +71,11 @@ export class OrderController {
|
||||
|
||||
let orders = await this.orderService.orders({
|
||||
include: {
|
||||
Platform: true
|
||||
Account: {
|
||||
include: {
|
||||
Platform: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: { date: 'desc' },
|
||||
where: { userId: impersonationUserId || this.request.user.id }
|
||||
@ -118,31 +122,22 @@ export class OrderController {
|
||||
|
||||
const date = parseISO(data.date);
|
||||
|
||||
if (data.platformId) {
|
||||
const platformId = data.platformId;
|
||||
delete data.platformId;
|
||||
const accountId = data.accountId;
|
||||
delete data.accountId;
|
||||
|
||||
return this.orderService.createOrder(
|
||||
{
|
||||
...data,
|
||||
date,
|
||||
Platform: { connect: { id: platformId } },
|
||||
User: { connect: { id: this.request.user.id } }
|
||||
return this.orderService.createOrder(
|
||||
{
|
||||
...data,
|
||||
date,
|
||||
Account: {
|
||||
connect: {
|
||||
id_userId: { id: accountId, userId: this.request.user.id }
|
||||
}
|
||||
},
|
||||
this.request.user.id
|
||||
);
|
||||
} else {
|
||||
delete data.platformId;
|
||||
|
||||
return this.orderService.createOrder(
|
||||
{
|
||||
...data,
|
||||
date,
|
||||
User: { connect: { id: this.request.user.id } }
|
||||
},
|
||||
this.request.user.id
|
||||
);
|
||||
}
|
||||
User: { connect: { id: this.request.user.id } }
|
||||
},
|
||||
this.request.user.id
|
||||
);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@ -167,52 +162,38 @@ export class OrderController {
|
||||
}
|
||||
});
|
||||
|
||||
const date = parseISO(data.date);
|
||||
|
||||
if (data.platformId) {
|
||||
const platformId = data.platformId;
|
||||
delete data.platformId;
|
||||
|
||||
return this.orderService.updateOrder(
|
||||
{
|
||||
data: {
|
||||
...data,
|
||||
date,
|
||||
Platform: { connect: { id: platformId } },
|
||||
User: { connect: { id: this.request.user.id } }
|
||||
},
|
||||
where: {
|
||||
id_userId: {
|
||||
id,
|
||||
userId: this.request.user.id
|
||||
}
|
||||
}
|
||||
},
|
||||
this.request.user.id
|
||||
);
|
||||
} else {
|
||||
// platformId is null, remove it
|
||||
delete data.platformId;
|
||||
|
||||
return this.orderService.updateOrder(
|
||||
{
|
||||
data: {
|
||||
...data,
|
||||
date,
|
||||
Platform: originalOrder.platformId
|
||||
? { disconnect: true }
|
||||
: undefined,
|
||||
User: { connect: { id: this.request.user.id } }
|
||||
},
|
||||
where: {
|
||||
id_userId: {
|
||||
id,
|
||||
userId: this.request.user.id
|
||||
}
|
||||
}
|
||||
},
|
||||
this.request.user.id
|
||||
if (!originalOrder) {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||
StatusCodes.FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
const date = parseISO(data.date);
|
||||
|
||||
const accountId = data.accountId;
|
||||
delete data.accountId;
|
||||
|
||||
return this.orderService.updateOrder(
|
||||
{
|
||||
data: {
|
||||
...data,
|
||||
date,
|
||||
Account: {
|
||||
connect: {
|
||||
id_userId: { id: accountId, userId: this.request.user.id }
|
||||
}
|
||||
},
|
||||
User: { connect: { id: this.request.user.id } }
|
||||
},
|
||||
where: {
|
||||
id_userId: {
|
||||
id,
|
||||
userId: this.request.user.id
|
||||
}
|
||||
}
|
||||
},
|
||||
this.request.user.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
||||
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
|
||||
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
||||
import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
|
||||
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
|
||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { ConfigurationService } from '../../services/configuration.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 { ImpersonationService } from '../../services/impersonation.service';
|
||||
import { PrismaService } from '../../services/prisma.service';
|
||||
import { CacheService } from '../cache/cache.service';
|
||||
import { RedisCacheModule } from '../redis-cache/redis-cache.module';
|
||||
import { OrderController } from './order.controller';
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Order, Prisma } from '@prisma/client';
|
||||
|
||||
import { DataGatheringService } from '../../services/data-gathering.service';
|
||||
import { PrismaService } from '../../services/prisma.service';
|
||||
import { CacheService } from '../cache/cache.service';
|
||||
import { RedisCacheService } from '../redis-cache/redis-cache.service';
|
||||
import { OrderWithPlatform } from './interfaces/order-with-platform.type';
|
||||
import { OrderWithAccount } from './interfaces/order-with-account.type';
|
||||
|
||||
@Injectable()
|
||||
export class OrderService {
|
||||
@ -31,7 +31,7 @@ export class OrderService {
|
||||
cursor?: Prisma.OrderWhereUniqueInput;
|
||||
where?: Prisma.OrderWhereInput;
|
||||
orderBy?: Prisma.OrderOrderByInput;
|
||||
}): Promise<OrderWithPlatform[]> {
|
||||
}): Promise<OrderWithAccount[]> {
|
||||
const { include, skip, take, cursor, where, orderBy } = params;
|
||||
|
||||
return this.prisma.order.findMany({
|
||||
|
@ -1,20 +1,22 @@
|
||||
import { Currency, Type } from '@prisma/client';
|
||||
import { Currency, DataSource, Type } from '@prisma/client';
|
||||
import { IsISO8601, IsNumber, IsString, ValidateIf } from 'class-validator';
|
||||
|
||||
export class UpdateOrderDto {
|
||||
@IsString()
|
||||
accountId: string;
|
||||
|
||||
@IsString()
|
||||
currency: Currency;
|
||||
|
||||
@IsString()
|
||||
dataSource: DataSource;
|
||||
|
||||
@IsISO8601()
|
||||
date: string;
|
||||
|
||||
@IsNumber()
|
||||
fee: number;
|
||||
|
||||
@IsString()
|
||||
@ValidateIf((object, value) => value !== null)
|
||||
platformId: string | null;
|
||||
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
|
@ -16,4 +16,5 @@ export interface Position {
|
||||
investmentInOriginalCurrency?: number;
|
||||
marketPrice?: number;
|
||||
quantity: number;
|
||||
transactionCount: number;
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { Currency } from '@prisma/client';
|
||||
|
||||
export interface PortfolioPositionDetail {
|
||||
averagePrice: number;
|
||||
currency: string;
|
||||
currency: Currency;
|
||||
firstBuyDate: string;
|
||||
grossPerformance: number;
|
||||
grossPerformancePercent: number;
|
||||
@ -11,6 +13,7 @@ export interface PortfolioPositionDetail {
|
||||
minPrice: number;
|
||||
quantity: number;
|
||||
symbol: string;
|
||||
transactionCount: number;
|
||||
}
|
||||
|
||||
export interface HistoricalDataItem {
|
||||
|
@ -1,24 +1,26 @@
|
||||
import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { Currency } from '@prisma/client';
|
||||
|
||||
export interface PortfolioPosition {
|
||||
accounts: {
|
||||
[name: string]: { current: number; original: number };
|
||||
};
|
||||
currency: Currency;
|
||||
exchange?: string;
|
||||
grossPerformance: number;
|
||||
grossPerformancePercent: number;
|
||||
industry?: string;
|
||||
investment: number;
|
||||
isMarketOpen: boolean;
|
||||
marketChange?: number;
|
||||
marketChangePercent?: number;
|
||||
marketPrice: number;
|
||||
marketState: MarketState;
|
||||
name: string;
|
||||
platforms: {
|
||||
[name: string]: { current: number; original: number };
|
||||
};
|
||||
quantity: number;
|
||||
sector?: string;
|
||||
shareCurrent: number;
|
||||
shareInvestment: number;
|
||||
transactionCount: number;
|
||||
symbol: string;
|
||||
type?: string;
|
||||
url?: string;
|
||||
|
@ -1,3 +1,9 @@
|
||||
import {
|
||||
hasNotDefinedValuesInObject,
|
||||
nullifyValuesInObject
|
||||
} from '@ghostfolio/api/helper/object.helper';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
||||
import { getPermissions, hasPermission, permissions } from '@ghostfolio/helper';
|
||||
import {
|
||||
Controller,
|
||||
@ -15,12 +21,6 @@ import { AuthGuard } from '@nestjs/passport';
|
||||
import { Response } from 'express';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
|
||||
import {
|
||||
hasNotDefinedValuesInObject,
|
||||
nullifyValuesInObject
|
||||
} from '../../helper/object.helper';
|
||||
import { ExchangeRateDataService } from '../../services/exchange-rate-data.service';
|
||||
import { ImpersonationService } from '../../services/impersonation.service';
|
||||
import { RequestWithUser } from '../interfaces/request-with-user.type';
|
||||
import { PortfolioItem } from './interfaces/portfolio-item.interface';
|
||||
import { PortfolioOverview } from './interfaces/portfolio-overview.interface';
|
||||
@ -185,11 +185,11 @@ export class PortfolioController {
|
||||
portfolioPosition.investment =
|
||||
portfolioPosition.investment / totalInvestment;
|
||||
|
||||
for (const [platform, { current, original }] of Object.entries(
|
||||
portfolioPosition.platforms
|
||||
for (const [account, { current, original }] of Object.entries(
|
||||
portfolioPosition.accounts
|
||||
)) {
|
||||
portfolioPosition.platforms[platform].current = current / totalValue;
|
||||
portfolioPosition.platforms[platform].original =
|
||||
portfolioPosition.accounts[account].current = current / totalValue;
|
||||
portfolioPosition.accounts[account].original =
|
||||
original / totalInvestment;
|
||||
}
|
||||
|
||||
|
@ -1,16 +1,16 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
||||
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
|
||||
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
||||
import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
|
||||
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
|
||||
import { 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 { Module } from '@nestjs/common';
|
||||
|
||||
import { ConfigurationService } from '../../services/configuration.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 { ImpersonationService } from '../../services/impersonation.service';
|
||||
import { PrismaService } from '../../services/prisma.service';
|
||||
import { RulesService } from '../../services/rules.service';
|
||||
import { CacheService } from '../cache/cache.service';
|
||||
import { OrderService } from '../order/order.service';
|
||||
import { RedisCacheModule } from '../redis-cache/redis-cache.module';
|
||||
|
@ -1,6 +1,12 @@
|
||||
import { RequestWithUser } from '@ghostfolio/api/app/interfaces/request-with-user.type';
|
||||
import { Portfolio } from '@ghostfolio/api/models/portfolio';
|
||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
||||
import { IOrder } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { RulesService } from '@ghostfolio/api/services/rules.service';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { RequestWithUser } from 'apps/api/src/app/interfaces/request-with-user.type';
|
||||
import {
|
||||
add,
|
||||
format,
|
||||
@ -9,7 +15,6 @@ import {
|
||||
getYear,
|
||||
isAfter,
|
||||
isSameDay,
|
||||
parse,
|
||||
parseISO,
|
||||
setDate,
|
||||
setMonth,
|
||||
@ -18,12 +23,6 @@ import {
|
||||
import { isEmpty } from 'lodash';
|
||||
import * as roundTo from 'round-to';
|
||||
|
||||
import { Portfolio } from '../../models/portfolio';
|
||||
import { DataProviderService } from '../../services/data-provider.service';
|
||||
import { ExchangeRateDataService } from '../../services/exchange-rate-data.service';
|
||||
import { ImpersonationService } from '../../services/impersonation.service';
|
||||
import { IOrder } from '../../services/interfaces/interfaces';
|
||||
import { RulesService } from '../../services/rules.service';
|
||||
import { OrderService } from '../order/order.service';
|
||||
import { RedisCacheService } from '../redis-cache/redis-cache.service';
|
||||
import { UserService } from '../user/user.service';
|
||||
@ -74,7 +73,7 @@ export class PortfolioService {
|
||||
// Get portfolio from database
|
||||
const orders = await this.orderService.orders({
|
||||
include: {
|
||||
Platform: true
|
||||
Account: true
|
||||
},
|
||||
orderBy: { date: 'asc' },
|
||||
where: { userId: aUserId }
|
||||
@ -210,7 +209,8 @@ export class PortfolioService {
|
||||
firstBuyDate,
|
||||
investment,
|
||||
marketPrice,
|
||||
quantity
|
||||
quantity,
|
||||
transactionCount
|
||||
} = portfolio.getPositions(new Date())[aSymbol];
|
||||
|
||||
const historicalData = await this.dataProviderService.getHistorical(
|
||||
@ -263,6 +263,7 @@ export class PortfolioService {
|
||||
maxPrice,
|
||||
minPrice,
|
||||
quantity,
|
||||
transactionCount,
|
||||
grossPerformance: this.exchangeRateDataService.toCurrency(
|
||||
marketPrice - averagePrice,
|
||||
currency,
|
||||
@ -316,7 +317,8 @@ export class PortfolioService {
|
||||
maxPrice: undefined,
|
||||
minPrice: undefined,
|
||||
quantity: undefined,
|
||||
symbol: aSymbol
|
||||
symbol: aSymbol,
|
||||
transactionCount: undefined
|
||||
};
|
||||
}
|
||||
|
||||
@ -332,7 +334,8 @@ export class PortfolioService {
|
||||
maxPrice: undefined,
|
||||
minPrice: undefined,
|
||||
quantity: undefined,
|
||||
symbol: aSymbol
|
||||
symbol: aSymbol,
|
||||
transactionCount: undefined
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { CacheModule, Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import * as redisStore from 'cache-manager-redis-store';
|
||||
|
||||
import { ConfigurationService } from '../../services/configuration.service';
|
||||
import { RedisCacheService } from './redis-cache.service';
|
||||
|
||||
@Module({
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { CACHE_MANAGER, Inject, Injectable } from '@nestjs/common';
|
||||
import { Cache } from 'cache-manager';
|
||||
|
||||
import { ConfigurationService } from '../../services/configuration.service';
|
||||
|
||||
@Injectable()
|
||||
export class RedisCacheService {
|
||||
public constructor(
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Currency } from '@prisma/client';
|
||||
import { Currency, DataSource } from '@prisma/client';
|
||||
|
||||
export interface SymbolItem {
|
||||
currency: Currency;
|
||||
dataSource: DataSource;
|
||||
marketPrice: number;
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { RequestWithUser } from '@ghostfolio/api/app/interfaces/request-with-user.type';
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
@ -9,7 +10,6 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { RequestWithUser } from 'apps/api/src/app/interfaces/request-with-user.type';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
|
||||
import { LookupItem } from './interfaces/lookup-item.interface';
|
||||
|
@ -1,12 +1,12 @@
|
||||
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 { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { ConfigurationService } from '../../services/configuration.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 { PrismaService } from '../../services/prisma.service';
|
||||
import { SymbolController } from './symbol.controller';
|
||||
import { SymbolService } from './symbol.service';
|
||||
|
||||
|
@ -1,40 +1,62 @@
|
||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
||||
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
||||
import { convertFromYahooSymbol } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Currency } from '@prisma/client';
|
||||
import { convertFromYahooSymbol } from 'apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service';
|
||||
import * as bent from 'bent';
|
||||
|
||||
import { DataProviderService } from '../../services/data-provider.service';
|
||||
import { LookupItem } from './interfaces/lookup-item.interface';
|
||||
import { SymbolItem } from './interfaces/symbol-item.interface';
|
||||
|
||||
@Injectable()
|
||||
export class SymbolService {
|
||||
public constructor(
|
||||
private readonly dataProviderService: DataProviderService
|
||||
private readonly dataProviderService: DataProviderService,
|
||||
private readonly ghostfolioScraperApiService: GhostfolioScraperApiService
|
||||
) {}
|
||||
|
||||
public async get(aSymbol: string): Promise<SymbolItem> {
|
||||
const response = await this.dataProviderService.get([aSymbol]);
|
||||
const { currency, marketPrice } = response[aSymbol];
|
||||
const { currency, dataSource, marketPrice } = response[aSymbol];
|
||||
|
||||
return {
|
||||
dataSource,
|
||||
marketPrice,
|
||||
currency: <Currency>(<unknown>currency)
|
||||
};
|
||||
}
|
||||
|
||||
public async lookup(aQuery: string): Promise<LookupItem[]> {
|
||||
public async lookup(aQuery = ''): Promise<LookupItem[]> {
|
||||
const query = aQuery.toLowerCase();
|
||||
const results: LookupItem[] = [];
|
||||
|
||||
if (!query) {
|
||||
return results;
|
||||
}
|
||||
|
||||
const get = bent(
|
||||
`https://query1.finance.yahoo.com/v1/finance/search?q=${aQuery}&lang=en-US®ion=US"esCount=8&newsCount=0&enableFuzzyQuery=false"esQueryId=tss_match_phrase_query&multiQuoteQueryId=multi_quote_single_token_query&newsQueryId=news_cie_vespa&enableCb=true&enableNavLinks=false&enableEnhancedTrivialQuery=true`,
|
||||
`https://query1.finance.yahoo.com/v1/finance/search?q=${query}&lang=en-US®ion=US"esCount=8&newsCount=0&enableFuzzyQuery=false"esQueryId=tss_match_phrase_query&multiQuoteQueryId=multi_quote_single_token_query&newsQueryId=news_cie_vespa&enableCb=true&enableNavLinks=false&enableEnhancedTrivialQuery=true`,
|
||||
'GET',
|
||||
'json',
|
||||
200
|
||||
);
|
||||
|
||||
// Add custom symbols
|
||||
const scraperConfigurations = await this.ghostfolioScraperApiService.getScraperConfigurations();
|
||||
scraperConfigurations.forEach((scraperConfiguration) => {
|
||||
if (scraperConfiguration.name.toLowerCase().startsWith(query)) {
|
||||
results.push({
|
||||
name: scraperConfiguration.name,
|
||||
symbol: scraperConfiguration.symbol
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const { quotes } = await get();
|
||||
|
||||
return quotes
|
||||
const searchResult = quotes
|
||||
.filter(({ isYahooFinance }) => {
|
||||
return isYahooFinance;
|
||||
})
|
||||
@ -59,6 +81,8 @@ export class SymbolService {
|
||||
symbol: convertFromYahooSymbol(symbol)
|
||||
};
|
||||
});
|
||||
|
||||
return results.concat(searchResult);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { Currency } from '@prisma/client';
|
||||
import { Account, Currency } from '@prisma/client';
|
||||
|
||||
import { Access } from './access.interface';
|
||||
|
||||
export interface User {
|
||||
access: Access[];
|
||||
accounts: Account[];
|
||||
alias?: string;
|
||||
id: string;
|
||||
permissions: string[];
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { RequestWithUser } from '@ghostfolio/api/app/interfaces/request-with-user.type';
|
||||
import { getPermissions, hasPermission, permissions } from '@ghostfolio/helper';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpException,
|
||||
Inject,
|
||||
@ -14,7 +16,7 @@ import { REQUEST } from '@nestjs/core';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { Provider } from '@prisma/client';
|
||||
import { RequestWithUser } from 'apps/api/src/app/interfaces/request-with-user.type';
|
||||
import { User as UserModel } from '@prisma/client';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
|
||||
import { UserItem } from './interfaces/user-item.interface';
|
||||
@ -30,6 +32,27 @@ export class UserController {
|
||||
private readonly userService: UserService
|
||||
) {}
|
||||
|
||||
@Delete(':id')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async deleteUser(@Param('id') id: string): Promise<UserModel> {
|
||||
if (
|
||||
!hasPermission(
|
||||
getPermissions(this.request.user.role),
|
||||
permissions.deleteUser
|
||||
) ||
|
||||
id === this.request.user.id
|
||||
) {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||
StatusCodes.FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
return this.userService.deleteUser({
|
||||
id
|
||||
});
|
||||
}
|
||||
|
||||
@Get()
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async getUser(@Param('id') id: string): Promise<User> {
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
|
||||
import { ConfigurationService } from '../../services/configuration.service';
|
||||
import { PrismaService } from '../../services/prisma.service';
|
||||
import { UserController } from './user.controller';
|
||||
import { UserService } from './user.service';
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import {
|
||||
getPermissions,
|
||||
locale,
|
||||
@ -8,8 +10,6 @@ import { Injectable } from '@nestjs/common';
|
||||
import { Currency, Prisma, Provider, User } from '@prisma/client';
|
||||
import { add } from 'date-fns';
|
||||
|
||||
import { ConfigurationService } from '../../services/configuration.service';
|
||||
import { PrismaService } from '../../services/prisma.service';
|
||||
import { UserWithSettings } from '../interfaces/user-with-settings';
|
||||
import { User as IUser } from './interfaces/user.interface';
|
||||
|
||||
@ -25,6 +25,7 @@ export class UserService {
|
||||
) {}
|
||||
|
||||
public async getUser({
|
||||
Account,
|
||||
alias,
|
||||
id,
|
||||
role,
|
||||
@ -53,6 +54,7 @@ export class UserService {
|
||||
id: accessItem.id
|
||||
};
|
||||
}),
|
||||
accounts: Account,
|
||||
permissions: currentPermissions,
|
||||
settings: {
|
||||
baseCurrency: Settings?.currency || UserService.DEFAULT_CURRENCY,
|
||||
@ -69,7 +71,7 @@ export class UserService {
|
||||
userWhereUniqueInput: Prisma.UserWhereUniqueInput
|
||||
): Promise<UserWithSettings | null> {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
include: { Settings: true },
|
||||
include: { Account: true, Settings: true },
|
||||
where: userWhereUniqueInput
|
||||
});
|
||||
|
||||
@ -116,7 +118,15 @@ export class UserService {
|
||||
|
||||
public async createUser(data?: Prisma.UserCreateInput): Promise<User> {
|
||||
let user = await this.prisma.user.create({
|
||||
data
|
||||
data: {
|
||||
...data,
|
||||
Account: {
|
||||
create: {
|
||||
isDefault: true,
|
||||
name: 'Default Account'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (data.provider === Provider.ANONYMOUS) {
|
||||
@ -153,6 +163,28 @@ export class UserService {
|
||||
}
|
||||
|
||||
public async deleteUser(where: Prisma.UserWhereUniqueInput): Promise<User> {
|
||||
await this.prisma.access.deleteMany({
|
||||
where: { OR: [{ granteeUserId: where.id }, { userId: where.id }] }
|
||||
});
|
||||
|
||||
await this.prisma.account.deleteMany({
|
||||
where: { userId: where.id }
|
||||
});
|
||||
|
||||
await this.prisma.analytics.delete({
|
||||
where: { userId: where.id }
|
||||
});
|
||||
|
||||
await this.prisma.order.deleteMany({
|
||||
where: { userId: where.id }
|
||||
});
|
||||
|
||||
try {
|
||||
await this.prisma.settings.delete({
|
||||
where: { userId: where.id }
|
||||
});
|
||||
} catch {}
|
||||
|
||||
return this.prisma.user.delete({
|
||||
where
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {
|
||||
PortfolioItem,
|
||||
Position
|
||||
} from 'apps/api/src/app/portfolio/interfaces/portfolio-item.interface';
|
||||
} from '@ghostfolio/api/app/portfolio/interfaces/portfolio-item.interface';
|
||||
|
||||
import { Order } from '../order';
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { PortfolioPosition } from '../../app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
|
||||
|
||||
import { EvaluationResult } from './evaluation-result.interface';
|
||||
|
||||
export interface RuleInterface {
|
||||
|
@ -1,27 +1,27 @@
|
||||
import { Currency, Platform } from '@prisma/client';
|
||||
import { Account, Currency, Platform } from '@prisma/client';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { IOrder } from '../services/interfaces/interfaces';
|
||||
import { OrderType } from './order-type';
|
||||
|
||||
export class Order {
|
||||
private account: Account;
|
||||
private currency: Currency;
|
||||
private fee: number;
|
||||
private date: string;
|
||||
private id: string;
|
||||
private quantity: number;
|
||||
private platform: Platform;
|
||||
private symbol: string;
|
||||
private total: number;
|
||||
private type: OrderType;
|
||||
private unitPrice: number;
|
||||
|
||||
public constructor(data: IOrder) {
|
||||
this.account = data.account;
|
||||
this.currency = data.currency;
|
||||
this.fee = data.fee;
|
||||
this.date = data.date;
|
||||
this.id = data.id || uuidv4();
|
||||
this.platform = data.platform;
|
||||
this.quantity = data.quantity;
|
||||
this.symbol = data.symbol;
|
||||
this.type = data.type;
|
||||
@ -30,6 +30,10 @@ export class Order {
|
||||
this.total = this.quantity * data.unitPrice;
|
||||
}
|
||||
|
||||
public getAccount() {
|
||||
return this.account;
|
||||
}
|
||||
|
||||
public getCurrency() {
|
||||
return this.currency;
|
||||
}
|
||||
@ -46,10 +50,6 @@ export class Order {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public getPlatform() {
|
||||
return this.platform;
|
||||
}
|
||||
|
||||
public getQuantity() {
|
||||
return this.quantity;
|
||||
}
|
||||
|
@ -1,22 +1,28 @@
|
||||
import { baseCurrency, getUtc, getYesterday } from '@ghostfolio/helper';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { Currency, Role, Type } from '@prisma/client';
|
||||
import { AccountType, Currency, DataSource, Role, Type } from '@prisma/client';
|
||||
|
||||
import { ConfigurationService } from '../services/configuration.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 { MarketState } from '../services/interfaces/interfaces';
|
||||
import { PrismaService } from '../services/prisma.service';
|
||||
import { RulesService } from '../services/rules.service';
|
||||
import { Portfolio } from './portfolio';
|
||||
|
||||
const DEFAULT_ACCOUNT_ID = '693a834b-eb89-42c9-ae47-35196c25d269';
|
||||
const USER_ID = 'ca6ce867-5d31-495a-bce9-5942bbca9237';
|
||||
|
||||
describe('Portfolio', () => {
|
||||
let alphaVantageService: AlphaVantageService;
|
||||
let configurationService: ConfigurationService;
|
||||
let dataProviderService: DataProviderService;
|
||||
let exchangeRateDataService: ExchangeRateDataService;
|
||||
let ghostfolioScraperApiService: GhostfolioScraperApiService;
|
||||
let portfolio: Portfolio;
|
||||
let prismaService: PrismaService;
|
||||
let rakutenRapidApiService: RakutenRapidApiService;
|
||||
@ -31,6 +37,7 @@ describe('Portfolio', () => {
|
||||
ConfigurationService,
|
||||
DataProviderService,
|
||||
ExchangeRateDataService,
|
||||
GhostfolioScraperApiService,
|
||||
PrismaService,
|
||||
RakutenRapidApiService,
|
||||
RulesService,
|
||||
@ -44,6 +51,9 @@ describe('Portfolio', () => {
|
||||
exchangeRateDataService = app.get<ExchangeRateDataService>(
|
||||
ExchangeRateDataService
|
||||
);
|
||||
ghostfolioScraperApiService = app.get<GhostfolioScraperApiService>(
|
||||
GhostfolioScraperApiService
|
||||
);
|
||||
prismaService = app.get<PrismaService>(PrismaService);
|
||||
rakutenRapidApiService = app.get<RakutenRapidApiService>(
|
||||
RakutenRapidApiService
|
||||
@ -60,15 +70,27 @@ describe('Portfolio', () => {
|
||||
);
|
||||
portfolio.setUser({
|
||||
accessToken: null,
|
||||
Account: [
|
||||
{
|
||||
accountType: AccountType.SECURITIES,
|
||||
createdAt: new Date(),
|
||||
id: DEFAULT_ACCOUNT_ID,
|
||||
isDefault: true,
|
||||
name: 'Default Account',
|
||||
platformId: null,
|
||||
updatedAt: new Date(),
|
||||
userId: USER_ID
|
||||
}
|
||||
],
|
||||
alias: 'Test',
|
||||
createdAt: new Date(),
|
||||
id: '',
|
||||
id: USER_ID,
|
||||
provider: null,
|
||||
role: Role.USER,
|
||||
Settings: {
|
||||
currency: Currency.CHF,
|
||||
updatedAt: new Date(),
|
||||
userId: ''
|
||||
userId: USER_ID
|
||||
},
|
||||
thirdPartyId: null,
|
||||
updatedAt: new Date()
|
||||
@ -119,18 +141,20 @@ describe('Portfolio', () => {
|
||||
it('should return ["BTC"]', async () => {
|
||||
await portfolio.setOrders([
|
||||
{
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
accountUserId: USER_ID,
|
||||
createdAt: null,
|
||||
currency: Currency.USD,
|
||||
dataSource: DataSource.YAHOO,
|
||||
fee: 0,
|
||||
date: new Date(),
|
||||
id: '8d999347-dee2-46ee-88e1-26b344e71fcc',
|
||||
platformId: null,
|
||||
quantity: 1,
|
||||
symbol: 'BTCUSD',
|
||||
type: Type.BUY,
|
||||
unitPrice: 49631.24,
|
||||
updatedAt: null,
|
||||
userId: null
|
||||
userId: USER_ID
|
||||
}
|
||||
]);
|
||||
|
||||
@ -145,19 +169,7 @@ describe('Portfolio', () => {
|
||||
const details = await portfolio.getDetails('1d');
|
||||
expect(details).toMatchObject({
|
||||
BTCUSD: {
|
||||
currency: Currency.USD,
|
||||
exchange: 'Other',
|
||||
grossPerformance: 0,
|
||||
grossPerformancePercent: 0,
|
||||
investment: exchangeRateDataService.toCurrency(
|
||||
1 * 49631.24,
|
||||
Currency.USD,
|
||||
baseCurrency
|
||||
),
|
||||
isMarketOpen: true,
|
||||
// marketPrice: 57973.008,
|
||||
name: 'Bitcoin USD',
|
||||
platforms: {
|
||||
accounts: {
|
||||
Other: {
|
||||
/*current: exchangeRateDataService.toCurrency(
|
||||
1 * 49631.24,
|
||||
@ -171,10 +183,23 @@ describe('Portfolio', () => {
|
||||
)
|
||||
}
|
||||
},
|
||||
currency: Currency.USD,
|
||||
exchange: 'Other',
|
||||
grossPerformance: 0,
|
||||
grossPerformancePercent: 0,
|
||||
investment: exchangeRateDataService.toCurrency(
|
||||
1 * 49631.24,
|
||||
Currency.USD,
|
||||
baseCurrency
|
||||
),
|
||||
// marketPrice: 57973.008,
|
||||
marketState: MarketState.open,
|
||||
name: 'Bitcoin USD',
|
||||
quantity: 1,
|
||||
// shareCurrent: 0.9999999559148652,
|
||||
shareInvestment: 1,
|
||||
symbol: 'BTCUSD',
|
||||
transactionCount: 1,
|
||||
type: 'Cryptocurrency'
|
||||
}
|
||||
});
|
||||
@ -217,18 +242,20 @@ describe('Portfolio', () => {
|
||||
it('should return ["ETHUSD"]', async () => {
|
||||
await portfolio.setOrders([
|
||||
{
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
accountUserId: USER_ID,
|
||||
createdAt: null,
|
||||
currency: Currency.USD,
|
||||
dataSource: DataSource.YAHOO,
|
||||
fee: 0,
|
||||
date: new Date(getUtc('2018-01-05')),
|
||||
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fb',
|
||||
platformId: null,
|
||||
quantity: 0.2,
|
||||
symbol: 'ETHUSD',
|
||||
type: Type.BUY,
|
||||
unitPrice: 991.49,
|
||||
updatedAt: null,
|
||||
userId: null
|
||||
userId: USER_ID
|
||||
}
|
||||
]);
|
||||
|
||||
@ -243,18 +270,7 @@ describe('Portfolio', () => {
|
||||
const details = await portfolio.getDetails('1d');
|
||||
expect(details).toMatchObject({
|
||||
ETHUSD: {
|
||||
currency: Currency.USD,
|
||||
exchange: 'Other',
|
||||
// grossPerformance: 0,
|
||||
// grossPerformancePercent: 0,
|
||||
investment: exchangeRateDataService.toCurrency(
|
||||
0.2 * 991.49,
|
||||
Currency.USD,
|
||||
baseCurrency
|
||||
),
|
||||
// marketPrice: 57973.008,
|
||||
name: 'Ethereum USD',
|
||||
platforms: {
|
||||
accounts: {
|
||||
Other: {
|
||||
/*current: exchangeRateDataService.toCurrency(
|
||||
0.2 * 991.49,
|
||||
@ -268,9 +284,21 @@ describe('Portfolio', () => {
|
||||
)
|
||||
}
|
||||
},
|
||||
currency: Currency.USD,
|
||||
exchange: 'Other',
|
||||
// grossPerformance: 0,
|
||||
// grossPerformancePercent: 0,
|
||||
investment: exchangeRateDataService.toCurrency(
|
||||
0.2 * 991.49,
|
||||
Currency.USD,
|
||||
baseCurrency
|
||||
),
|
||||
// marketPrice: 57973.008,
|
||||
name: 'Ethereum USD',
|
||||
quantity: 0.2,
|
||||
shareCurrent: 1,
|
||||
// shareCurrent: 1,
|
||||
shareInvestment: 1,
|
||||
transactionCount: 1,
|
||||
symbol: 'ETHUSD',
|
||||
type: 'Cryptocurrency'
|
||||
}
|
||||
@ -309,32 +337,36 @@ describe('Portfolio', () => {
|
||||
it('should return ["ETHUSD"]', async () => {
|
||||
await portfolio.setOrders([
|
||||
{
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
accountUserId: USER_ID,
|
||||
createdAt: null,
|
||||
currency: Currency.USD,
|
||||
dataSource: DataSource.YAHOO,
|
||||
fee: 0,
|
||||
date: new Date(getUtc('2018-01-05')),
|
||||
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fb',
|
||||
platformId: null,
|
||||
quantity: 0.2,
|
||||
symbol: 'ETHUSD',
|
||||
type: Type.BUY,
|
||||
unitPrice: 991.49,
|
||||
updatedAt: null,
|
||||
userId: null
|
||||
userId: USER_ID
|
||||
},
|
||||
{
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
accountUserId: USER_ID,
|
||||
createdAt: null,
|
||||
currency: Currency.USD,
|
||||
dataSource: DataSource.YAHOO,
|
||||
fee: 0,
|
||||
date: new Date(getUtc('2018-01-28')),
|
||||
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fc',
|
||||
platformId: null,
|
||||
quantity: 0.3,
|
||||
symbol: 'ETHUSD',
|
||||
type: Type.BUY,
|
||||
unitPrice: 1050,
|
||||
updatedAt: null,
|
||||
userId: null
|
||||
userId: USER_ID
|
||||
}
|
||||
]);
|
||||
|
||||
@ -381,32 +413,36 @@ describe('Portfolio', () => {
|
||||
it('should return ["BTCUSD", "ETHUSD"]', async () => {
|
||||
await portfolio.setOrders([
|
||||
{
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
accountUserId: USER_ID,
|
||||
createdAt: null,
|
||||
currency: Currency.EUR,
|
||||
dataSource: DataSource.YAHOO,
|
||||
date: new Date(getUtc('2017-08-16')),
|
||||
fee: 2.99,
|
||||
id: 'd96795b2-6ae6-420e-aa21-fabe5e45d475',
|
||||
platformId: null,
|
||||
quantity: 0.05614682,
|
||||
symbol: 'BTCUSD',
|
||||
type: Type.BUY,
|
||||
unitPrice: 3562.089535970158,
|
||||
updatedAt: null,
|
||||
userId: null
|
||||
userId: USER_ID
|
||||
},
|
||||
{
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
accountUserId: USER_ID,
|
||||
createdAt: null,
|
||||
currency: Currency.USD,
|
||||
dataSource: DataSource.YAHOO,
|
||||
fee: 2.99,
|
||||
date: new Date(getUtc('2018-01-05')),
|
||||
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fb',
|
||||
platformId: null,
|
||||
quantity: 0.2,
|
||||
symbol: 'ETHUSD',
|
||||
type: Type.BUY,
|
||||
unitPrice: 991.49,
|
||||
updatedAt: null,
|
||||
userId: null
|
||||
userId: USER_ID
|
||||
}
|
||||
]);
|
||||
|
||||
@ -466,46 +502,52 @@ describe('Portfolio', () => {
|
||||
it('should work with buy and sell', async () => {
|
||||
await portfolio.setOrders([
|
||||
{
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
accountUserId: USER_ID,
|
||||
createdAt: null,
|
||||
currency: Currency.USD,
|
||||
dataSource: DataSource.YAHOO,
|
||||
fee: 1.0,
|
||||
date: new Date(getUtc('2018-01-05')),
|
||||
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fb',
|
||||
platformId: null,
|
||||
quantity: 0.2,
|
||||
symbol: 'ETHUSD',
|
||||
type: Type.BUY,
|
||||
unitPrice: 991.49,
|
||||
updatedAt: null,
|
||||
userId: null
|
||||
userId: USER_ID
|
||||
},
|
||||
{
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
accountUserId: USER_ID,
|
||||
createdAt: null,
|
||||
currency: Currency.USD,
|
||||
dataSource: DataSource.YAHOO,
|
||||
fee: 1.0,
|
||||
date: new Date(getUtc('2018-01-28')),
|
||||
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fc',
|
||||
platformId: null,
|
||||
quantity: 0.1,
|
||||
symbol: 'ETHUSD',
|
||||
type: Type.SELL,
|
||||
unitPrice: 1050,
|
||||
updatedAt: null,
|
||||
userId: null
|
||||
userId: USER_ID
|
||||
},
|
||||
{
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
accountUserId: USER_ID,
|
||||
createdAt: null,
|
||||
currency: Currency.USD,
|
||||
dataSource: DataSource.YAHOO,
|
||||
fee: 1.0,
|
||||
date: new Date(getUtc('2018-01-31')),
|
||||
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fc',
|
||||
platformId: null,
|
||||
quantity: 0.2,
|
||||
symbol: 'ETHUSD',
|
||||
type: Type.BUY,
|
||||
unitPrice: 1050,
|
||||
updatedAt: null,
|
||||
userId: null
|
||||
userId: USER_ID
|
||||
}
|
||||
]);
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { getToday, getYesterday, resetHours } from '@ghostfolio/helper';
|
||||
import {
|
||||
PortfolioItem,
|
||||
Position
|
||||
} from 'apps/api/src/app/portfolio/interfaces/portfolio-item.interface';
|
||||
} from '@ghostfolio/api/app/portfolio/interfaces/portfolio-item.interface';
|
||||
import { getToday, getYesterday, resetHours } from '@ghostfolio/helper';
|
||||
import {
|
||||
add,
|
||||
format,
|
||||
@ -23,7 +23,7 @@ import { cloneDeep, isEmpty } from 'lodash';
|
||||
import * as roundTo from 'round-to';
|
||||
|
||||
import { UserWithSettings } from '../app/interfaces/user-with-settings';
|
||||
import { OrderWithPlatform } from '../app/order/interfaces/order-with-platform.type';
|
||||
import { OrderWithAccount } from '../app/order/interfaces/order-with-account.type';
|
||||
import { DateRange } from '../app/portfolio/interfaces/date-range.type';
|
||||
import { PortfolioPerformance } from '../app/portfolio/interfaces/portfolio-performance.interface';
|
||||
import { PortfolioPosition } from '../app/portfolio/interfaces/portfolio-position.interface';
|
||||
@ -34,14 +34,14 @@ import { IOrder } from '../services/interfaces/interfaces';
|
||||
import { RulesService } from '../services/rules.service';
|
||||
import { PortfolioInterface } from './interfaces/portfolio.interface';
|
||||
import { Order } from './order';
|
||||
import { AccountClusterRiskCurrentInvestment } from './rules/account-cluster-risk/current-investment';
|
||||
import { AccountClusterRiskInitialInvestment } from './rules/account-cluster-risk/initial-investment';
|
||||
import { AccountClusterRiskSingleAccount } from './rules/account-cluster-risk/single-account';
|
||||
import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from './rules/currency-cluster-risk/base-currency-current-investment';
|
||||
import { CurrencyClusterRiskBaseCurrencyInitialInvestment } from './rules/currency-cluster-risk/base-currency-initial-investment';
|
||||
import { CurrencyClusterRiskCurrentInvestment } from './rules/currency-cluster-risk/current-investment';
|
||||
import { CurrencyClusterRiskInitialInvestment } from './rules/currency-cluster-risk/initial-investment';
|
||||
import { FeeRatioInitialInvestment } from './rules/fees/fee-ratio-initial-investment';
|
||||
import { PlatformClusterRiskCurrentInvestment } from './rules/platform-cluster-risk/current-investment';
|
||||
import { PlatformClusterRiskInitialInvestment } from './rules/platform-cluster-risk/initial-investment';
|
||||
import { PlatformClusterRiskSinglePlatform } from './rules/platform-cluster-risk/single-platform';
|
||||
|
||||
export class Portfolio implements PortfolioInterface {
|
||||
private orders: Order[] = [];
|
||||
@ -57,7 +57,7 @@ export class Portfolio implements PortfolioInterface {
|
||||
public async addCurrentPortfolioItems() {
|
||||
const currentData = await this.dataProviderService.get(this.getSymbols());
|
||||
|
||||
let currentDate = new Date();
|
||||
const currentDate = new Date();
|
||||
|
||||
const year = getYear(currentDate);
|
||||
const month = getMonth(currentDate);
|
||||
@ -82,7 +82,9 @@ export class Portfolio implements PortfolioInterface {
|
||||
marketPrice:
|
||||
currentData[symbol]?.marketPrice ??
|
||||
portfolioItemsYesterday.positions[symbol]?.marketPrice,
|
||||
quantity: portfolioItemsYesterday?.positions[symbol]?.quantity
|
||||
quantity: portfolioItemsYesterday?.positions[symbol]?.quantity,
|
||||
transactionCount:
|
||||
portfolioItemsYesterday?.positions[symbol]?.transactionCount
|
||||
};
|
||||
});
|
||||
|
||||
@ -117,11 +119,11 @@ export class Portfolio implements PortfolioInterface {
|
||||
}): Portfolio {
|
||||
orders.forEach(
|
||||
({
|
||||
account,
|
||||
currency,
|
||||
fee,
|
||||
date,
|
||||
id,
|
||||
platform,
|
||||
quantity,
|
||||
symbol,
|
||||
type,
|
||||
@ -129,11 +131,11 @@ export class Portfolio implements PortfolioInterface {
|
||||
}) => {
|
||||
this.orders.push(
|
||||
new Order({
|
||||
account,
|
||||
currency,
|
||||
fee,
|
||||
date,
|
||||
id,
|
||||
platform,
|
||||
quantity,
|
||||
symbol,
|
||||
type,
|
||||
@ -200,7 +202,7 @@ export class Portfolio implements PortfolioInterface {
|
||||
const data = await this.dataProviderService.get(symbols);
|
||||
|
||||
symbols.forEach((symbol) => {
|
||||
const platforms: PortfolioPosition['platforms'] = {};
|
||||
const accounts: PortfolioPosition['accounts'] = {};
|
||||
const [portfolioItem] = portfolioItems;
|
||||
|
||||
const ordersBySymbol = this.getOrders().filter((order) => {
|
||||
@ -225,15 +227,15 @@ export class Portfolio implements PortfolioInterface {
|
||||
originalValueOfSymbol *= -1;
|
||||
}
|
||||
|
||||
if (platforms[orderOfSymbol.getPlatform()?.name || 'Other']?.current) {
|
||||
platforms[
|
||||
orderOfSymbol.getPlatform()?.name || 'Other'
|
||||
if (accounts[orderOfSymbol.getAccount()?.name || 'Other']?.current) {
|
||||
accounts[
|
||||
orderOfSymbol.getAccount()?.name || 'Other'
|
||||
].current += currentValueOfSymbol;
|
||||
platforms[
|
||||
orderOfSymbol.getPlatform()?.name || 'Other'
|
||||
accounts[
|
||||
orderOfSymbol.getAccount()?.name || 'Other'
|
||||
].original += originalValueOfSymbol;
|
||||
} else {
|
||||
platforms[orderOfSymbol.getPlatform()?.name || 'Other'] = {
|
||||
accounts[orderOfSymbol.getAccount()?.name || 'Other'] = {
|
||||
current: currentValueOfSymbol,
|
||||
original: originalValueOfSymbol
|
||||
};
|
||||
@ -274,7 +276,7 @@ export class Portfolio implements PortfolioInterface {
|
||||
|
||||
details[symbol] = {
|
||||
...data[symbol],
|
||||
platforms,
|
||||
accounts,
|
||||
symbol,
|
||||
grossPerformance: roundTo(
|
||||
portfolioItemsNow.positions[symbol].quantity * (now - before),
|
||||
@ -289,7 +291,9 @@ export class Portfolio implements PortfolioInterface {
|
||||
data[symbol]?.currency,
|
||||
this.user.Settings.currency
|
||||
) / value,
|
||||
shareInvestment: portfolioItem.positions[symbol].investment / investment
|
||||
shareInvestment:
|
||||
portfolioItem.positions[symbol].investment / investment,
|
||||
transactionCount: portfolioItem.positions[symbol].transactionCount
|
||||
};
|
||||
});
|
||||
|
||||
@ -392,6 +396,19 @@ export class Portfolio implements PortfolioInterface {
|
||||
|
||||
return {
|
||||
rules: {
|
||||
accountClusterRisk: await this.rulesService.evaluate(
|
||||
this,
|
||||
[
|
||||
new AccountClusterRiskCurrentInvestment(
|
||||
this.exchangeRateDataService
|
||||
),
|
||||
new AccountClusterRiskInitialInvestment(
|
||||
this.exchangeRateDataService
|
||||
),
|
||||
new AccountClusterRiskSingleAccount(this.exchangeRateDataService)
|
||||
],
|
||||
{ baseCurrency: this.user.Settings.currency }
|
||||
),
|
||||
currencyClusterRisk: await this.rulesService.evaluate(
|
||||
this,
|
||||
[
|
||||
@ -410,19 +427,6 @@ export class Portfolio implements PortfolioInterface {
|
||||
],
|
||||
{ baseCurrency: this.user.Settings.currency }
|
||||
),
|
||||
platformClusterRisk: await this.rulesService.evaluate(
|
||||
this,
|
||||
[
|
||||
new PlatformClusterRiskSinglePlatform(this.exchangeRateDataService),
|
||||
new PlatformClusterRiskInitialInvestment(
|
||||
this.exchangeRateDataService
|
||||
),
|
||||
new PlatformClusterRiskCurrentInvestment(
|
||||
this.exchangeRateDataService
|
||||
)
|
||||
],
|
||||
{ baseCurrency: this.user.Settings.currency }
|
||||
),
|
||||
fees: await this.rulesService.evaluate(
|
||||
this,
|
||||
[new FeeRatioInitialInvestment(this.exchangeRateDataService)],
|
||||
@ -518,17 +522,17 @@ export class Portfolio implements PortfolioInterface {
|
||||
return isFinite(value) ? value : null;
|
||||
}
|
||||
|
||||
public async setOrders(aOrders: OrderWithPlatform[]) {
|
||||
public async setOrders(aOrders: OrderWithAccount[]) {
|
||||
this.orders = [];
|
||||
|
||||
// Map data
|
||||
aOrders.forEach((order) => {
|
||||
this.orders.push(
|
||||
new Order({
|
||||
account: order.Account,
|
||||
currency: <any>order.currency,
|
||||
date: order.date.toISOString(),
|
||||
fee: order.fee,
|
||||
platform: order.Platform,
|
||||
quantity: order.quantity,
|
||||
symbol: order.symbol,
|
||||
type: <any>order.type,
|
||||
@ -582,7 +586,8 @@ export class Portfolio implements PortfolioInterface {
|
||||
marketPrice:
|
||||
historicalData[symbol]?.[format(currentDate, 'yyyy-MM-dd')]
|
||||
?.marketPrice || 0,
|
||||
quantity: 0
|
||||
quantity: 0,
|
||||
transactionCount: 0
|
||||
};
|
||||
});
|
||||
|
||||
@ -623,7 +628,8 @@ export class Portfolio implements PortfolioInterface {
|
||||
marketPrice:
|
||||
historicalData[symbol]?.[format(yesterday, 'yyyy-MM-dd')]
|
||||
?.marketPrice || 0,
|
||||
quantity: 0
|
||||
quantity: 0,
|
||||
transactionCount: 0
|
||||
};
|
||||
});
|
||||
|
||||
@ -730,6 +736,10 @@ export class Portfolio implements PortfolioInterface {
|
||||
order.getSymbol()
|
||||
].currency = order.getCurrency();
|
||||
|
||||
this.portfolioItems[i].positions[
|
||||
order.getSymbol()
|
||||
].transactionCount += 1;
|
||||
|
||||
if (order.getType() === 'BUY') {
|
||||
if (
|
||||
!this.portfolioItems[i].positions[order.getSymbol()].firstBuyDate
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { PortfolioPosition } from 'apps/api/src/app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
||||
import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||
|
||||
import { Rule } from '../../rule';
|
||||
|
||||
export class PlatformClusterRiskCurrentInvestment extends Rule {
|
||||
export class AccountClusterRiskCurrentInvestment extends Rule {
|
||||
public constructor(public exchangeRateDataService: ExchangeRateDataService) {
|
||||
super(exchangeRateDataService, {
|
||||
name: 'Current Investment'
|
||||
@ -18,24 +18,22 @@ export class PlatformClusterRiskCurrentInvestment extends Rule {
|
||||
}
|
||||
) {
|
||||
const ruleSettings =
|
||||
aRuleSettingsMap[PlatformClusterRiskCurrentInvestment.name];
|
||||
aRuleSettingsMap[AccountClusterRiskCurrentInvestment.name];
|
||||
|
||||
const platforms: {
|
||||
const accounts: {
|
||||
[symbol: string]: Pick<PortfolioPosition, 'name'> & {
|
||||
investment: number;
|
||||
};
|
||||
} = {};
|
||||
|
||||
Object.values(aPositions).forEach((position) => {
|
||||
for (const [platform, { current }] of Object.entries(
|
||||
position.platforms
|
||||
)) {
|
||||
if (platforms[platform]?.investment) {
|
||||
platforms[platform].investment += current;
|
||||
for (const [account, { current }] of Object.entries(position.accounts)) {
|
||||
if (accounts[account]?.investment) {
|
||||
accounts[account].investment += current;
|
||||
} else {
|
||||
platforms[platform] = {
|
||||
accounts[account] = {
|
||||
investment: current,
|
||||
name: platform
|
||||
name: account
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -44,17 +42,17 @@ export class PlatformClusterRiskCurrentInvestment extends Rule {
|
||||
let maxItem;
|
||||
let totalInvestment = 0;
|
||||
|
||||
Object.values(platforms).forEach((platform) => {
|
||||
Object.values(accounts).forEach((account) => {
|
||||
if (!maxItem) {
|
||||
maxItem = platform;
|
||||
maxItem = account;
|
||||
}
|
||||
|
||||
// Calculate total investment
|
||||
totalInvestment += platform.investment;
|
||||
totalInvestment += account.investment;
|
||||
|
||||
// Find maximum
|
||||
if (platform.investment > maxItem?.investment) {
|
||||
maxItem = platform;
|
||||
if (account.investment > maxItem?.investment) {
|
||||
maxItem = account;
|
||||
}
|
||||
});
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { PortfolioPosition } from 'apps/api/src/app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
||||
|
||||
import { Rule } from '../../rule';
|
||||
|
||||
export class PlatformClusterRiskInitialInvestment extends Rule {
|
||||
export class AccountClusterRiskInitialInvestment extends Rule {
|
||||
public constructor(public exchangeRateDataService: ExchangeRateDataService) {
|
||||
super(exchangeRateDataService, {
|
||||
name: 'Initial Investment'
|
||||
@ -18,7 +18,7 @@ export class PlatformClusterRiskInitialInvestment extends Rule {
|
||||
}
|
||||
) {
|
||||
const ruleSettings =
|
||||
aRuleSettingsMap[PlatformClusterRiskInitialInvestment.name];
|
||||
aRuleSettingsMap[AccountClusterRiskInitialInvestment.name];
|
||||
|
||||
const platforms: {
|
||||
[symbol: string]: Pick<PortfolioPosition, 'name'> & {
|
||||
@ -27,15 +27,13 @@ export class PlatformClusterRiskInitialInvestment extends Rule {
|
||||
} = {};
|
||||
|
||||
Object.values(aPositions).forEach((position) => {
|
||||
for (const [platform, { original }] of Object.entries(
|
||||
position.platforms
|
||||
)) {
|
||||
if (platforms[platform]?.investment) {
|
||||
platforms[platform].investment += original;
|
||||
for (const [account, { original }] of Object.entries(position.accounts)) {
|
||||
if (platforms[account]?.investment) {
|
||||
platforms[account].investment += original;
|
||||
} else {
|
||||
platforms[platform] = {
|
||||
platforms[account] = {
|
||||
investment: original,
|
||||
name: platform
|
||||
name: account
|
||||
};
|
||||
}
|
||||
}
|
@ -1,35 +1,35 @@
|
||||
import { PortfolioPosition } from 'apps/api/src/app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
||||
|
||||
import { Rule } from '../../rule';
|
||||
|
||||
export class PlatformClusterRiskSinglePlatform extends Rule {
|
||||
export class AccountClusterRiskSingleAccount extends Rule {
|
||||
public constructor(public exchangeRateDataService: ExchangeRateDataService) {
|
||||
super(exchangeRateDataService, {
|
||||
name: 'Single Platform'
|
||||
name: 'Single Account'
|
||||
});
|
||||
}
|
||||
|
||||
public evaluate(positions: { [symbol: string]: PortfolioPosition }) {
|
||||
const platforms: string[] = [];
|
||||
const accounts: string[] = [];
|
||||
|
||||
Object.values(positions).forEach((position) => {
|
||||
for (const [platform] of Object.entries(position.platforms)) {
|
||||
if (!platforms.includes(platform)) {
|
||||
platforms.push(platform);
|
||||
for (const [account] of Object.entries(position.accounts)) {
|
||||
if (!accounts.includes(account)) {
|
||||
accounts.push(account);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (platforms.length === 1) {
|
||||
if (accounts.length === 1) {
|
||||
return {
|
||||
evaluation: `All your investment is managed by a single platform`,
|
||||
evaluation: `All your investment is managed by a single account`,
|
||||
value: false
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
evaluation: `Your investment is managed by ${platforms.length} platforms`,
|
||||
evaluation: `Your investment is managed by ${accounts.length} accounts`,
|
||||
value: true
|
||||
};
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { PortfolioPosition } from 'apps/api/src/app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
||||
import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||
|
||||
import { Rule } from '../../rule';
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { PortfolioPosition } from 'apps/api/src/app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
||||
|
||||
import { Rule } from '../../rule';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { PortfolioPosition } from 'apps/api/src/app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
||||
|
||||
import { Rule } from '../../rule';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { PortfolioPosition } from 'apps/api/src/app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
||||
|
||||
import { Rule } from '../../rule';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { PortfolioPosition } from 'apps/api/src/app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
||||
|
||||
import { Rule } from '../../rule';
|
||||
|
@ -15,6 +15,7 @@ export class ConfigurationService {
|
||||
ENABLE_FEATURE_CUSTOM_SYMBOLS: bool({ default: false }),
|
||||
ENABLE_FEATURE_FEAR_AND_GREED_INDEX: bool({ default: false }),
|
||||
ENABLE_FEATURE_SOCIAL_LOGIN: bool({ default: false }),
|
||||
ENABLE_FEATURE_SUBSCRIPTION: bool({ default: false }),
|
||||
GOOGLE_CLIENT_ID: str({ default: 'dummyClientId' }),
|
||||
GOOGLE_SECRET: str({ default: 'dummySecret' }),
|
||||
JWT_SECRET_KEY: str({}),
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
|
||||
import { ConfigurationService } from './configuration.service';
|
||||
import { DataProviderService } from './data-provider.service';
|
||||
import { GhostfolioScraperApiService } from './data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
||||
import { PrismaService } from './prisma.service';
|
||||
|
||||
@Injectable()
|
||||
@ -25,6 +26,7 @@ export class DataGatheringService {
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService,
|
||||
private readonly dataProviderService: DataProviderService,
|
||||
private readonly ghostfolioScraperApi: GhostfolioScraperApiService,
|
||||
private prisma: PrismaService
|
||||
) {}
|
||||
|
||||
@ -183,6 +185,17 @@ export class DataGatheringService {
|
||||
}
|
||||
}
|
||||
|
||||
public async getCustomSymbolsToGather(startDate?: Date) {
|
||||
const scraperConfigurations = await this.ghostfolioScraperApi.getScraperConfigurations();
|
||||
|
||||
return scraperConfigurations.map((scraperConfiguration) => {
|
||||
return {
|
||||
date: startDate,
|
||||
symbol: scraperConfiguration.symbol
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private getBenchmarksToGather(startDate: Date) {
|
||||
const benchmarksToGather = benchmarks.map((symbol) => {
|
||||
return {
|
||||
@ -201,32 +214,6 @@ export class DataGatheringService {
|
||||
return benchmarksToGather;
|
||||
}
|
||||
|
||||
private async getCustomSymbolsToGather(startDate: Date) {
|
||||
const customSymbolsToGather = [];
|
||||
|
||||
if (this.configurationService.get('ENABLE_FEATURE_CUSTOM_SYMBOLS')) {
|
||||
try {
|
||||
const {
|
||||
value: scraperConfigString
|
||||
} = await this.prisma.property.findFirst({
|
||||
select: {
|
||||
value: true
|
||||
},
|
||||
where: { key: 'SCRAPER_CONFIG' }
|
||||
});
|
||||
|
||||
JSON.parse(scraperConfigString).forEach((item) => {
|
||||
customSymbolsToGather.push({
|
||||
date: startDate,
|
||||
symbol: item.symbol
|
||||
});
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return customSymbolsToGather;
|
||||
}
|
||||
|
||||
private async getSymbols7D(): Promise<{ date: Date; symbol: string }[]> {
|
||||
const startDate = subDays(resetHours(new Date()), 7);
|
||||
|
||||
@ -267,7 +254,7 @@ export class DataGatheringService {
|
||||
}
|
||||
|
||||
private async getSymbolsMax() {
|
||||
const startDate = new Date(getUtc('2000-01-01'));
|
||||
const startDate = new Date(getUtc('2015-01-01'));
|
||||
|
||||
const customSymbolsToGather = await this.getCustomSymbolsToGather(
|
||||
startDate
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { getYesterday } from '@ghostfolio/helper';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource } from '@prisma/client';
|
||||
import * as bent from 'bent';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { format } from 'date-fns';
|
||||
@ -8,12 +9,16 @@ import { DataProviderInterface } from '../../interfaces/data-provider.interface'
|
||||
import { Granularity } from '../../interfaces/granularity.type';
|
||||
import {
|
||||
IDataProviderHistoricalResponse,
|
||||
IDataProviderResponse
|
||||
IDataProviderResponse,
|
||||
MarketState
|
||||
} from '../../interfaces/interfaces';
|
||||
import { PrismaService } from '../../prisma.service';
|
||||
import { ScraperConfig } from './interfaces/scraper-config.interface';
|
||||
|
||||
@Injectable()
|
||||
export class GhostfolioScraperApiService implements DataProviderInterface {
|
||||
private static NUMERIC_REGEXP = /[-]{0,1}[\d]*[.,]{0,1}[\d]+/g;
|
||||
|
||||
public constructor(private prisma: PrismaService) {}
|
||||
|
||||
public async get(
|
||||
@ -26,7 +31,7 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
||||
try {
|
||||
const symbol = aSymbols[0];
|
||||
|
||||
const scraperConfig = await this.getScraperConfig(symbol);
|
||||
const scraperConfig = await this.getScraperConfigurationBySymbol(symbol);
|
||||
|
||||
const { marketPrice } = await this.prisma.marketData.findFirst({
|
||||
orderBy: {
|
||||
@ -41,7 +46,8 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
||||
[symbol]: {
|
||||
marketPrice,
|
||||
currency: scraperConfig?.currency,
|
||||
isMarketOpen: false,
|
||||
dataSource: DataSource.GHOSTFOLIO,
|
||||
marketState: MarketState.delayed,
|
||||
name: scraperConfig?.name
|
||||
}
|
||||
};
|
||||
@ -67,19 +73,18 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
||||
try {
|
||||
const symbol = aSymbols[0];
|
||||
|
||||
const scraperConfig = await this.getScraperConfig(symbol);
|
||||
const scraperConfiguration = await this.getScraperConfigurationBySymbol(
|
||||
symbol
|
||||
);
|
||||
|
||||
const get = bent(scraperConfig?.url, 'GET', 'string', 200, {});
|
||||
const get = bent(scraperConfiguration?.url, 'GET', 'string', 200, {});
|
||||
|
||||
const html = await get();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const string = $(scraperConfig?.selector)
|
||||
.text()
|
||||
.replace('CHF', '')
|
||||
.trim();
|
||||
|
||||
const value = parseFloat(string);
|
||||
const value = this.extractNumberFromString(
|
||||
$(scraperConfiguration?.selector).text()
|
||||
);
|
||||
|
||||
return {
|
||||
[symbol]: {
|
||||
@ -95,7 +100,7 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
||||
return {};
|
||||
}
|
||||
|
||||
private async getScraperConfig(aSymbol: string) {
|
||||
public async getScraperConfigurations(): Promise<ScraperConfig[]> {
|
||||
try {
|
||||
const {
|
||||
value: scraperConfigString
|
||||
@ -106,11 +111,27 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
||||
where: { key: 'SCRAPER_CONFIG' }
|
||||
});
|
||||
|
||||
return JSON.parse(scraperConfigString).find((item) => {
|
||||
return item.symbol === aSymbol;
|
||||
});
|
||||
return JSON.parse(scraperConfigString);
|
||||
} catch {}
|
||||
|
||||
return {};
|
||||
return [];
|
||||
}
|
||||
|
||||
private extractNumberFromString(aString: string): number {
|
||||
try {
|
||||
const [numberString] = aString.match(
|
||||
GhostfolioScraperApiService.NUMERIC_REGEXP
|
||||
);
|
||||
return parseFloat(numberString.trim());
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async getScraperConfigurationBySymbol(aSymbol: string) {
|
||||
const scraperConfigurations = await this.getScraperConfigurations();
|
||||
return scraperConfigurations.find((scraperConfiguration) => {
|
||||
return scraperConfiguration.symbol === aSymbol;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,9 @@
|
||||
import { Currency } from '@prisma/client';
|
||||
|
||||
export interface ScraperConfig {
|
||||
currency: Currency;
|
||||
name: string;
|
||||
selector: string;
|
||||
symbol: string;
|
||||
url: string;
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import { getToday, getYesterday } from '@ghostfolio/helper';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource } from '@prisma/client';
|
||||
import * as bent from 'bent';
|
||||
import { format, subMonths, subWeeks, subYears } from 'date-fns';
|
||||
|
||||
@ -8,7 +9,8 @@ import { DataProviderInterface } from '../../interfaces/data-provider.interface'
|
||||
import { Granularity } from '../../interfaces/granularity.type';
|
||||
import {
|
||||
IDataProviderHistoricalResponse,
|
||||
IDataProviderResponse
|
||||
IDataProviderResponse,
|
||||
MarketState
|
||||
} from '../../interfaces/interfaces';
|
||||
import { PrismaService } from '../../prisma.service';
|
||||
|
||||
@ -38,8 +40,9 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
||||
return {
|
||||
'GF.FEAR_AND_GREED_INDEX': {
|
||||
currency: undefined,
|
||||
isMarketOpen: true,
|
||||
dataSource: DataSource.RAKUTEN,
|
||||
marketPrice: fgi.now.value,
|
||||
marketState: MarketState.open,
|
||||
name: RakutenRapidApiService.FEAR_AND_GREED_INDEX_NAME
|
||||
}
|
||||
};
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { isCrypto, isCurrency, parseCurrency } from '@ghostfolio/helper';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource } from '@prisma/client';
|
||||
import { format } from 'date-fns';
|
||||
import * as yahooFinance from 'yahoo-finance';
|
||||
|
||||
@ -9,6 +10,7 @@ import {
|
||||
IDataProviderHistoricalResponse,
|
||||
IDataProviderResponse,
|
||||
Industry,
|
||||
MarketState,
|
||||
Sector,
|
||||
Type
|
||||
} from '../../interfaces/interfaces';
|
||||
@ -48,9 +50,12 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
|
||||
response[symbol] = {
|
||||
currency: parseCurrency(value.price?.currency),
|
||||
dataSource: DataSource.YAHOO,
|
||||
exchange: this.parseExchange(value.price?.exchangeName),
|
||||
isMarketOpen:
|
||||
value.price?.marketState === 'REGULAR' || isCrypto(symbol),
|
||||
marketState:
|
||||
value.price?.marketState === 'REGULAR' || isCrypto(symbol)
|
||||
? MarketState.open
|
||||
: MarketState.closed,
|
||||
marketPrice: value.price?.regularMarketPrice || 0,
|
||||
name: value.price?.longName || value.price?.shortName || symbol,
|
||||
type: this.parseType(this.getType(symbol, value))
|
||||
|
@ -7,6 +7,7 @@ export interface Environment extends CleanedEnvAccessors {
|
||||
ENABLE_FEATURE_CUSTOM_SYMBOLS: boolean;
|
||||
ENABLE_FEATURE_FEAR_AND_GREED_INDEX: boolean;
|
||||
ENABLE_FEATURE_SOCIAL_LOGIN: boolean;
|
||||
ENABLE_FEATURE_SUBSCRIPTION: boolean;
|
||||
GOOGLE_CLIENT_ID: string;
|
||||
GOOGLE_SECRET: string;
|
||||
JWT_SECRET_KEY: string;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Currency, Platform } from '@prisma/client';
|
||||
import { Account, Currency, DataSource, Platform } from '@prisma/client';
|
||||
|
||||
import { OrderType } from '../../models/order-type';
|
||||
|
||||
@ -12,6 +12,12 @@ export const Industry = {
|
||||
Software: 'Software'
|
||||
};
|
||||
|
||||
export const MarketState = {
|
||||
closed: 'closed',
|
||||
delayed: 'delayed',
|
||||
open: 'open'
|
||||
};
|
||||
|
||||
export const Sector = {
|
||||
Consumer: 'Consumer',
|
||||
Healthcare: 'Healthcare',
|
||||
@ -27,11 +33,11 @@ export const Type = {
|
||||
};
|
||||
|
||||
export interface IOrder {
|
||||
account: Account;
|
||||
currency: Currency;
|
||||
date: string;
|
||||
fee: number;
|
||||
id?: string;
|
||||
platform: Platform;
|
||||
quantity: number;
|
||||
symbol: string;
|
||||
type: OrderType;
|
||||
@ -45,12 +51,13 @@ export interface IDataProviderHistoricalResponse {
|
||||
|
||||
export interface IDataProviderResponse {
|
||||
currency: Currency;
|
||||
dataSource: DataSource;
|
||||
exchange?: string;
|
||||
industry?: Industry;
|
||||
isMarketOpen: boolean;
|
||||
marketChange?: number;
|
||||
marketChangePercent?: number;
|
||||
marketPrice: number;
|
||||
marketState: MarketState;
|
||||
name: string;
|
||||
sector?: Sector;
|
||||
type?: Type;
|
||||
@ -59,6 +66,8 @@ export interface IDataProviderResponse {
|
||||
|
||||
export type Industry = typeof Industry[keyof typeof Industry];
|
||||
|
||||
export type MarketState = typeof MarketState[keyof typeof MarketState];
|
||||
|
||||
export type Sector = typeof Sector[keyof typeof Sector];
|
||||
|
||||
export type Type = typeof Type[keyof typeof Type];
|
||||
|
@ -2,7 +2,8 @@ import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
@Injectable()
|
||||
export class PrismaService extends PrismaClient
|
||||
export class PrismaService
|
||||
extends PrismaClient
|
||||
implements OnModuleInit, OnModuleDestroy {
|
||||
async onModuleInit() {
|
||||
await this.$connect();
|
||||
|
@ -2,14 +2,14 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { Portfolio } from '../models/portfolio';
|
||||
import { Rule } from '../models/rule';
|
||||
import { AccountClusterRiskCurrentInvestment } from '../models/rules/account-cluster-risk/current-investment';
|
||||
import { AccountClusterRiskInitialInvestment } from '../models/rules/account-cluster-risk/initial-investment';
|
||||
import { AccountClusterRiskSingleAccount } from '../models/rules/account-cluster-risk/single-account';
|
||||
import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from '../models/rules/currency-cluster-risk/base-currency-current-investment';
|
||||
import { CurrencyClusterRiskBaseCurrencyInitialInvestment } from '../models/rules/currency-cluster-risk/base-currency-initial-investment';
|
||||
import { CurrencyClusterRiskCurrentInvestment } from '../models/rules/currency-cluster-risk/current-investment';
|
||||
import { CurrencyClusterRiskInitialInvestment } from '../models/rules/currency-cluster-risk/initial-investment';
|
||||
import { FeeRatioInitialInvestment } from '../models/rules/fees/fee-ratio-initial-investment';
|
||||
import { PlatformClusterRiskCurrentInvestment } from '../models/rules/platform-cluster-risk/current-investment';
|
||||
import { PlatformClusterRiskInitialInvestment } from '../models/rules/platform-cluster-risk/initial-investment';
|
||||
import { PlatformClusterRiskSinglePlatform } from '../models/rules/platform-cluster-risk/single-platform';
|
||||
|
||||
@Injectable()
|
||||
export class RulesService {
|
||||
@ -39,6 +39,17 @@ export class RulesService {
|
||||
|
||||
private getDefaultRuleSettings(aUserSettings: { baseCurrency: string }) {
|
||||
return {
|
||||
[AccountClusterRiskCurrentInvestment.name]: {
|
||||
baseCurrency: aUserSettings.baseCurrency,
|
||||
isActive: true,
|
||||
threshold: 0.5
|
||||
},
|
||||
[AccountClusterRiskInitialInvestment.name]: {
|
||||
baseCurrency: aUserSettings.baseCurrency,
|
||||
isActive: true,
|
||||
threshold: 0.5
|
||||
},
|
||||
[AccountClusterRiskSingleAccount.name]: { isActive: true },
|
||||
[CurrencyClusterRiskBaseCurrencyInitialInvestment.name]: {
|
||||
baseCurrency: aUserSettings.baseCurrency,
|
||||
isActive: true
|
||||
@ -61,18 +72,7 @@ export class RulesService {
|
||||
baseCurrency: aUserSettings.baseCurrency,
|
||||
isActive: true,
|
||||
threshold: 0.01
|
||||
},
|
||||
[PlatformClusterRiskCurrentInvestment.name]: {
|
||||
baseCurrency: aUserSettings.baseCurrency,
|
||||
isActive: true,
|
||||
threshold: 0.5
|
||||
},
|
||||
[PlatformClusterRiskInitialInvestment.name]: {
|
||||
baseCurrency: aUserSettings.baseCurrency,
|
||||
isActive: true,
|
||||
threshold: 0.5
|
||||
},
|
||||
[PlatformClusterRiskSinglePlatform.name]: { isActive: true }
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -9,11 +9,6 @@ const routes: Routes = [
|
||||
loadChildren: () =>
|
||||
import('./pages/about/about-page.module').then((m) => m.AboutPageModule)
|
||||
},
|
||||
{
|
||||
path: 'admin',
|
||||
loadChildren: () =>
|
||||
import('./pages/admin/admin-page.module').then((m) => m.AdminPageModule)
|
||||
},
|
||||
{
|
||||
path: 'account',
|
||||
loadChildren: () =>
|
||||
@ -21,6 +16,18 @@ const routes: Routes = [
|
||||
(m) => m.AccountPageModule
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'accounts',
|
||||
loadChildren: () =>
|
||||
import('./pages/accounts/accounts-page.module').then(
|
||||
(m) => m.AccountsPageModule
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'admin',
|
||||
loadChildren: () =>
|
||||
import('./pages/admin/admin-page.module').then((m) => m.AdminPageModule)
|
||||
},
|
||||
{
|
||||
path: 'auth',
|
||||
loadChildren: () =>
|
||||
|
@ -6,6 +6,8 @@ import {
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { NavigationEnd, Router } from '@angular/router';
|
||||
import { InfoItem } from '@ghostfolio/api/app/info/interfaces/info-item.interface';
|
||||
import { User } from '@ghostfolio/api/app/user/interfaces/user.interface';
|
||||
import {
|
||||
hasPermission,
|
||||
permissions,
|
||||
@ -13,8 +15,6 @@ import {
|
||||
secondaryColorHex
|
||||
} from '@ghostfolio/helper';
|
||||
import { MaterialCssVarsService } from 'angular-material-css-vars';
|
||||
import { InfoItem } from 'apps/api/src/app/info/interfaces/info-item.interface';
|
||||
import { User } from 'apps/api/src/app/user/interfaces/user.interface';
|
||||
import { Subject } from 'rxjs';
|
||||
import { filter, takeUntil } from 'rxjs/operators';
|
||||
|
||||
@ -74,7 +74,7 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
|
||||
this.canCreateAccount = hasPermission(
|
||||
this.user.permissions,
|
||||
permissions.createAccount
|
||||
permissions.createUserAccount
|
||||
);
|
||||
|
||||
this.cd.markForCheck();
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { Access } from 'apps/api/src/app/access/interfaces/access.interface';
|
||||
import { Access } from '@ghostfolio/api/app/access/interfaces/access.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'gf-access-table',
|
||||
|
@ -0,0 +1,98 @@
|
||||
<table
|
||||
class="w-100"
|
||||
matSort
|
||||
matSortActive="account"
|
||||
matSortDirection="desc"
|
||||
mat-table
|
||||
[dataSource]="dataSource"
|
||||
>
|
||||
<ng-container matColumnDef="account">
|
||||
<th *matHeaderCellDef i18n mat-header-cell mat-sort-header>Name</th>
|
||||
<td *matCellDef="let element" mat-cell>
|
||||
{{ element.name }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="type">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="d-none d-lg-table-cell justify-content-center"
|
||||
i18n
|
||||
mat-header-cell
|
||||
mat-sort-header
|
||||
>
|
||||
Type
|
||||
</th>
|
||||
<td
|
||||
mat-cell
|
||||
*matCellDef="let element"
|
||||
class="d-none d-lg-table-cell text-center"
|
||||
>
|
||||
<div class="d-inline-flex justify-content-center px-2 py-1 type-badge">
|
||||
<span>{{ element.accountType }}</span>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="platform">
|
||||
<th *matHeaderCellDef i18n mat-header-cell mat-sort-header>Platform</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<div class="d-flex">
|
||||
<gf-symbol-icon
|
||||
*ngIf="element.Platform?.url"
|
||||
class="mr-1"
|
||||
[tooltip]=""
|
||||
[url]="element.Platform?.url"
|
||||
></gf-symbol-icon>
|
||||
<span>{{ element.Platform?.name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th *matHeaderCellDef class="px-0 text-center" i18n mat-header-cell></th>
|
||||
<td *matCellDef="let element" class="px-0 text-center" mat-cell>
|
||||
<button
|
||||
class="mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
[matMenuTriggerFor]="accountMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||
</button>
|
||||
<mat-menu #accountMenu="matMenu" xPosition="before">
|
||||
<button i18n mat-menu-item (click)="onUpdateAccount(element)">
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
i18n
|
||||
mat-menu-item
|
||||
[disabled]="element.isDefault || element.Order?.length > 0"
|
||||
(click)="onDeleteAccount(element.id)"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="transactions">
|
||||
<th *matHeaderCellDef i18n mat-header-cell mat-sort-header>Transactions</th>
|
||||
<td *matCellDef="let element" mat-cell>
|
||||
{{ element.Order?.length }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
||||
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
|
||||
</table>
|
||||
|
||||
<ngx-skeleton-loader
|
||||
*ngIf="isLoading"
|
||||
animation="pulse"
|
||||
class="px-4 py-3"
|
||||
[theme]="{
|
||||
height: '1.5rem',
|
||||
width: '100%'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
@ -0,0 +1,59 @@
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
::ng-deep {
|
||||
.mat-form-field-infix {
|
||||
border-top: 0 solid transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-table {
|
||||
td {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
th {
|
||||
::ng-deep {
|
||||
.mat-sort-header-container {
|
||||
justify-content: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mat-row {
|
||||
&:nth-child(even) {
|
||||
background-color: rgba(
|
||||
var(--dark-primary-text),
|
||||
var(--palette-background-hover-alpha)
|
||||
);
|
||||
}
|
||||
|
||||
.type-badge {
|
||||
background-color: rgba(var(--dark-primary-text), 0.05);
|
||||
border-radius: 1rem;
|
||||
line-height: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
.mat-form-field {
|
||||
color: rgba(var(--light-primary-text));
|
||||
}
|
||||
|
||||
.mat-table {
|
||||
.mat-row {
|
||||
&:nth-child(even) {
|
||||
background-color: rgba(
|
||||
var(--light-primary-text),
|
||||
var(--palette-background-hover-alpha)
|
||||
);
|
||||
}
|
||||
|
||||
.type-badge {
|
||||
background-color: rgba(var(--light-primary-text), 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Account as AccountModel } from '@prisma/client';
|
||||
import { Subject, Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'gf-accounts-table',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: './accounts-table.component.html',
|
||||
styleUrls: ['./accounts-table.component.scss']
|
||||
})
|
||||
export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
|
||||
@Input() accounts: AccountModel[];
|
||||
@Input() baseCurrency: string;
|
||||
@Input() deviceType: string;
|
||||
@Input() locale: string;
|
||||
@Input() showActions: boolean;
|
||||
|
||||
@Output() accountDeleted = new EventEmitter<string>();
|
||||
@Output() accountToUpdate = new EventEmitter<AccountModel>();
|
||||
|
||||
@ViewChild(MatSort) sort: MatSort;
|
||||
|
||||
public dataSource: MatTableDataSource<AccountModel> = new MatTableDataSource();
|
||||
public displayedColumns = [];
|
||||
public isLoading = true;
|
||||
public routeQueryParams: Subscription;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor(
|
||||
private dialog: MatDialog,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router
|
||||
) {}
|
||||
|
||||
public ngOnInit() {}
|
||||
|
||||
public ngOnChanges() {
|
||||
this.displayedColumns = ['account', 'type', 'platform', 'transactions'];
|
||||
|
||||
if (this.showActions) {
|
||||
this.displayedColumns.push('actions');
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
if (this.accounts) {
|
||||
this.dataSource = new MatTableDataSource(this.accounts);
|
||||
this.dataSource.sort = this.sort;
|
||||
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
public onDeleteAccount(aId: string) {
|
||||
const confirmation = confirm('Do you really want to delete this account?');
|
||||
|
||||
if (confirmation) {
|
||||
this.accountDeleted.emit(aId);
|
||||
}
|
||||
}
|
||||
|
||||
public onUpdateAccount(aAccount: AccountModel) {
|
||||
this.accountToUpdate.emit(aAccount);
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
|
||||
import { GfSymbolIconModule } from '../symbol-icon/symbol-icon.module';
|
||||
import { GfValueModule } from '../value/value.module';
|
||||
import { AccountsTableComponent } from './accounts-table.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AccountsTableComponent],
|
||||
exports: [AccountsTableComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfSymbolIconModule,
|
||||
GfValueModule,
|
||||
MatButtonModule,
|
||||
MatInputModule,
|
||||
MatMenuModule,
|
||||
MatSortModule,
|
||||
MatTableModule,
|
||||
NgxSkeletonLoaderModule,
|
||||
RouterModule
|
||||
],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfAccountsTableModule {}
|
@ -1,3 +1,9 @@
|
||||
<gf-symbol-icon
|
||||
*ngIf="symbolUrl"
|
||||
class="mr-1"
|
||||
size="large"
|
||||
[url]="symbolUrl"
|
||||
></gf-symbol-icon>
|
||||
<span class="flex-grow-1 text-truncate">{{ title }}</span>
|
||||
<button
|
||||
*ngIf="deviceType !== 'mobile'"
|
||||
|
@ -1,3 +1,4 @@
|
||||
:host {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
})
|
||||
export class DialogHeaderComponent implements OnInit {
|
||||
@Input() deviceType: string;
|
||||
@Input() symbolUrl: string;
|
||||
@Input() title: string;
|
||||
|
||||
@Output() closeButtonClicked = new EventEmitter<void>();
|
||||
|
@ -1,13 +1,14 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module';
|
||||
|
||||
import { DialogHeaderComponent } from './dialog-header.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [DialogHeaderComponent],
|
||||
exports: [DialogHeaderComponent],
|
||||
imports: [CommonModule, MatButtonModule],
|
||||
imports: [CommonModule, GfSymbolIconModule, MatButtonModule],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
|
@ -37,7 +37,15 @@
|
||||
>Transactions</a
|
||||
>
|
||||
<a
|
||||
*ngIf="canAccessAdminAccessControl"
|
||||
class="d-none d-sm-block mx-1"
|
||||
[routerLink]="['/accounts']"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[color]="currentRoute === 'accounts' ? 'primary' : null"
|
||||
>Accounts</a
|
||||
>
|
||||
<a
|
||||
*ngIf="hasPermissionToAccessAdminControl"
|
||||
class="d-none d-sm-block mx-1"
|
||||
[routerLink]="['/admin']"
|
||||
i18n
|
||||
@ -141,16 +149,24 @@
|
||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'transactions' }"
|
||||
>Transactions</a
|
||||
>
|
||||
<a
|
||||
class="d-block d-sm-none"
|
||||
[routerLink]="['/accounts']"
|
||||
i18n
|
||||
mat-menu-item
|
||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'accounts' }"
|
||||
>Accounts</a
|
||||
>
|
||||
<a
|
||||
class="align-items-center d-flex"
|
||||
[routerLink]="['/account']"
|
||||
i18n
|
||||
mat-menu-item
|
||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'account' }"
|
||||
>Account</a
|
||||
>Ghostfolio Account</a
|
||||
>
|
||||
<a
|
||||
*ngIf="canAccessAdminAccessControl"
|
||||
*ngIf="hasPermissionToAccessAdminControl"
|
||||
class="d-block d-sm-none"
|
||||
[routerLink]="['/admin']"
|
||||
i18n
|
||||
|
@ -6,17 +6,16 @@ import {
|
||||
} from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { Router } from '@angular/router';
|
||||
import { InfoItem } from '@ghostfolio/api/app/info/interfaces/info-item.interface';
|
||||
import { User } from '@ghostfolio/api/app/user/interfaces/user.interface';
|
||||
import { LoginWithAccessTokenDialog } from '@ghostfolio/client/pages/login/login-with-access-token-dialog/login-with-access-token-dialog.component';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||
import { hasPermission, permissions } from '@ghostfolio/helper';
|
||||
import { InfoItem } from 'apps/api/src/app/info/interfaces/info-item.interface';
|
||||
import { User } from 'apps/api/src/app/user/interfaces/user.interface';
|
||||
import { EMPTY, Subject } from 'rxjs';
|
||||
import { catchError, takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { LoginWithAccessTokenDialog } from '../../pages/login/login-with-access-token-dialog/login-with-access-token-dialog.component';
|
||||
import { DataService } from '../../services/data.service';
|
||||
import { ImpersonationStorageService } from '../../services/impersonation-storage.service';
|
||||
import { TokenStorageService } from '../../services/token-storage.service';
|
||||
|
||||
@Component({
|
||||
selector: 'gf-header',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
@ -28,8 +27,8 @@ export class HeaderComponent implements OnChanges {
|
||||
@Input() info: InfoItem;
|
||||
@Input() user: User;
|
||||
|
||||
public canAccessAdminAccessControl: boolean;
|
||||
public hasPermissionToUseSocialLogin: boolean;
|
||||
public hasPermissionToAccessAdminControl: boolean;
|
||||
public hasPermissionForSocialLogin: boolean;
|
||||
public impersonationId: string;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
@ -50,15 +49,15 @@ export class HeaderComponent implements OnChanges {
|
||||
|
||||
public ngOnChanges() {
|
||||
if (this.user) {
|
||||
this.canAccessAdminAccessControl = hasPermission(
|
||||
this.hasPermissionToAccessAdminControl = hasPermission(
|
||||
this.user.permissions,
|
||||
permissions.accessAdminControl
|
||||
);
|
||||
}
|
||||
|
||||
this.hasPermissionToUseSocialLogin = hasPermission(
|
||||
this.hasPermissionForSocialLogin = hasPermission(
|
||||
this.info?.globalPermissions,
|
||||
permissions.useSocialLogin
|
||||
permissions.enableSocialLogin
|
||||
);
|
||||
}
|
||||
|
||||
@ -82,7 +81,7 @@ export class HeaderComponent implements OnChanges {
|
||||
autoFocus: false,
|
||||
data: {
|
||||
accessToken: '',
|
||||
hasPermissionToUseSocialLogin: this.hasPermissionToUseSocialLogin
|
||||
hasPermissionToUseSocialLogin: this.hasPermissionForSocialLogin
|
||||
},
|
||||
width: '30rem'
|
||||
});
|
||||
|
@ -4,8 +4,8 @@ import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { LoginWithAccessTokenDialogModule } from '@ghostfolio/client/pages/login/login-with-access-token-dialog/login-with-access-token-dialog.module';
|
||||
|
||||
import { LoginWithAccessTokenDialogModule } from '../../pages/login/login-with-access-token-dialog/login-with-access-token-dialog.module';
|
||||
import { GfLogoModule } from '../logo/logo.module';
|
||||
import { HeaderComponent } from './header.component';
|
||||
|
||||
|
@ -9,8 +9,8 @@ import {
|
||||
OnInit,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { PortfolioItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-item.interface';
|
||||
import { primaryColorRgb } from '@ghostfolio/helper';
|
||||
import { PortfolioItem } from 'apps/api/src/app/portfolio/interfaces/portfolio-item.interface';
|
||||
import {
|
||||
LineController,
|
||||
LineElement,
|
||||
|
@ -5,9 +5,9 @@ import {
|
||||
Inject
|
||||
} from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { isToday, parse } from 'date-fns';
|
||||
|
||||
import { DataService } from '../../services/data.service';
|
||||
import { LineChartItem } from '../line-chart/interfaces/line-chart.interface';
|
||||
import { PositionDetailDialogParams } from './interfaces/interfaces';
|
||||
|
||||
|
@ -2,9 +2,9 @@ import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
|
||||
import { GfLineChartModule } from '../../components/line-chart/line-chart.module';
|
||||
import { GfDialogFooterModule } from '../dialog-footer/dialog-footer.module';
|
||||
import { GfDialogHeaderModule } from '../dialog-header/dialog-header.module';
|
||||
import { GfFearAndGreedIndexModule } from '../fear-and-greed-index/fear-and-greed-index.module';
|
||||
|
@ -5,8 +5,8 @@ import {
|
||||
OnChanges,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { PortfolioOverview } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-overview.interface';
|
||||
import { Currency } from '@prisma/client';
|
||||
import { PortfolioOverview } from 'apps/api/src/app/portfolio/interfaces/portfolio-overview.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'gf-portfolio-overview',
|
||||
|
@ -34,16 +34,16 @@
|
||||
<div *ngIf="showDetails" class="row">
|
||||
<div class="d-flex col justify-content-end">
|
||||
<gf-value
|
||||
colorizeSign="true"
|
||||
isCurrency="true"
|
||||
[colorizeSign]="true"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : performance?.currentNetPerformance"
|
||||
></gf-value>
|
||||
</div>
|
||||
<div class="col">
|
||||
<gf-value
|
||||
colorizeSign="true"
|
||||
isPercent="true"
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="locale"
|
||||
[value]="
|
||||
isLoading ? undefined : performance?.currentNetPerformancePercent
|
||||
|
@ -7,8 +7,8 @@ import {
|
||||
OnInit,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { PortfolioPerformance } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-performance.interface';
|
||||
import { Currency } from '@prisma/client';
|
||||
import { PortfolioPerformance } from 'apps/api/src/app/portfolio/interfaces/portfolio-performance.interface';
|
||||
import { CountUp } from 'countup.js';
|
||||
import { isNumber } from 'lodash';
|
||||
|
||||
|
@ -16,17 +16,17 @@
|
||||
<div class="d-flex flex-column flex-wrap justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end mb-2"
|
||||
colorizeSign="true"
|
||||
position="end"
|
||||
[colorizeSign]="true"
|
||||
[currency]="baseCurrency"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : performance?.currentGrossPerformance"
|
||||
></gf-value>
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
colorizeSign="true"
|
||||
isPercent="true"
|
||||
position="end"
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="locale"
|
||||
[value]="
|
||||
isLoading ? undefined : performance?.currentGrossPerformancePercent
|
||||
@ -39,17 +39,17 @@
|
||||
<div class="d-flex flex-column flex-wrap justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end mb-2"
|
||||
colorizeSign="true"
|
||||
position="end"
|
||||
[colorizeSign]="true"
|
||||
[currency]="baseCurrency"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : performance?.currentNetPerformance"
|
||||
></gf-value>
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
colorizeSign="true"
|
||||
isPercent="true"
|
||||
position="end"
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="locale"
|
||||
[value]="
|
||||
isLoading ? undefined : performance?.currentNetPerformancePercent
|
||||
|
@ -4,8 +4,8 @@ import {
|
||||
Input,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { PortfolioPerformance } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-performance.interface';
|
||||
import { Currency } from '@prisma/client';
|
||||
import { PortfolioPerformance } from 'apps/api/src/app/portfolio/interfaces/portfolio-performance.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'gf-portfolio-performance',
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
OnChanges,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { PortfolioItem } from 'apps/api/src/app/portfolio/interfaces/portfolio-item.interface';
|
||||
import { PortfolioItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-item.interface';
|
||||
import { endOfDay, parseISO, startOfDay } from 'date-fns';
|
||||
|
||||
@Component({
|
||||
|
@ -7,8 +7,8 @@ import {
|
||||
OnInit,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { Currency } from '@prisma/client';
|
||||
import { PortfolioPosition } from 'apps/api/src/app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { Tooltip } from 'chart.js';
|
||||
import { LinearScale } from 'chart.js';
|
||||
import { ArcElement } from 'chart.js';
|
||||
|
@ -3,5 +3,6 @@ export interface PositionDetailDialogParams {
|
||||
deviceType: string;
|
||||
locale: string;
|
||||
symbol: string;
|
||||
symbolUrl: string;
|
||||
title: string;
|
||||
}
|
||||
|
@ -5,9 +5,9 @@ import {
|
||||
Inject
|
||||
} from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { format, isSameMonth, isToday, parseISO } from 'date-fns';
|
||||
|
||||
import { DataService } from '../../../services/data.service';
|
||||
import { LineChartItem } from '../../line-chart/interfaces/line-chart.interface';
|
||||
import { PositionDetailDialogParams } from './interfaces/interfaces';
|
||||
|
||||
@ -23,6 +23,7 @@ export class PositionDetailDialog {
|
||||
public benchmarkDataItems: LineChartItem[];
|
||||
public currency: string;
|
||||
public firstBuyDate: string;
|
||||
public grossPerformance: number;
|
||||
public grossPerformancePercent: number;
|
||||
public historicalDataItems: LineChartItem[];
|
||||
public investment: number;
|
||||
@ -30,6 +31,7 @@ export class PositionDetailDialog {
|
||||
public maxPrice: number;
|
||||
public minPrice: number;
|
||||
public quantity: number;
|
||||
public transactionCount: number;
|
||||
|
||||
public constructor(
|
||||
private cd: ChangeDetectorRef,
|
||||
@ -44,18 +46,21 @@ export class PositionDetailDialog {
|
||||
averagePrice,
|
||||
currency,
|
||||
firstBuyDate,
|
||||
grossPerformance,
|
||||
grossPerformancePercent,
|
||||
historicalData,
|
||||
investment,
|
||||
marketPrice,
|
||||
maxPrice,
|
||||
minPrice,
|
||||
quantity
|
||||
quantity,
|
||||
transactionCount
|
||||
}) => {
|
||||
this.averagePrice = averagePrice;
|
||||
this.benchmarkDataItems = [];
|
||||
this.currency = currency;
|
||||
this.firstBuyDate = firstBuyDate;
|
||||
this.grossPerformance = quantity * grossPerformance;
|
||||
this.grossPerformancePercent = grossPerformancePercent;
|
||||
this.historicalDataItems = historicalData.map(
|
||||
(historicalDataItem) => {
|
||||
@ -75,6 +80,7 @@ export class PositionDetailDialog {
|
||||
this.maxPrice = maxPrice;
|
||||
this.minPrice = minPrice;
|
||||
this.quantity = quantity;
|
||||
this.transactionCount = transactionCount;
|
||||
|
||||
if (isToday(parseISO(this.firstBuyDate))) {
|
||||
// Add average price
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user