Compare commits
61 Commits
Author | SHA1 | Date | |
---|---|---|---|
7d03c373ac | |||
edb66bb166 | |||
54bbc8446b | |||
9933967e42 | |||
5618513d07 | |||
1397cd62a8 | |||
e7fb31d1a6 | |||
51e7b94ad0 | |||
9133ea38f3 | |||
a864c617b9 | |||
442df9d6f8 | |||
2de0e75cb8 | |||
1296f95602 | |||
e98dff877a | |||
964b37af30 | |||
2d89d12d31 | |||
19e2d54791 | |||
e24e5e1c44 | |||
a0d4ff7920 | |||
099ad18aaf | |||
e3cd99f5d2 | |||
6dea9093ba | |||
43104f81d0 | |||
6d2e3b6e40 | |||
1d9a31dbb8 | |||
ea0e92220c | |||
b57301ef50 | |||
67dbc6b014 | |||
2e5176bacf | |||
060846023f | |||
f06a0fbbee | |||
4ab6a1a071 | |||
93dcbeb6c7 | |||
b9f0a57522 | |||
174c1d1a62 | |||
f308ae7a13 | |||
a7a6b0608b | |||
15a61b7a20 | |||
d1eedf9726 | |||
30a592b524 | |||
de94494aa0 | |||
d3c6788ad5 | |||
3ec4a73b35 | |||
1050bfa098 | |||
595ec1d7b4 | |||
c8389599b6 | |||
8769fe4c90 | |||
4219e1121e | |||
f558eb8de8 | |||
fe2bd6eea8 | |||
035052be99 | |||
bcdd2780b3 | |||
22d1ed7920 | |||
39d9828f9f | |||
6333aa972d | |||
554f2f861f | |||
dcee651098 | |||
508a48f4c3 | |||
8466e3d73f | |||
9ae9904389 | |||
af022ae316 |
@ -1 +1,2 @@
|
|||||||
/dist
|
/dist
|
||||||
|
/test/import
|
||||||
|
155
CHANGELOG.md
155
CHANGELOG.md
@ -5,6 +5,161 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## 1.68.0 - 01.11.2021
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Prettified the generic scraper symbols in the portfolio proportion chart component
|
||||||
|
- Extended the statistics section on the about page by the active users count (7d)
|
||||||
|
- Extended the statistics section on the about page by the new users count
|
||||||
|
|
||||||
|
## 1.67.0 - 31.10.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added more details to the public page (currencies, sectors, continents and regions)
|
||||||
|
- Added a `Dockerfile` and documentation to build a _Docker_ image
|
||||||
|
|
||||||
|
## 1.66.0 - 30.10.2021
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the landing page
|
||||||
|
- Ordered the granted accesses by type
|
||||||
|
|
||||||
|
## 1.65.0 - 25.10.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the user interface for granting and revoking public access to share the portfolio
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Moved the data enhancer calls from the data provider (`get()`) to the data gathering service to reduce traffic to 3rd party data providers
|
||||||
|
- Changed the profile data gathering from every 12 hours to once every weekend
|
||||||
|
|
||||||
|
## 1.64.0 - 21.10.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support for more cryptocurrency symbols like _Avalanche_, _Polygon_, _Shiba Inu_ etc.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Changed the data provider service to handle a dynamic list of services
|
||||||
|
|
||||||
|
## 1.63.0 - 19.10.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added a public page to share the portfolio
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the skeleton loader size of the portfolio proportion chart component
|
||||||
|
|
||||||
|
### Todo
|
||||||
|
|
||||||
|
- Apply data migration (`yarn prisma migrate deploy`)
|
||||||
|
|
||||||
|
## 1.62.0 - 17.10.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Extended the validation message of the import functionality for transactions
|
||||||
|
|
||||||
|
## 1.61.0 - 15.10.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Extended the import functionality for transactions by `csv` files
|
||||||
|
- Introduced the primary data source
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Restricted the file selector of the import functionality for transactions to `csv` and `json`
|
||||||
|
|
||||||
|
## 1.60.0 - 13.10.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Extended the validation of the import functionality for transactions
|
||||||
|
- Valid data types
|
||||||
|
- Maximum number of orders
|
||||||
|
- No duplicate orders
|
||||||
|
- Data provider service returns data for the `dataSource` / `symbol` pair
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Harmonized the page layouts
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the broken line charts showing value labels
|
||||||
|
|
||||||
|
## 1.59.0 - 11.10.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added a data enhancer for symbol profile data (countries and sectors) via _Trackinsight_
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Changed the values of the global heat map to fixed-point notation
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the links of cryptocurrency assets in the positions table
|
||||||
|
- Fixed various values in the impersonation mode which have not been nullified
|
||||||
|
|
||||||
|
## 1.58.1 - 03.10.2021
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue in the symbol conversion for _Yahoo Finance_ (for a cryptocurrency with the same code as a currency)
|
||||||
|
|
||||||
|
## 1.58.0 - 02.10.2021
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the symbol conversion for _Yahoo Finance_: Support for _Solana USD_ (`SOL1-USD`)
|
||||||
|
- Improved the tooltips of the allocations page
|
||||||
|
- Upgraded `envalid` from version `7.1.0` to `7.2.1`
|
||||||
|
|
||||||
|
## 1.57.0 - 29.09.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added a protection for endpoints (subscriptions)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Reformatted the exchange rates table in the admin control panel
|
||||||
|
|
||||||
|
## 1.56.0 - 25.09.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added a story for the line chart component
|
||||||
|
- Added a story for the portfolio proportion chart component
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Changed the navigation to always show the portfolio page
|
||||||
|
- Migrated the data type of currencies from `enum` to `string` in the database
|
||||||
|
- Supported unlimited currencies (instead of `CHF`, `EUR`, `GBP` and `USD`)
|
||||||
|
- Respected the accounts' currencies in the exchange rate service
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Hid the actions from the accounts table in the _Presenter View_
|
||||||
|
- Hid the actions from the transactions table in the _Presenter View_
|
||||||
|
- Fixed the data gathering of the initial project setup (database seeding)
|
||||||
|
|
||||||
|
### Todo
|
||||||
|
|
||||||
|
- Apply data migration (`yarn prisma migrate deploy`)
|
||||||
|
|
||||||
## 1.55.0 - 20.09.2021
|
## 1.55.0 - 20.09.2021
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
51
Dockerfile
Normal file
51
Dockerfile
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
FROM node:14-alpine as builder
|
||||||
|
|
||||||
|
# Build application and add additional files
|
||||||
|
|
||||||
|
WORKDIR /ghostfolio
|
||||||
|
|
||||||
|
# Only add basic files without the application itself to avoid rebuilding
|
||||||
|
# layers when files (package.json etc.) have not changed
|
||||||
|
COPY ./CHANGELOG.md CHANGELOG.md
|
||||||
|
COPY ./LICENSE LICENSE
|
||||||
|
COPY ./package.json package.json
|
||||||
|
COPY ./yarn.lock yarn.lock
|
||||||
|
COPY ./prisma/schema.prisma prisma/schema.prisma
|
||||||
|
|
||||||
|
RUN yarn
|
||||||
|
|
||||||
|
# See https://github.com/nrwl/nx/issues/6586 for further details
|
||||||
|
COPY ./decorate-angular-cli.js decorate-angular-cli.js
|
||||||
|
RUN node decorate-angular-cli.js
|
||||||
|
|
||||||
|
COPY ./angular.json angular.json
|
||||||
|
COPY ./nx.json nx.json
|
||||||
|
COPY ./replace.build.js replace.build.js
|
||||||
|
COPY ./jest.preset.js jest.preset.js
|
||||||
|
COPY ./jest.config.js jest.config.js
|
||||||
|
COPY ./tsconfig.base.json tsconfig.base.json
|
||||||
|
COPY ./libs libs
|
||||||
|
COPY ./apps apps
|
||||||
|
|
||||||
|
RUN yarn build:all
|
||||||
|
|
||||||
|
# Prepare the dist image with additional node_modules
|
||||||
|
WORKDIR /ghostfolio/dist/apps/api
|
||||||
|
# package.json was generated by the build process, however the original
|
||||||
|
# yarn.lock needs to be used to ensure the same versions
|
||||||
|
COPY ./yarn.lock /ghostfolio/dist/apps/api/yarn.lock
|
||||||
|
|
||||||
|
RUN yarn
|
||||||
|
COPY prisma /ghostfolio/dist/apps/api/prisma
|
||||||
|
|
||||||
|
# Overwrite the generated package.json with the original one to ensure having
|
||||||
|
# all the scripts
|
||||||
|
COPY package.json /ghostfolio/dist/apps/api
|
||||||
|
RUN yarn database:generate-typings
|
||||||
|
|
||||||
|
# Image to run, copy everything needed from builder
|
||||||
|
FROM node:14-alpine
|
||||||
|
COPY --from=builder /ghostfolio/dist/apps /ghostfolio/apps
|
||||||
|
WORKDIR /ghostfolio/apps/api
|
||||||
|
EXPOSE 3333
|
||||||
|
CMD [ "node", "main" ]
|
47
README.md
47
README.md
@ -34,7 +34,7 @@
|
|||||||
|
|
||||||
Our official **[Ghostfolio Premium](https://ghostfol.io/pricing)** cloud offering is the easiest way to get started. Due to the time it saves, this will be the best option for most people. The revenue is used for covering the hosting costs.
|
Our official **[Ghostfolio Premium](https://ghostfol.io/pricing)** cloud offering is the easiest way to get started. Due to the time it saves, this will be the best option for most people. The revenue is used for covering the hosting costs.
|
||||||
|
|
||||||
If you prefer to run Ghostfolio on your own infrastructure, please find the source code and further instructions here on _GitHub_.
|
If you prefer to run Ghostfolio on your own infrastructure, please find further instructions in the section [Run with Docker](#run-with-docker).
|
||||||
|
|
||||||
## Why Ghostfolio?
|
## Why Ghostfolio?
|
||||||
|
|
||||||
@ -81,13 +81,44 @@ The backend is based on [NestJS](https://nestjs.com) using [PostgreSQL](https://
|
|||||||
|
|
||||||
The frontend is built with [Angular](https://angular.io) and uses [Angular Material](https://material.angular.io) with utility classes from [Bootstrap](https://getbootstrap.com).
|
The frontend is built with [Angular](https://angular.io) and uses [Angular Material](https://material.angular.io) with utility classes from [Bootstrap](https://getbootstrap.com).
|
||||||
|
|
||||||
## Getting Started
|
## Run with Docker
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
|
- [Docker](https://www.docker.com/products/docker-desktop)
|
||||||
|
|
||||||
|
### Setup Docker Image
|
||||||
|
|
||||||
|
Run the following commands to build and start the Docker image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker/docker-compose-build-local.yml build
|
||||||
|
docker-compose -f docker/docker-compose-build-local.yml up
|
||||||
|
```
|
||||||
|
|
||||||
|
### Setup Database
|
||||||
|
|
||||||
|
Run the following command to setup the database once Ghostfolio is running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker/docker-compose-build-local.yml exec ghostfolio yarn setup:database
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fetch Historical Data
|
||||||
|
|
||||||
|
Open http://localhost:3333 in your browser and accomplish these steps:
|
||||||
|
|
||||||
|
1. Login as _Admin_ with the following _Security Token_: `ae76872ae8f3419c6d6f64bf51888ecbcc703927a342d815fafe486acdb938da07d0cf44fca211a0be74a423238f535362d390a41e81e633a9ce668a6e31cdf9`
|
||||||
|
1. Go to the _Admin Control Panel_ and click _Gather All Data_ to fetch historical data
|
||||||
|
1. Click _Sign out_ and check out the _Live Demo_
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- [Docker](https://www.docker.com/products/docker-desktop)
|
||||||
- [Node.js](https://nodejs.org/en/download) (version 14+)
|
- [Node.js](https://nodejs.org/en/download) (version 14+)
|
||||||
- [Yarn](https://yarnpkg.com/en/docs/install)
|
- [Yarn](https://yarnpkg.com/en/docs/install)
|
||||||
- [Docker](https://www.docker.com/products/docker-desktop)
|
|
||||||
|
|
||||||
### Setup
|
### Setup
|
||||||
|
|
||||||
@ -101,18 +132,14 @@ The frontend is built with [Angular](https://angular.io) and uses [Angular Mater
|
|||||||
1. Go to the _Admin Control Panel_ and click _Gather All Data_ to fetch historical data
|
1. Go to the _Admin Control Panel_ and click _Gather All Data_ to fetch historical data
|
||||||
1. Click _Sign out_ and check out the _Live Demo_
|
1. Click _Sign out_ and check out the _Live Demo_
|
||||||
|
|
||||||
## Development
|
### Start Server
|
||||||
|
|
||||||
Please make sure you have completed the instructions from [_Setup_](#Setup).
|
|
||||||
|
|
||||||
### Start server
|
|
||||||
|
|
||||||
<ol type="a">
|
<ol type="a">
|
||||||
<li>Debug: Run <code>yarn watch:server</code> and click "Launch Program" in <i>Visual Studio Code</i></li>
|
<li>Debug: Run <code>yarn watch:server</code> and click "Launch Program" in <a href="https://code.visualstudio.com">Visual Studio Code</a></li>
|
||||||
<li>Serve: Run <code>yarn start:server</code></li>
|
<li>Serve: Run <code>yarn start:server</code></li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
### Start client
|
### Start Client
|
||||||
|
|
||||||
Run `yarn start:client`
|
Run `yarn start:client`
|
||||||
|
|
||||||
|
@ -35,6 +35,7 @@
|
|||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
|
"generatePackageJson": true,
|
||||||
"optimization": true,
|
"optimization": true,
|
||||||
"extractLicenses": true,
|
"extractLicenses": true,
|
||||||
"inspect": false,
|
"inspect": false,
|
||||||
@ -99,12 +100,12 @@
|
|||||||
{
|
{
|
||||||
"glob": "CHANGELOG.md",
|
"glob": "CHANGELOG.md",
|
||||||
"input": "",
|
"input": "",
|
||||||
"output": "./"
|
"output": "./assets"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"glob": "LICENSE",
|
"glob": "LICENSE",
|
||||||
"input": "",
|
"input": "",
|
||||||
"output": "./"
|
"output": "./assets"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"glob": "robots.txt",
|
"glob": "robots.txt",
|
||||||
|
@ -1,10 +1,29 @@
|
|||||||
import { Access } from '@ghostfolio/common/interfaces';
|
import { Access } from '@ghostfolio/common/interfaces';
|
||||||
|
import {
|
||||||
|
getPermissions,
|
||||||
|
hasPermission,
|
||||||
|
permissions
|
||||||
|
} from '@ghostfolio/common/permissions';
|
||||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import { Controller, Get, Inject, UseGuards } from '@nestjs/common';
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
HttpException,
|
||||||
|
Inject,
|
||||||
|
Param,
|
||||||
|
Post,
|
||||||
|
UseGuards
|
||||||
|
} from '@nestjs/common';
|
||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { Access as AccessModel } from '@prisma/client';
|
||||||
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
|
||||||
|
import { AccessModule } from './access.module';
|
||||||
import { AccessService } from './access.service';
|
import { AccessService } from './access.service';
|
||||||
|
import { CreateAccessDto } from './create-access.dto';
|
||||||
|
|
||||||
@Controller('access')
|
@Controller('access')
|
||||||
export class AccessController {
|
export class AccessController {
|
||||||
@ -20,13 +39,69 @@ export class AccessController {
|
|||||||
include: {
|
include: {
|
||||||
GranteeUser: true
|
GranteeUser: true
|
||||||
},
|
},
|
||||||
|
orderBy: { granteeUserId: 'asc' },
|
||||||
where: { userId: this.request.user.id }
|
where: { userId: this.request.user.id }
|
||||||
});
|
});
|
||||||
|
|
||||||
return accessesWithGranteeUser.map((access) => {
|
return accessesWithGranteeUser.map((access) => {
|
||||||
|
if (access.GranteeUser) {
|
||||||
return {
|
return {
|
||||||
granteeAlias: access.GranteeUser.alias
|
granteeAlias: access.GranteeUser?.alias,
|
||||||
|
id: access.id,
|
||||||
|
type: 'RESTRICTED_VIEW'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
granteeAlias: 'Public',
|
||||||
|
id: access.id,
|
||||||
|
type: 'PUBLIC'
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
public async createAccess(
|
||||||
|
@Body() data: CreateAccessDto
|
||||||
|
): Promise<AccessModel> {
|
||||||
|
if (
|
||||||
|
!hasPermission(
|
||||||
|
getPermissions(this.request.user.role),
|
||||||
|
permissions.createAccess
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
|
StatusCodes.FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.accessService.createAccess({
|
||||||
|
User: { connect: { id: this.request.user.id } }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
public async deleteAccess(@Param('id') id: string): Promise<AccessModule> {
|
||||||
|
if (
|
||||||
|
!hasPermission(
|
||||||
|
getPermissions(this.request.user.role),
|
||||||
|
permissions.deleteAccess
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
|
StatusCodes.FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.accessService.deleteAccess({
|
||||||
|
id_userId: {
|
||||||
|
id,
|
||||||
|
userId: this.request.user.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,9 @@ import { AccessController } from './access.controller';
|
|||||||
import { AccessService } from './access.service';
|
import { AccessService } from './access.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
|
||||||
controllers: [AccessController],
|
controllers: [AccessController],
|
||||||
|
exports: [AccessService],
|
||||||
|
imports: [],
|
||||||
providers: [AccessService, PrismaService]
|
providers: [AccessService, PrismaService]
|
||||||
})
|
})
|
||||||
export class AccessModule {}
|
export class AccessModule {}
|
||||||
|
@ -1,12 +1,23 @@
|
|||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { AccessWithGranteeUser } from '@ghostfolio/common/types';
|
import { AccessWithGranteeUser } from '@ghostfolio/common/types';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Prisma } from '@prisma/client';
|
import { Access, Prisma } from '@prisma/client';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AccessService {
|
export class AccessService {
|
||||||
public constructor(private readonly prismaService: PrismaService) {}
|
public constructor(private readonly prismaService: PrismaService) {}
|
||||||
|
|
||||||
|
public async access(
|
||||||
|
accessWhereInput: Prisma.AccessWhereInput
|
||||||
|
): Promise<AccessWithGranteeUser | null> {
|
||||||
|
return this.prismaService.access.findFirst({
|
||||||
|
include: {
|
||||||
|
GranteeUser: true
|
||||||
|
},
|
||||||
|
where: accessWhereInput
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public async accesses(params: {
|
public async accesses(params: {
|
||||||
include?: Prisma.AccessInclude;
|
include?: Prisma.AccessInclude;
|
||||||
skip?: number;
|
skip?: number;
|
||||||
@ -26,4 +37,18 @@ export class AccessService {
|
|||||||
where
|
where
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async createAccess(data: Prisma.AccessCreateInput): Promise<Access> {
|
||||||
|
return this.prismaService.access.create({
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteAccess(
|
||||||
|
where: Prisma.AccessWhereUniqueInput
|
||||||
|
): Promise<Access> {
|
||||||
|
return this.prismaService.access.delete({
|
||||||
|
where
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
1
apps/api/src/app/access/create-access.dto.ts
Normal file
1
apps/api/src/app/access/create-access.dto.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export class CreateAccessDto {}
|
@ -1,7 +1,7 @@
|
|||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Account, Currency, Order, Platform, Prisma } from '@prisma/client';
|
import { Account, Order, Platform, Prisma } from '@prisma/client';
|
||||||
|
|
||||||
import { CashDetails } from './interfaces/cash-details.interface';
|
import { CashDetails } from './interfaces/cash-details.interface';
|
||||||
|
|
||||||
@ -95,7 +95,7 @@ export class AccountService {
|
|||||||
|
|
||||||
public async getCashDetails(
|
public async getCashDetails(
|
||||||
aUserId: string,
|
aUserId: string,
|
||||||
aCurrency: Currency
|
aCurrency: string
|
||||||
): Promise<CashDetails> {
|
): Promise<CashDetails> {
|
||||||
let totalCashBalance = 0;
|
let totalCashBalance = 0;
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { AccountType, Currency } from '@prisma/client';
|
import { AccountType } from '@prisma/client';
|
||||||
import { IsNumber, IsString, ValidateIf } from 'class-validator';
|
import { IsNumber, IsString, ValidateIf } from 'class-validator';
|
||||||
|
|
||||||
export class CreateAccountDto {
|
export class CreateAccountDto {
|
||||||
@ -9,7 +9,7 @@ export class CreateAccountDto {
|
|||||||
balance: number;
|
balance: number;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
currency: Currency;
|
currency: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { AccountType, Currency } from '@prisma/client';
|
import { AccountType } from '@prisma/client';
|
||||||
import { IsNumber, IsString, ValidateIf } from 'class-validator';
|
import { IsNumber, IsString, ValidateIf } from 'class-validator';
|
||||||
|
|
||||||
export class UpdateAccountDto {
|
export class UpdateAccountDto {
|
||||||
@ -9,7 +9,7 @@ export class UpdateAccountDto {
|
|||||||
balance: number;
|
balance: number;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
currency: Currency;
|
currency: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -3,9 +3,9 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration.ser
|
|||||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
|
import { baseCurrency } from '@ghostfolio/common/config';
|
||||||
import { AdminData } from '@ghostfolio/common/interfaces';
|
import { AdminData } from '@ghostfolio/common/interfaces';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Currency } from '@prisma/client';
|
|
||||||
import { differenceInDays } from 'date-fns';
|
import { differenceInDays } from 'date-fns';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -20,53 +20,22 @@ export class AdminService {
|
|||||||
|
|
||||||
public async get(): Promise<AdminData> {
|
public async get(): Promise<AdminData> {
|
||||||
return {
|
return {
|
||||||
exchangeRates: [
|
exchangeRates: this.exchangeRateDataService
|
||||||
{
|
.getCurrencies()
|
||||||
label1: Currency.EUR,
|
.filter((currency) => {
|
||||||
label2: Currency.CHF,
|
return currency !== baseCurrency;
|
||||||
value: await this.exchangeRateDataService.toCurrency(
|
})
|
||||||
|
.map((currency) => {
|
||||||
|
return {
|
||||||
|
label1: baseCurrency,
|
||||||
|
label2: currency,
|
||||||
|
value: this.exchangeRateDataService.toCurrency(
|
||||||
1,
|
1,
|
||||||
Currency.EUR,
|
baseCurrency,
|
||||||
Currency.CHF
|
currency
|
||||||
)
|
)
|
||||||
},
|
};
|
||||||
{
|
}),
|
||||||
label1: Currency.GBP,
|
|
||||||
label2: Currency.CHF,
|
|
||||||
value: await this.exchangeRateDataService.toCurrency(
|
|
||||||
1,
|
|
||||||
Currency.GBP,
|
|
||||||
Currency.CHF
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label1: Currency.USD,
|
|
||||||
label2: Currency.CHF,
|
|
||||||
value: await this.exchangeRateDataService.toCurrency(
|
|
||||||
1,
|
|
||||||
Currency.USD,
|
|
||||||
Currency.CHF
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label1: Currency.USD,
|
|
||||||
label2: Currency.EUR,
|
|
||||||
value: await this.exchangeRateDataService.toCurrency(
|
|
||||||
1,
|
|
||||||
Currency.USD,
|
|
||||||
Currency.EUR
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label1: Currency.USD,
|
|
||||||
label2: Currency.GBP,
|
|
||||||
value: await this.exchangeRateDataService.toCurrency(
|
|
||||||
1,
|
|
||||||
Currency.USD,
|
|
||||||
Currency.GBP
|
|
||||||
)
|
|
||||||
}
|
|
||||||
],
|
|
||||||
lastDataGathering: await this.getLastDataGathering(),
|
lastDataGathering: await this.getLastDataGathering(),
|
||||||
transactionCount: await this.prismaService.order.count(),
|
transactionCount: await this.prismaService.order.count(),
|
||||||
userCount: await this.prismaService.user.count(),
|
userCount: await this.prismaService.user.count(),
|
||||||
|
22
apps/api/src/app/cache/cache.module.ts
vendored
22
apps/api/src/app/cache/cache.module.ts
vendored
@ -1,30 +1,28 @@
|
|||||||
import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
|
import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
|
||||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
|
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
|
||||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||||
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module';
|
||||||
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
|
||||||
import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
|
|
||||||
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
|
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { CacheController } from './cache.controller';
|
import { CacheController } from './cache.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [RedisCacheModule],
|
imports: [
|
||||||
|
DataGatheringModule,
|
||||||
|
DataProviderModule,
|
||||||
|
ExchangeRateDataModule,
|
||||||
|
RedisCacheModule
|
||||||
|
],
|
||||||
controllers: [CacheController],
|
controllers: [CacheController],
|
||||||
providers: [
|
providers: [
|
||||||
AlphaVantageService,
|
|
||||||
CacheService,
|
CacheService,
|
||||||
ConfigurationService,
|
ConfigurationService,
|
||||||
DataGatheringService,
|
DataGatheringService,
|
||||||
DataProviderService,
|
PrismaService
|
||||||
GhostfolioScraperApiService,
|
|
||||||
PrismaService,
|
|
||||||
RakutenRapidApiService,
|
|
||||||
YahooFinanceService
|
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CacheModule {}
|
export class CacheModule {}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { Currency, Type } from '@prisma/client';
|
import { Type } from '@prisma/client';
|
||||||
import { IsISO8601, IsNumber, IsString, ValidateIf } from 'class-validator';
|
import { IsISO8601, IsNumber, IsString } from 'class-validator';
|
||||||
|
|
||||||
export class CreateOrderDto {
|
export class CreateOrderDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
currency: Currency;
|
currency: string;
|
||||||
|
|
||||||
@IsISO8601()
|
@IsISO8601()
|
||||||
date: string;
|
date: string;
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import { Currency } from '@prisma/client';
|
|
||||||
|
|
||||||
export interface Data {
|
export interface Data {
|
||||||
currency: Currency;
|
currency: string;
|
||||||
value: number;
|
value: number;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
|
|
||||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
|
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
|
||||||
@ -18,6 +17,6 @@ import { ExportService } from './export.service';
|
|||||||
RedisCacheModule
|
RedisCacheModule
|
||||||
],
|
],
|
||||||
controllers: [ExportController],
|
controllers: [ExportController],
|
||||||
providers: [CacheService, ExportService]
|
providers: [ExportService]
|
||||||
})
|
})
|
||||||
export class ExportModule {}
|
export class ExportModule {}
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
|
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
||||||
import { Order } from '@prisma/client';
|
import { Order } from '@prisma/client';
|
||||||
import { IsArray } from 'class-validator';
|
import { Type } from 'class-transformer';
|
||||||
|
import { IsArray, ValidateNested } from 'class-validator';
|
||||||
|
|
||||||
export class ImportDataDto {
|
export class ImportDataDto {
|
||||||
@IsArray()
|
@IsArray()
|
||||||
orders: Partial<Order>[];
|
@Type(() => CreateOrderDto)
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
orders: Order[];
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,10 @@ export class ImportController {
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
getReasonPhrase(StatusCodes.BAD_REQUEST),
|
{
|
||||||
|
error: getReasonPhrase(StatusCodes.BAD_REQUEST),
|
||||||
|
message: [error.message]
|
||||||
|
},
|
||||||
StatusCodes.BAD_REQUEST
|
StatusCodes.BAD_REQUEST
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,17 @@
|
|||||||
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
||||||
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Order } from '@prisma/client';
|
import { Order } from '@prisma/client';
|
||||||
import { parseISO } from 'date-fns';
|
import { isSameDay, parseISO } from 'date-fns';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ImportService {
|
export class ImportService {
|
||||||
public constructor(private readonly orderService: OrderService) {}
|
private static MAX_ORDERS_TO_IMPORT = 20;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private readonly dataProviderService: DataProviderService,
|
||||||
|
private readonly orderService: OrderService
|
||||||
|
) {}
|
||||||
|
|
||||||
public async import({
|
public async import({
|
||||||
orders,
|
orders,
|
||||||
@ -14,7 +20,10 @@ export class ImportService {
|
|||||||
orders: Partial<Order>[];
|
orders: Partial<Order>[];
|
||||||
userId: string;
|
userId: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
|
await this.validateOrders({ orders, userId });
|
||||||
|
|
||||||
for (const {
|
for (const {
|
||||||
|
accountId,
|
||||||
currency,
|
currency,
|
||||||
dataSource,
|
dataSource,
|
||||||
date,
|
date,
|
||||||
@ -25,6 +34,11 @@ export class ImportService {
|
|||||||
unitPrice
|
unitPrice
|
||||||
} of orders) {
|
} of orders) {
|
||||||
await this.orderService.createOrder({
|
await this.orderService.createOrder({
|
||||||
|
Account: {
|
||||||
|
connect: {
|
||||||
|
id_userId: { userId, id: accountId }
|
||||||
|
}
|
||||||
|
},
|
||||||
currency,
|
currency,
|
||||||
dataSource,
|
dataSource,
|
||||||
fee,
|
fee,
|
||||||
@ -37,4 +51,53 @@ export class ImportService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async validateOrders({
|
||||||
|
orders,
|
||||||
|
userId
|
||||||
|
}: {
|
||||||
|
orders: Partial<Order>[];
|
||||||
|
userId: string;
|
||||||
|
}) {
|
||||||
|
if (orders?.length > ImportService.MAX_ORDERS_TO_IMPORT) {
|
||||||
|
throw new Error('Too many transactions');
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingOrders = await this.orderService.orders({
|
||||||
|
orderBy: { date: 'desc' },
|
||||||
|
where: { userId }
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const [
|
||||||
|
index,
|
||||||
|
{ currency, dataSource, date, fee, quantity, symbol, type, unitPrice }
|
||||||
|
] of orders.entries()) {
|
||||||
|
const duplicateOrder = existingOrders.find((order) => {
|
||||||
|
return (
|
||||||
|
order.currency === currency &&
|
||||||
|
order.dataSource === dataSource &&
|
||||||
|
isSameDay(order.date, parseISO(<string>(<unknown>date))) &&
|
||||||
|
order.fee === fee &&
|
||||||
|
order.quantity === quantity &&
|
||||||
|
order.symbol === symbol &&
|
||||||
|
order.type === type &&
|
||||||
|
order.unitPrice === unitPrice
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (duplicateOrder) {
|
||||||
|
throw new Error(`orders.${index} is a duplicate transaction`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.dataProviderService.get([
|
||||||
|
{ dataSource, symbol }
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (result[symbol] === undefined) {
|
||||||
|
throw new Error(
|
||||||
|
`orders.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
|
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
|
||||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||||
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module';
|
||||||
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
|
||||||
import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
|
|
||||||
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
|
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
@ -14,6 +12,9 @@ import { InfoService } from './info.service';
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
DataGatheringModule,
|
||||||
|
DataProviderModule,
|
||||||
|
ExchangeRateDataModule,
|
||||||
JwtModule.register({
|
JwtModule.register({
|
||||||
secret: process.env.JWT_SECRET_KEY,
|
secret: process.env.JWT_SECRET_KEY,
|
||||||
signOptions: { expiresIn: '30 days' }
|
signOptions: { expiresIn: '30 days' }
|
||||||
@ -21,15 +22,10 @@ import { InfoService } from './info.service';
|
|||||||
],
|
],
|
||||||
controllers: [InfoController],
|
controllers: [InfoController],
|
||||||
providers: [
|
providers: [
|
||||||
AlphaVantageService,
|
|
||||||
ConfigurationService,
|
ConfigurationService,
|
||||||
DataGatheringService,
|
DataGatheringService,
|
||||||
DataProviderService,
|
|
||||||
GhostfolioScraperApiService,
|
|
||||||
InfoService,
|
InfoService,
|
||||||
PrismaService,
|
PrismaService
|
||||||
RakutenRapidApiService,
|
|
||||||
YahooFinanceService
|
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class InfoModule {}
|
export class InfoModule {}
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||||
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||||
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { InfoItem } from '@ghostfolio/common/interfaces';
|
import { InfoItem } from '@ghostfolio/common/interfaces';
|
||||||
import { Subscription } from '@ghostfolio/common/interfaces/subscription.interface';
|
import { Subscription } from '@ghostfolio/common/interfaces/subscription.interface';
|
||||||
import { permissions } from '@ghostfolio/common/permissions';
|
import { permissions } from '@ghostfolio/common/permissions';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import { Currency } from '@prisma/client';
|
|
||||||
import * as bent from 'bent';
|
import * as bent from 'bent';
|
||||||
import { subDays } from 'date-fns';
|
import { subDays } from 'date-fns';
|
||||||
|
|
||||||
@ -16,6 +17,8 @@ export class InfoService {
|
|||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly configurationService: ConfigurationService,
|
private readonly configurationService: ConfigurationService,
|
||||||
|
private readonly dataProviderService: DataProviderService,
|
||||||
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||||
private readonly dataGatheringService: DataGatheringService,
|
private readonly dataGatheringService: DataGatheringService,
|
||||||
private readonly jwtService: JwtService,
|
private readonly jwtService: JwtService,
|
||||||
private readonly prismaService: PrismaService
|
private readonly prismaService: PrismaService
|
||||||
@ -56,9 +59,10 @@ export class InfoService {
|
|||||||
...info,
|
...info,
|
||||||
globalPermissions,
|
globalPermissions,
|
||||||
platforms,
|
platforms,
|
||||||
currencies: Object.values(Currency),
|
currencies: this.exchangeRateDataService.getCurrencies(),
|
||||||
demoAuthToken: this.getDemoAuthToken(),
|
demoAuthToken: this.getDemoAuthToken(),
|
||||||
lastDataGathering: await this.getLastDataGathering(),
|
lastDataGathering: await this.getLastDataGathering(),
|
||||||
|
primaryDataSource: this.dataProviderService.getPrimaryDataSource(),
|
||||||
statistics: await this.getStatistics(),
|
statistics: await this.getStatistics(),
|
||||||
subscriptions: await this.getSubscriptions()
|
subscriptions: await this.getSubscriptions()
|
||||||
};
|
};
|
||||||
@ -132,6 +136,28 @@ export class InfoService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async countNewUsers(aDays: number) {
|
||||||
|
return await this.prismaService.user.count({
|
||||||
|
orderBy: {
|
||||||
|
createdAt: 'desc'
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
AND: [
|
||||||
|
{
|
||||||
|
NOT: {
|
||||||
|
Analytics: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
createdAt: {
|
||||||
|
gt: subDays(new Date(), aDays)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private getDemoAuthToken() {
|
private getDemoAuthToken() {
|
||||||
return this.jwtService.sign({
|
return this.jwtService.sign({
|
||||||
id: InfoService.DEMO_USER_ID
|
id: InfoService.DEMO_USER_ID
|
||||||
@ -151,15 +177,19 @@ export class InfoService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const activeUsers1d = await this.countActiveUsers(1);
|
const activeUsers1d = await this.countActiveUsers(1);
|
||||||
|
const activeUsers7d = await this.countActiveUsers(7);
|
||||||
const activeUsers30d = await this.countActiveUsers(30);
|
const activeUsers30d = await this.countActiveUsers(30);
|
||||||
|
const newUsers30d = await this.countNewUsers(30);
|
||||||
const gitHubContributors = await this.countGitHubContributors();
|
const gitHubContributors = await this.countGitHubContributors();
|
||||||
const gitHubStargazers = await this.countGitHubStargazers();
|
const gitHubStargazers = await this.countGitHubStargazers();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
activeUsers1d,
|
activeUsers1d,
|
||||||
|
activeUsers7d,
|
||||||
activeUsers30d,
|
activeUsers30d,
|
||||||
gitHubContributors,
|
gitHubContributors,
|
||||||
gitHubStargazers
|
gitHubStargazers,
|
||||||
|
newUsers30d
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import { Currency, DataSource, Type } from '@prisma/client';
|
import { DataSource, Type } from '@prisma/client';
|
||||||
import { IsISO8601, IsNumber, IsString } from 'class-validator';
|
import { IsEnum, IsISO8601, IsNumber, IsString } from 'class-validator';
|
||||||
|
|
||||||
export class CreateOrderDto {
|
export class CreateOrderDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
accountId: string;
|
accountId: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
currency: Currency;
|
currency: string;
|
||||||
|
|
||||||
@IsString()
|
@IsEnum(DataSource, { each: true })
|
||||||
dataSource: DataSource;
|
dataSource: DataSource;
|
||||||
|
|
||||||
@IsISO8601()
|
@IsISO8601()
|
||||||
@ -23,7 +23,7 @@ export class CreateOrderDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
symbol: string;
|
symbol: string;
|
||||||
|
|
||||||
@IsString()
|
@IsEnum(Type, { each: true })
|
||||||
type: Type;
|
type: Type;
|
||||||
|
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { Currency, DataSource, Type } from '@prisma/client';
|
import { DataSource, Type } from '@prisma/client';
|
||||||
import { IsISO8601, IsNumber, IsString, ValidateIf } from 'class-validator';
|
import { IsISO8601, IsNumber, IsString } from 'class-validator';
|
||||||
|
|
||||||
export class UpdateOrderDto {
|
export class UpdateOrderDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
accountId: string;
|
accountId: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
currency: Currency;
|
currency: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
dataSource: DataSource;
|
dataSource: DataSource;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { Currency, DataSource, MarketData } from '@prisma/client';
|
import { DataSource, MarketData } from '@prisma/client';
|
||||||
|
|
||||||
import { CurrentRateService } from './current-rate.service';
|
import { CurrentRateService } from './current-rate.service';
|
||||||
import { MarketDataService } from './market-data.service';
|
import { MarketDataService } from './market-data.service';
|
||||||
@ -72,15 +72,8 @@ describe('CurrentRateService', () => {
|
|||||||
let marketDataService: MarketDataService;
|
let marketDataService: MarketDataService;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
dataProviderService = new DataProviderService(
|
dataProviderService = new DataProviderService(null, [], null);
|
||||||
null,
|
exchangeRateDataService = new ExchangeRateDataService(null, null);
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
exchangeRateDataService = new ExchangeRateDataService(null);
|
|
||||||
marketDataService = new MarketDataService(null);
|
marketDataService = new MarketDataService(null);
|
||||||
|
|
||||||
await exchangeRateDataService.initialize();
|
await exchangeRateDataService.initialize();
|
||||||
@ -95,10 +88,10 @@ describe('CurrentRateService', () => {
|
|||||||
it('getValue', async () => {
|
it('getValue', async () => {
|
||||||
expect(
|
expect(
|
||||||
await currentRateService.getValue({
|
await currentRateService.getValue({
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
date: new Date(Date.UTC(2020, 0, 1, 0, 0, 0)),
|
date: new Date(Date.UTC(2020, 0, 1, 0, 0, 0)),
|
||||||
symbol: 'AMZN',
|
symbol: 'AMZN',
|
||||||
userCurrency: Currency.CHF
|
userCurrency: 'CHF'
|
||||||
})
|
})
|
||||||
).toMatchObject({
|
).toMatchObject({
|
||||||
marketPrice: 1847.839966
|
marketPrice: 1847.839966
|
||||||
@ -108,13 +101,13 @@ describe('CurrentRateService', () => {
|
|||||||
it('getValues', async () => {
|
it('getValues', async () => {
|
||||||
expect(
|
expect(
|
||||||
await currentRateService.getValues({
|
await currentRateService.getValues({
|
||||||
currencies: { AMZN: Currency.USD },
|
currencies: { AMZN: 'USD' },
|
||||||
dataGatheringItems: [{ dataSource: DataSource.YAHOO, symbol: 'AMZN' }],
|
dataGatheringItems: [{ dataSource: DataSource.YAHOO, symbol: 'AMZN' }],
|
||||||
dateQuery: {
|
dateQuery: {
|
||||||
lt: new Date(Date.UTC(2020, 0, 2, 0, 0, 0)),
|
lt: new Date(Date.UTC(2020, 0, 2, 0, 0, 0)),
|
||||||
gte: new Date(Date.UTC(2020, 0, 1, 0, 0, 0))
|
gte: new Date(Date.UTC(2020, 0, 1, 0, 0, 0))
|
||||||
},
|
},
|
||||||
userCurrency: Currency.CHF
|
userCurrency: 'CHF'
|
||||||
})
|
})
|
||||||
).toMatchObject([
|
).toMatchObject([
|
||||||
{
|
{
|
||||||
|
@ -2,7 +2,6 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data
|
|||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { resetHours } from '@ghostfolio/common/helper';
|
import { resetHours } from '@ghostfolio/common/helper';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { DataSource } from '@prisma/client';
|
|
||||||
import { isBefore, isToday } from 'date-fns';
|
import { isBefore, isToday } from 'date-fns';
|
||||||
import { flatten } from 'lodash';
|
import { flatten } from 'lodash';
|
||||||
|
|
||||||
@ -27,12 +26,15 @@ export class CurrentRateService {
|
|||||||
}: GetValueParams): Promise<GetValueObject> {
|
}: GetValueParams): Promise<GetValueObject> {
|
||||||
if (isToday(date)) {
|
if (isToday(date)) {
|
||||||
const dataProviderResult = await this.dataProviderService.get([
|
const dataProviderResult = await this.dataProviderService.get([
|
||||||
{ symbol, dataSource: DataSource.YAHOO }
|
{
|
||||||
|
symbol,
|
||||||
|
dataSource: this.dataProviderService.getPrimaryDataSource()
|
||||||
|
}
|
||||||
]);
|
]);
|
||||||
return {
|
return {
|
||||||
|
symbol,
|
||||||
date: resetHours(date),
|
date: resetHours(date),
|
||||||
marketPrice: dataProviderResult?.[symbol]?.marketPrice ?? 0,
|
marketPrice: dataProviderResult?.[symbol]?.marketPrice ?? 0
|
||||||
symbol: symbol
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import { Currency } from '@prisma/client';
|
|
||||||
|
|
||||||
export interface GetValueParams {
|
export interface GetValueParams {
|
||||||
currency: Currency;
|
currency: string;
|
||||||
date: Date;
|
date: Date;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
userCurrency: Currency;
|
userCurrency: string;
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { Currency } from '@prisma/client';
|
|
||||||
|
|
||||||
import { DateQuery } from './date-query.interface';
|
import { DateQuery } from './date-query.interface';
|
||||||
|
|
||||||
export interface GetValuesParams {
|
export interface GetValuesParams {
|
||||||
currencies: { [symbol: string]: Currency };
|
currencies: { [symbol: string]: string };
|
||||||
dataGatheringItems: IDataGatheringItem[];
|
dataGatheringItems: IDataGatheringItem[];
|
||||||
dateQuery: DateQuery;
|
dateQuery: DateQuery;
|
||||||
userCurrency: Currency;
|
userCurrency: string;
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { OrderType } from '@ghostfolio/api/models/order-type';
|
import { OrderType } from '@ghostfolio/api/models/order-type';
|
||||||
import { Currency, DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
|
|
||||||
export interface PortfolioOrder {
|
export interface PortfolioOrder {
|
||||||
currency: Currency;
|
currency: string;
|
||||||
date: string;
|
date: string;
|
||||||
dataSource: DataSource;
|
dataSource: DataSource;
|
||||||
fee: Big;
|
fee: Big;
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import { Currency } from '@prisma/client';
|
|
||||||
|
|
||||||
export interface PortfolioPositionDetail {
|
export interface PortfolioPositionDetail {
|
||||||
averagePrice: number;
|
averagePrice: number;
|
||||||
currency: Currency;
|
currency: string;
|
||||||
firstBuyDate: string;
|
firstBuyDate: string;
|
||||||
grossPerformance: number;
|
grossPerformance: number;
|
||||||
grossPerformancePercent: number;
|
grossPerformancePercent: number;
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { Currency, DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
|
|
||||||
export interface TransactionPointSymbol {
|
export interface TransactionPointSymbol {
|
||||||
currency: Currency;
|
currency: string;
|
||||||
dataSource: DataSource;
|
dataSource: DataSource;
|
||||||
fee: Big;
|
fee: Big;
|
||||||
firstBuyDate: string;
|
firstBuyDate: string;
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
|
|
||||||
import { OrderType } from '@ghostfolio/api/models/order-type';
|
import { OrderType } from '@ghostfolio/api/models/order-type';
|
||||||
import { parseDate, resetHours } from '@ghostfolio/common/helper';
|
import { parseDate, resetHours } from '@ghostfolio/common/helper';
|
||||||
import { Currency, DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import {
|
import {
|
||||||
addDays,
|
addDays,
|
||||||
@ -134,7 +133,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
it('with orders of only one symbol', () => {
|
it('with orders of only one symbol', () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
currentRateService,
|
currentRateService,
|
||||||
Currency.USD
|
'USD'
|
||||||
);
|
);
|
||||||
portfolioCalculator.computeTransactionPoints(ordersVTI);
|
portfolioCalculator.computeTransactionPoints(ordersVTI);
|
||||||
const portfolioItemsAtTransactionPoints =
|
const portfolioItemsAtTransactionPoints =
|
||||||
@ -148,7 +147,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
it('with orders of only one symbol and a fee', () => {
|
it('with orders of only one symbol and a fee', () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
currentRateService,
|
currentRateService,
|
||||||
Currency.USD
|
'USD'
|
||||||
);
|
);
|
||||||
const orders: PortfolioOrder[] = [
|
const orders: PortfolioOrder[] = [
|
||||||
{
|
{
|
||||||
@ -158,7 +157,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
type: OrderType.Buy,
|
type: OrderType.Buy,
|
||||||
unitPrice: new Big('144.38'),
|
unitPrice: new Big('144.38'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
fee: new Big('5')
|
fee: new Big('5')
|
||||||
},
|
},
|
||||||
@ -169,7 +168,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
type: OrderType.Buy,
|
type: OrderType.Buy,
|
||||||
unitPrice: new Big('147.99'),
|
unitPrice: new Big('147.99'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
fee: new Big('10')
|
fee: new Big('10')
|
||||||
},
|
},
|
||||||
@ -180,7 +179,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
type: OrderType.Sell,
|
type: OrderType.Sell,
|
||||||
unitPrice: new Big('151.41'),
|
unitPrice: new Big('151.41'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
fee: new Big('5')
|
fee: new Big('5')
|
||||||
}
|
}
|
||||||
@ -198,7 +197,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('10'),
|
quantity: new Big('10'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('1443.8'),
|
investment: new Big('1443.8'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
transactionCount: 1,
|
transactionCount: 1,
|
||||||
fee: new Big('5')
|
fee: new Big('5')
|
||||||
@ -213,7 +212,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('20'),
|
quantity: new Big('20'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('2923.7'),
|
investment: new Big('2923.7'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
transactionCount: 2,
|
transactionCount: 2,
|
||||||
fee: new Big('15')
|
fee: new Big('15')
|
||||||
@ -228,7 +227,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('652.55'),
|
investment: new Big('652.55'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
transactionCount: 3,
|
transactionCount: 3,
|
||||||
fee: new Big('20')
|
fee: new Big('20')
|
||||||
@ -241,7 +240,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
it('with orders of two different symbols and a fee', () => {
|
it('with orders of two different symbols and a fee', () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
currentRateService,
|
currentRateService,
|
||||||
Currency.USD
|
'USD'
|
||||||
);
|
);
|
||||||
const orders: PortfolioOrder[] = [
|
const orders: PortfolioOrder[] = [
|
||||||
{
|
{
|
||||||
@ -251,7 +250,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
type: OrderType.Buy,
|
type: OrderType.Buy,
|
||||||
unitPrice: new Big('144.38'),
|
unitPrice: new Big('144.38'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
fee: new Big('5')
|
fee: new Big('5')
|
||||||
},
|
},
|
||||||
@ -262,7 +261,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
symbol: 'VTX',
|
symbol: 'VTX',
|
||||||
type: OrderType.Buy,
|
type: OrderType.Buy,
|
||||||
unitPrice: new Big('147.99'),
|
unitPrice: new Big('147.99'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
fee: new Big('10')
|
fee: new Big('10')
|
||||||
},
|
},
|
||||||
@ -273,7 +272,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
type: OrderType.Sell,
|
type: OrderType.Sell,
|
||||||
unitPrice: new Big('151.41'),
|
unitPrice: new Big('151.41'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
fee: new Big('5')
|
fee: new Big('5')
|
||||||
}
|
}
|
||||||
@ -291,7 +290,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('10'),
|
quantity: new Big('10'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('1443.8'),
|
investment: new Big('1443.8'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
transactionCount: 1,
|
transactionCount: 1,
|
||||||
fee: new Big('5')
|
fee: new Big('5')
|
||||||
@ -306,7 +305,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('10'),
|
quantity: new Big('10'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('1443.8'),
|
investment: new Big('1443.8'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
transactionCount: 1,
|
transactionCount: 1,
|
||||||
fee: new Big('5')
|
fee: new Big('5')
|
||||||
@ -316,7 +315,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('10'),
|
quantity: new Big('10'),
|
||||||
symbol: 'VTX',
|
symbol: 'VTX',
|
||||||
investment: new Big('1479.9'),
|
investment: new Big('1479.9'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
firstBuyDate: '2019-08-03',
|
firstBuyDate: '2019-08-03',
|
||||||
transactionCount: 1,
|
transactionCount: 1,
|
||||||
fee: new Big('10')
|
fee: new Big('10')
|
||||||
@ -331,7 +330,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('686.75'),
|
investment: new Big('686.75'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
transactionCount: 2,
|
transactionCount: 2,
|
||||||
fee: new Big('10')
|
fee: new Big('10')
|
||||||
@ -341,7 +340,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('10'),
|
quantity: new Big('10'),
|
||||||
symbol: 'VTX',
|
symbol: 'VTX',
|
||||||
investment: new Big('1479.9'),
|
investment: new Big('1479.9'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
firstBuyDate: '2019-08-03',
|
firstBuyDate: '2019-08-03',
|
||||||
transactionCount: 1,
|
transactionCount: 1,
|
||||||
fee: new Big('10')
|
fee: new Big('10')
|
||||||
@ -355,7 +354,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
const orders: PortfolioOrder[] = [
|
const orders: PortfolioOrder[] = [
|
||||||
...ordersVTI,
|
...ordersVTI,
|
||||||
{
|
{
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
date: '2021-02-01',
|
date: '2021-02-01',
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
||||||
@ -368,7 +367,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
];
|
];
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
currentRateService,
|
currentRateService,
|
||||||
Currency.USD
|
'USD'
|
||||||
);
|
);
|
||||||
portfolioCalculator.computeTransactionPoints(orders);
|
portfolioCalculator.computeTransactionPoints(orders);
|
||||||
const portfolioItemsAtTransactionPoints =
|
const portfolioItemsAtTransactionPoints =
|
||||||
@ -379,7 +378,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
date: '2019-02-01',
|
date: '2019-02-01',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
investment: new Big('1443.8'),
|
investment: new Big('1443.8'),
|
||||||
@ -394,7 +393,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
date: '2019-08-03',
|
date: '2019-08-03',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
investment: new Big('2923.7'),
|
investment: new Big('2923.7'),
|
||||||
@ -409,7 +408,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
date: '2020-02-02',
|
date: '2020-02-02',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
investment: new Big('652.55'),
|
investment: new Big('652.55'),
|
||||||
@ -424,7 +423,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
date: '2021-02-01',
|
date: '2021-02-01',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
investment: new Big('6627.05'),
|
investment: new Big('6627.05'),
|
||||||
@ -439,7 +438,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
date: '2021-08-01',
|
date: '2021-08-01',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
investment: new Big('8403.95'),
|
investment: new Big('8403.95'),
|
||||||
@ -457,7 +456,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
const orders: PortfolioOrder[] = [
|
const orders: PortfolioOrder[] = [
|
||||||
...ordersVTI,
|
...ordersVTI,
|
||||||
{
|
{
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
date: '2019-09-01',
|
date: '2019-09-01',
|
||||||
name: 'Amazon.com, Inc.',
|
name: 'Amazon.com, Inc.',
|
||||||
@ -470,7 +469,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
];
|
];
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
currentRateService,
|
currentRateService,
|
||||||
Currency.USD
|
'USD'
|
||||||
);
|
);
|
||||||
portfolioCalculator.computeTransactionPoints(orders);
|
portfolioCalculator.computeTransactionPoints(orders);
|
||||||
const portfolioItemsAtTransactionPoints =
|
const portfolioItemsAtTransactionPoints =
|
||||||
@ -485,7 +484,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('10'),
|
quantity: new Big('10'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('1443.8'),
|
investment: new Big('1443.8'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
@ -500,7 +499,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('20'),
|
quantity: new Big('20'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('2923.7'),
|
investment: new Big('2923.7'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 2
|
transactionCount: 2
|
||||||
@ -515,7 +514,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
symbol: 'AMZN',
|
symbol: 'AMZN',
|
||||||
investment: new Big('10109.95'),
|
investment: new Big('10109.95'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
firstBuyDate: '2019-09-01',
|
firstBuyDate: '2019-09-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
@ -525,7 +524,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('20'),
|
quantity: new Big('20'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('2923.7'),
|
investment: new Big('2923.7'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 2
|
transactionCount: 2
|
||||||
@ -540,7 +539,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
symbol: 'AMZN',
|
symbol: 'AMZN',
|
||||||
investment: new Big('10109.95'),
|
investment: new Big('10109.95'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
firstBuyDate: '2019-09-01',
|
firstBuyDate: '2019-09-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
@ -550,7 +549,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('652.55'),
|
investment: new Big('652.55'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 3
|
transactionCount: 3
|
||||||
@ -565,7 +564,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
symbol: 'AMZN',
|
symbol: 'AMZN',
|
||||||
investment: new Big('10109.95'),
|
investment: new Big('10109.95'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
firstBuyDate: '2019-09-01',
|
firstBuyDate: '2019-09-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
@ -575,7 +574,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('15'),
|
quantity: new Big('15'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('2684.05'),
|
investment: new Big('2684.05'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 4
|
transactionCount: 4
|
||||||
@ -590,7 +589,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
symbol: 'AMZN',
|
symbol: 'AMZN',
|
||||||
investment: new Big('10109.95'),
|
investment: new Big('10109.95'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
firstBuyDate: '2019-09-01',
|
firstBuyDate: '2019-09-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
@ -600,7 +599,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('25'),
|
quantity: new Big('25'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('4460.95'),
|
investment: new Big('4460.95'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 5
|
transactionCount: 5
|
||||||
@ -620,7 +619,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
symbol: 'AMZN',
|
symbol: 'AMZN',
|
||||||
type: OrderType.Buy,
|
type: OrderType.Buy,
|
||||||
unitPrice: new Big('2021.99'),
|
unitPrice: new Big('2021.99'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
fee: new Big(0)
|
fee: new Big(0)
|
||||||
},
|
},
|
||||||
@ -631,14 +630,14 @@ describe('PortfolioCalculator', () => {
|
|||||||
symbol: 'AMZN',
|
symbol: 'AMZN',
|
||||||
type: OrderType.Sell,
|
type: OrderType.Sell,
|
||||||
unitPrice: new Big('2412.23'),
|
unitPrice: new Big('2412.23'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
fee: new Big(0)
|
fee: new Big(0)
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
currentRateService,
|
currentRateService,
|
||||||
Currency.USD
|
'USD'
|
||||||
);
|
);
|
||||||
portfolioCalculator.computeTransactionPoints(orders);
|
portfolioCalculator.computeTransactionPoints(orders);
|
||||||
const portfolioItemsAtTransactionPoints =
|
const portfolioItemsAtTransactionPoints =
|
||||||
@ -652,7 +651,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
it('with mixed symbols', () => {
|
it('with mixed symbols', () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
currentRateService,
|
currentRateService,
|
||||||
Currency.USD
|
'USD'
|
||||||
);
|
);
|
||||||
portfolioCalculator.computeTransactionPoints(ordersMixedSymbols);
|
portfolioCalculator.computeTransactionPoints(ordersMixedSymbols);
|
||||||
const portfolioItemsAtTransactionPoints =
|
const portfolioItemsAtTransactionPoints =
|
||||||
@ -667,7 +666,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('50'),
|
quantity: new Big('50'),
|
||||||
symbol: 'TSLA',
|
symbol: 'TSLA',
|
||||||
investment: new Big('2148.5'),
|
investment: new Big('2148.5'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
firstBuyDate: '2017-01-03',
|
firstBuyDate: '2017-01-03',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
@ -682,7 +681,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('0.5614682'),
|
quantity: new Big('0.5614682'),
|
||||||
symbol: 'BTCUSD',
|
symbol: 'BTCUSD',
|
||||||
investment: new Big('1999.9999999999998659756'),
|
investment: new Big('1999.9999999999998659756'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
firstBuyDate: '2017-07-01',
|
firstBuyDate: '2017-07-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
@ -692,7 +691,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('50'),
|
quantity: new Big('50'),
|
||||||
symbol: 'TSLA',
|
symbol: 'TSLA',
|
||||||
investment: new Big('2148.5'),
|
investment: new Big('2148.5'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
firstBuyDate: '2017-01-03',
|
firstBuyDate: '2017-01-03',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
@ -707,7 +706,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
symbol: 'AMZN',
|
symbol: 'AMZN',
|
||||||
investment: new Big('10109.95'),
|
investment: new Big('10109.95'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
firstBuyDate: '2018-09-01',
|
firstBuyDate: '2018-09-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
@ -717,7 +716,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('0.5614682'),
|
quantity: new Big('0.5614682'),
|
||||||
symbol: 'BTCUSD',
|
symbol: 'BTCUSD',
|
||||||
investment: new Big('1999.9999999999998659756'),
|
investment: new Big('1999.9999999999998659756'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
firstBuyDate: '2017-07-01',
|
firstBuyDate: '2017-07-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
@ -727,7 +726,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('50'),
|
quantity: new Big('50'),
|
||||||
symbol: 'TSLA',
|
symbol: 'TSLA',
|
||||||
investment: new Big('2148.5'),
|
investment: new Big('2148.5'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
firstBuyDate: '2017-01-03',
|
firstBuyDate: '2017-01-03',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
@ -742,7 +741,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
it('with single TSLA and early start', async () => {
|
it('with single TSLA and early start', async () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
currentRateService,
|
currentRateService,
|
||||||
Currency.USD
|
'USD'
|
||||||
);
|
);
|
||||||
portfolioCalculator.setTransactionPoints(orderTslaTransactionPoint);
|
portfolioCalculator.setTransactionPoints(orderTslaTransactionPoint);
|
||||||
|
|
||||||
@ -782,7 +781,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
it('with single TSLA and buy day start', async () => {
|
it('with single TSLA and buy day start', async () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
currentRateService,
|
currentRateService,
|
||||||
Currency.USD
|
'USD'
|
||||||
);
|
);
|
||||||
portfolioCalculator.setTransactionPoints(orderTslaTransactionPoint);
|
portfolioCalculator.setTransactionPoints(orderTslaTransactionPoint);
|
||||||
|
|
||||||
@ -822,7 +821,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
it('with single TSLA and late start', async () => {
|
it('with single TSLA and late start', async () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
currentRateService,
|
currentRateService,
|
||||||
Currency.USD
|
'USD'
|
||||||
);
|
);
|
||||||
portfolioCalculator.setTransactionPoints(orderTslaTransactionPoint);
|
portfolioCalculator.setTransactionPoints(orderTslaTransactionPoint);
|
||||||
|
|
||||||
@ -862,7 +861,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
it('with VTI only', async () => {
|
it('with VTI only', async () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
currentRateService,
|
currentRateService,
|
||||||
Currency.USD
|
'USD'
|
||||||
);
|
);
|
||||||
portfolioCalculator.setTransactionPoints(ordersVTITransactionPoints);
|
portfolioCalculator.setTransactionPoints(ordersVTITransactionPoints);
|
||||||
|
|
||||||
@ -905,7 +904,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
it('with buy and sell', async () => {
|
it('with buy and sell', async () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
currentRateService,
|
currentRateService,
|
||||||
Currency.USD
|
'USD'
|
||||||
);
|
);
|
||||||
portfolioCalculator.setTransactionPoints(transactionPointsBuyAndSell);
|
portfolioCalculator.setTransactionPoints(transactionPointsBuyAndSell);
|
||||||
|
|
||||||
@ -959,7 +958,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
it('with buy, sell, buy', async () => {
|
it('with buy, sell, buy', async () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
currentRateService,
|
currentRateService,
|
||||||
Currency.USD
|
'USD'
|
||||||
);
|
);
|
||||||
portfolioCalculator.setTransactionPoints([
|
portfolioCalculator.setTransactionPoints([
|
||||||
{
|
{
|
||||||
@ -969,7 +968,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('805.9'),
|
investment: new Big('805.9'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-09-01',
|
firstBuyDate: '2019-09-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
@ -984,7 +983,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('0'),
|
quantity: new Big('0'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('0'),
|
investment: new Big('0'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-09-01',
|
firstBuyDate: '2019-09-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
@ -999,7 +998,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('1013.9'),
|
investment: new Big('1013.9'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-09-01',
|
firstBuyDate: '2019-09-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
@ -1047,7 +1046,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
it('with performance since Jan 1st, 2020', async () => {
|
it('with performance since Jan 1st, 2020', async () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
currentRateService,
|
currentRateService,
|
||||||
Currency.USD
|
'USD'
|
||||||
);
|
);
|
||||||
const transactionPoints: TransactionPoint[] = [
|
const transactionPoints: TransactionPoint[] = [
|
||||||
{
|
{
|
||||||
@ -1057,7 +1056,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('10'),
|
quantity: new Big('10'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('1443.8'),
|
investment: new Big('1443.8'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
@ -1072,7 +1071,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('20'),
|
quantity: new Big('20'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('2923.7'),
|
investment: new Big('2923.7'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
@ -1130,7 +1129,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
it('with net performance since Jan 1st, 2020 - include fees', async () => {
|
it('with net performance since Jan 1st, 2020 - include fees', async () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
currentRateService,
|
currentRateService,
|
||||||
Currency.USD
|
'USD'
|
||||||
);
|
);
|
||||||
const transactionPoints: TransactionPoint[] = [
|
const transactionPoints: TransactionPoint[] = [
|
||||||
{
|
{
|
||||||
@ -1140,7 +1139,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('10'),
|
quantity: new Big('10'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('1443.8'),
|
investment: new Big('1443.8'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(50),
|
fee: new Big(50),
|
||||||
@ -1155,7 +1154,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('20'),
|
quantity: new Big('20'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('2923.7'),
|
investment: new Big('2923.7'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(100),
|
fee: new Big(100),
|
||||||
@ -1223,7 +1222,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
it('with net performance since Feb 1st, 2019 - include fees', async () => {
|
it('with net performance since Feb 1st, 2019 - include fees', async () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
currentRateService,
|
currentRateService,
|
||||||
Currency.USD
|
'USD'
|
||||||
);
|
);
|
||||||
const transactionPoints: TransactionPoint[] = [
|
const transactionPoints: TransactionPoint[] = [
|
||||||
{
|
{
|
||||||
@ -1233,7 +1232,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('10'),
|
quantity: new Big('10'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('1443.8'),
|
investment: new Big('1443.8'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(50),
|
fee: new Big(50),
|
||||||
@ -1248,7 +1247,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('20'),
|
quantity: new Big('20'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('2923.7'),
|
investment: new Big('2923.7'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(100),
|
fee: new Big(100),
|
||||||
@ -1311,7 +1310,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
it('with TWR example from Investopedia: Scenario 1', async () => {
|
it('with TWR example from Investopedia: Scenario 1', async () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
currentRateService,
|
currentRateService,
|
||||||
Currency.USD
|
'USD'
|
||||||
);
|
);
|
||||||
portfolioCalculator.setTransactionPoints([
|
portfolioCalculator.setTransactionPoints([
|
||||||
{
|
{
|
||||||
@ -1321,7 +1320,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('1000000'), // 1 million
|
quantity: new Big('1000000'), // 1 million
|
||||||
symbol: 'MFA', // Mutual Fund A
|
symbol: 'MFA', // Mutual Fund A
|
||||||
investment: new Big('1000000'), // 1 million
|
investment: new Big('1000000'), // 1 million
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2010-12-31',
|
firstBuyDate: '2010-12-31',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
@ -1336,7 +1335,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('1086022.689344541'), // 1,000,000 + 100,000 / 1.162484
|
quantity: new Big('1086022.689344541'), // 1,000,000 + 100,000 / 1.162484
|
||||||
symbol: 'MFA', // Mutual Fund A
|
symbol: 'MFA', // Mutual Fund A
|
||||||
investment: new Big('1100000'), // 1,000,000 + 100,000
|
investment: new Big('1100000'), // 1,000,000 + 100,000
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2010-12-31',
|
firstBuyDate: '2010-12-31',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
@ -1388,7 +1387,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
it('with example from chsoft.ch: Performance of a Combination of Investments', async () => {
|
it('with example from chsoft.ch: Performance of a Combination of Investments', async () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
currentRateService,
|
currentRateService,
|
||||||
Currency.CHF
|
'CHF'
|
||||||
);
|
);
|
||||||
portfolioCalculator.setTransactionPoints([
|
portfolioCalculator.setTransactionPoints([
|
||||||
{
|
{
|
||||||
@ -1398,7 +1397,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('200'),
|
quantity: new Big('200'),
|
||||||
symbol: 'SPA', // Sub Portfolio A
|
symbol: 'SPA', // Sub Portfolio A
|
||||||
investment: new Big('200'),
|
investment: new Big('200'),
|
||||||
currency: Currency.CHF,
|
currency: 'CHF',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2012-12-31',
|
firstBuyDate: '2012-12-31',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
@ -1408,7 +1407,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('300'),
|
quantity: new Big('300'),
|
||||||
symbol: 'SPB', // Sub Portfolio B
|
symbol: 'SPB', // Sub Portfolio B
|
||||||
investment: new Big('300'),
|
investment: new Big('300'),
|
||||||
currency: Currency.CHF,
|
currency: 'CHF',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2012-12-31',
|
firstBuyDate: '2012-12-31',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
@ -1423,7 +1422,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('200'),
|
quantity: new Big('200'),
|
||||||
symbol: 'SPA', // Sub Portfolio A
|
symbol: 'SPA', // Sub Portfolio A
|
||||||
investment: new Big('200'),
|
investment: new Big('200'),
|
||||||
currency: Currency.CHF,
|
currency: 'CHF',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2012-12-31',
|
firstBuyDate: '2012-12-31',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
@ -1433,7 +1432,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('300'),
|
quantity: new Big('300'),
|
||||||
symbol: 'SPB', // Sub Portfolio B
|
symbol: 'SPB', // Sub Portfolio B
|
||||||
investment: new Big('300'),
|
investment: new Big('300'),
|
||||||
currency: Currency.CHF,
|
currency: 'CHF',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2012-12-31',
|
firstBuyDate: '2012-12-31',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
@ -1494,7 +1493,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
it('with yearly', async () => {
|
it('with yearly', async () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
currentRateService,
|
currentRateService,
|
||||||
Currency.USD
|
'USD'
|
||||||
);
|
);
|
||||||
portfolioCalculator.setTransactionPoints(ordersVTITransactionPoints);
|
portfolioCalculator.setTransactionPoints(ordersVTITransactionPoints);
|
||||||
const timelineSpecification: TimelineSpecification[] = [
|
const timelineSpecification: TimelineSpecification[] = [
|
||||||
@ -1537,7 +1536,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
it('with yearly and fees', async () => {
|
it('with yearly and fees', async () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
currentRateService,
|
currentRateService,
|
||||||
Currency.USD
|
'USD'
|
||||||
);
|
);
|
||||||
const transactionPoints: TransactionPoint[] = [
|
const transactionPoints: TransactionPoint[] = [
|
||||||
{
|
{
|
||||||
@ -1547,7 +1546,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('10'),
|
quantity: new Big('10'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('1443.8'),
|
investment: new Big('1443.8'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(50),
|
fee: new Big(50),
|
||||||
@ -1562,7 +1561,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('20'),
|
quantity: new Big('20'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('2923.7'),
|
investment: new Big('2923.7'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(100),
|
fee: new Big(100),
|
||||||
@ -1577,7 +1576,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('652.55'),
|
investment: new Big('652.55'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(150),
|
fee: new Big(150),
|
||||||
@ -1592,7 +1591,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('15'),
|
quantity: new Big('15'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('2684.05'),
|
investment: new Big('2684.05'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(200),
|
fee: new Big(200),
|
||||||
@ -1607,7 +1606,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('25'),
|
quantity: new Big('25'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('4460.95'),
|
investment: new Big('4460.95'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(250),
|
fee: new Big(250),
|
||||||
@ -1657,7 +1656,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
it('with monthly', async () => {
|
it('with monthly', async () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
currentRateService,
|
currentRateService,
|
||||||
Currency.USD
|
'USD'
|
||||||
);
|
);
|
||||||
portfolioCalculator.setTransactionPoints(ordersVTITransactionPoints);
|
portfolioCalculator.setTransactionPoints(ordersVTITransactionPoints);
|
||||||
const timelineSpecification: TimelineSpecification[] = [
|
const timelineSpecification: TimelineSpecification[] = [
|
||||||
@ -1889,7 +1888,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
it('with yearly and monthly mixed', async () => {
|
it('with yearly and monthly mixed', async () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
currentRateService,
|
currentRateService,
|
||||||
Currency.USD
|
'USD'
|
||||||
);
|
);
|
||||||
portfolioCalculator.setTransactionPoints(ordersVTITransactionPoints);
|
portfolioCalculator.setTransactionPoints(ordersVTITransactionPoints);
|
||||||
const timelineSpecification: TimelineSpecification[] = [
|
const timelineSpecification: TimelineSpecification[] = [
|
||||||
@ -1971,7 +1970,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
it('with all mixed', async () => {
|
it('with all mixed', async () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
currentRateService,
|
currentRateService,
|
||||||
Currency.USD
|
'USD'
|
||||||
);
|
);
|
||||||
portfolioCalculator.setTransactionPoints(ordersVTITransactionPoints);
|
portfolioCalculator.setTransactionPoints(ordersVTITransactionPoints);
|
||||||
const timelineSpecification: TimelineSpecification[] = [
|
const timelineSpecification: TimelineSpecification[] = [
|
||||||
@ -2262,7 +2261,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
it('with mixed portfolio', async () => {
|
it('with mixed portfolio', async () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
currentRateService,
|
currentRateService,
|
||||||
Currency.USD
|
'USD'
|
||||||
);
|
);
|
||||||
portfolioCalculator.setTransactionPoints([
|
portfolioCalculator.setTransactionPoints([
|
||||||
{
|
{
|
||||||
@ -2272,7 +2271,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
symbol: 'AMZN',
|
symbol: 'AMZN',
|
||||||
investment: new Big('10109.95'),
|
investment: new Big('10109.95'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
@ -2282,7 +2281,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
quantity: new Big('10'),
|
quantity: new Big('10'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('1443.8'),
|
investment: new Big('1443.8'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
@ -2325,7 +2324,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
describe('annualized performance percentage', () => {
|
describe('annualized performance percentage', () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
currentRateService,
|
currentRateService,
|
||||||
Currency.USD
|
'USD'
|
||||||
);
|
);
|
||||||
|
|
||||||
it('Get annualized performance', async () => {
|
it('Get annualized performance', async () => {
|
||||||
@ -2391,7 +2390,7 @@ const ordersMixedSymbols: PortfolioOrder[] = [
|
|||||||
symbol: 'TSLA',
|
symbol: 'TSLA',
|
||||||
type: OrderType.Buy,
|
type: OrderType.Buy,
|
||||||
unitPrice: new Big('42.97'),
|
unitPrice: new Big('42.97'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
fee: new Big(0)
|
fee: new Big(0)
|
||||||
},
|
},
|
||||||
@ -2402,7 +2401,7 @@ const ordersMixedSymbols: PortfolioOrder[] = [
|
|||||||
symbol: 'BTCUSD',
|
symbol: 'BTCUSD',
|
||||||
type: OrderType.Buy,
|
type: OrderType.Buy,
|
||||||
unitPrice: new Big('3562.089535970158'),
|
unitPrice: new Big('3562.089535970158'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
fee: new Big(0)
|
fee: new Big(0)
|
||||||
},
|
},
|
||||||
@ -2413,7 +2412,7 @@ const ordersMixedSymbols: PortfolioOrder[] = [
|
|||||||
symbol: 'AMZN',
|
symbol: 'AMZN',
|
||||||
type: OrderType.Buy,
|
type: OrderType.Buy,
|
||||||
unitPrice: new Big('2021.99'),
|
unitPrice: new Big('2021.99'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
fee: new Big(0)
|
fee: new Big(0)
|
||||||
}
|
}
|
||||||
@ -2427,7 +2426,7 @@ const ordersVTI: PortfolioOrder[] = [
|
|||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
type: OrderType.Buy,
|
type: OrderType.Buy,
|
||||||
unitPrice: new Big('144.38'),
|
unitPrice: new Big('144.38'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
fee: new Big(0)
|
fee: new Big(0)
|
||||||
},
|
},
|
||||||
@ -2438,7 +2437,7 @@ const ordersVTI: PortfolioOrder[] = [
|
|||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
type: OrderType.Buy,
|
type: OrderType.Buy,
|
||||||
unitPrice: new Big('147.99'),
|
unitPrice: new Big('147.99'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
fee: new Big(0)
|
fee: new Big(0)
|
||||||
},
|
},
|
||||||
@ -2449,7 +2448,7 @@ const ordersVTI: PortfolioOrder[] = [
|
|||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
type: OrderType.Sell,
|
type: OrderType.Sell,
|
||||||
unitPrice: new Big('151.41'),
|
unitPrice: new Big('151.41'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
fee: new Big(0)
|
fee: new Big(0)
|
||||||
},
|
},
|
||||||
@ -2460,7 +2459,7 @@ const ordersVTI: PortfolioOrder[] = [
|
|||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
type: OrderType.Buy,
|
type: OrderType.Buy,
|
||||||
unitPrice: new Big('177.69'),
|
unitPrice: new Big('177.69'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
fee: new Big(0)
|
fee: new Big(0)
|
||||||
},
|
},
|
||||||
@ -2471,7 +2470,7 @@ const ordersVTI: PortfolioOrder[] = [
|
|||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
type: OrderType.Buy,
|
type: OrderType.Buy,
|
||||||
unitPrice: new Big('203.15'),
|
unitPrice: new Big('203.15'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
fee: new Big(0)
|
fee: new Big(0)
|
||||||
}
|
}
|
||||||
@ -2485,7 +2484,7 @@ const orderTslaTransactionPoint: TransactionPoint[] = [
|
|||||||
quantity: new Big('1'),
|
quantity: new Big('1'),
|
||||||
symbol: 'TSLA',
|
symbol: 'TSLA',
|
||||||
investment: new Big('719.46'),
|
investment: new Big('719.46'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2021-01-01',
|
firstBuyDate: '2021-01-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
@ -2503,7 +2502,7 @@ const ordersVTITransactionPoints: TransactionPoint[] = [
|
|||||||
quantity: new Big('10'),
|
quantity: new Big('10'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('1443.8'),
|
investment: new Big('1443.8'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
@ -2518,7 +2517,7 @@ const ordersVTITransactionPoints: TransactionPoint[] = [
|
|||||||
quantity: new Big('20'),
|
quantity: new Big('20'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('2923.7'),
|
investment: new Big('2923.7'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
@ -2533,7 +2532,7 @@ const ordersVTITransactionPoints: TransactionPoint[] = [
|
|||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('652.55'),
|
investment: new Big('652.55'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
@ -2548,7 +2547,7 @@ const ordersVTITransactionPoints: TransactionPoint[] = [
|
|||||||
quantity: new Big('15'),
|
quantity: new Big('15'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('2684.05'),
|
investment: new Big('2684.05'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
@ -2563,7 +2562,7 @@ const ordersVTITransactionPoints: TransactionPoint[] = [
|
|||||||
quantity: new Big('25'),
|
quantity: new Big('25'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('4460.95'),
|
investment: new Big('4460.95'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
@ -2581,7 +2580,7 @@ const transactionPointsBuyAndSell: TransactionPoint[] = [
|
|||||||
quantity: new Big('10'),
|
quantity: new Big('10'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('1443.8'),
|
investment: new Big('1443.8'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
@ -2596,7 +2595,7 @@ const transactionPointsBuyAndSell: TransactionPoint[] = [
|
|||||||
quantity: new Big('20'),
|
quantity: new Big('20'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('2923.7'),
|
investment: new Big('2923.7'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
@ -2611,7 +2610,7 @@ const transactionPointsBuyAndSell: TransactionPoint[] = [
|
|||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
symbol: 'AMZN',
|
symbol: 'AMZN',
|
||||||
investment: new Big('10109.95'),
|
investment: new Big('10109.95'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-09-01',
|
firstBuyDate: '2019-09-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
@ -2621,7 +2620,7 @@ const transactionPointsBuyAndSell: TransactionPoint[] = [
|
|||||||
quantity: new Big('20'),
|
quantity: new Big('20'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('2923.7'),
|
investment: new Big('2923.7'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
@ -2636,7 +2635,7 @@ const transactionPointsBuyAndSell: TransactionPoint[] = [
|
|||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
symbol: 'AMZN',
|
symbol: 'AMZN',
|
||||||
investment: new Big('10109.95'),
|
investment: new Big('10109.95'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-09-01',
|
firstBuyDate: '2019-09-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
@ -2646,7 +2645,7 @@ const transactionPointsBuyAndSell: TransactionPoint[] = [
|
|||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('652.55'),
|
investment: new Big('652.55'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
@ -2661,7 +2660,7 @@ const transactionPointsBuyAndSell: TransactionPoint[] = [
|
|||||||
quantity: new Big('0'),
|
quantity: new Big('0'),
|
||||||
symbol: 'AMZN',
|
symbol: 'AMZN',
|
||||||
investment: new Big('0'),
|
investment: new Big('0'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-09-01',
|
firstBuyDate: '2019-09-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
@ -2671,7 +2670,7 @@ const transactionPointsBuyAndSell: TransactionPoint[] = [
|
|||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('652.55'),
|
investment: new Big('652.55'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
@ -2686,7 +2685,7 @@ const transactionPointsBuyAndSell: TransactionPoint[] = [
|
|||||||
quantity: new Big('0'),
|
quantity: new Big('0'),
|
||||||
symbol: 'AMZN',
|
symbol: 'AMZN',
|
||||||
investment: new Big('0'),
|
investment: new Big('0'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-09-01',
|
firstBuyDate: '2019-09-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
@ -2696,7 +2695,7 @@ const transactionPointsBuyAndSell: TransactionPoint[] = [
|
|||||||
quantity: new Big('15'),
|
quantity: new Big('15'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('2684.05'),
|
investment: new Big('2684.05'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
@ -2711,7 +2710,7 @@ const transactionPointsBuyAndSell: TransactionPoint[] = [
|
|||||||
quantity: new Big('0'),
|
quantity: new Big('0'),
|
||||||
symbol: 'AMZN',
|
symbol: 'AMZN',
|
||||||
investment: new Big('0'),
|
investment: new Big('0'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-09-01',
|
firstBuyDate: '2019-09-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
@ -2721,7 +2720,7 @@ const transactionPointsBuyAndSell: TransactionPoint[] = [
|
|||||||
quantity: new Big('25'),
|
quantity: new Big('25'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('4460.95'),
|
investment: new Big('4460.95'),
|
||||||
currency: Currency.USD,
|
currency: 'USD',
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
|
@ -2,7 +2,6 @@ import { OrderType } from '@ghostfolio/api/models/order-type';
|
|||||||
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
|
||||||
import { TimelinePosition } from '@ghostfolio/common/interfaces';
|
import { TimelinePosition } from '@ghostfolio/common/interfaces';
|
||||||
import { Currency, DataSource } from '@prisma/client';
|
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import {
|
import {
|
||||||
addDays,
|
addDays,
|
||||||
@ -35,7 +34,7 @@ export class PortfolioCalculator {
|
|||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private currentRateService: CurrentRateService,
|
private currentRateService: CurrentRateService,
|
||||||
private currency: Currency
|
private currency: string
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public computeTransactionPoints(orders: PortfolioOrder[]) {
|
public computeTransactionPoints(orders: PortfolioOrder[]) {
|
||||||
@ -157,7 +156,7 @@ export class PortfolioCalculator {
|
|||||||
let firstIndex = this.transactionPoints.length;
|
let firstIndex = this.transactionPoints.length;
|
||||||
const dates = [];
|
const dates = [];
|
||||||
const dataGatheringItems: IDataGatheringItem[] = [];
|
const dataGatheringItems: IDataGatheringItem[] = [];
|
||||||
const currencies: { [symbol: string]: Currency } = {};
|
const currencies: { [symbol: string]: string } = {};
|
||||||
|
|
||||||
dates.push(resetHours(start));
|
dates.push(resetHours(start));
|
||||||
for (const item of this.transactionPoints[firstIndex - 1].items) {
|
for (const item of this.transactionPoints[firstIndex - 1].items) {
|
||||||
@ -521,7 +520,7 @@ export class PortfolioCalculator {
|
|||||||
[date: string]: { [symbol: string]: Big };
|
[date: string]: { [symbol: string]: Big };
|
||||||
} = {};
|
} = {};
|
||||||
if (j >= 0) {
|
if (j >= 0) {
|
||||||
const currencies: { [name: string]: Currency } = {};
|
const currencies: { [name: string]: string } = {};
|
||||||
const dataGatheringItems: IDataGatheringItem[] = [];
|
const dataGatheringItems: IDataGatheringItem[] = [];
|
||||||
|
|
||||||
for (const item of this.transactionPoints[j].items) {
|
for (const item of this.transactionPoints[j].items) {
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
|
import { AccessService } from '@ghostfolio/api/app/access/access.service';
|
||||||
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||||
import {
|
import {
|
||||||
hasNotDefinedValuesInObject,
|
hasNotDefinedValuesInObject,
|
||||||
nullifyValuesInObject
|
nullifyValuesInObject
|
||||||
} from '@ghostfolio/api/helper/object.helper';
|
} from '@ghostfolio/api/helper/object.helper';
|
||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
|
import { baseCurrency } from '@ghostfolio/common/config';
|
||||||
import {
|
import {
|
||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
PortfolioPerformance,
|
PortfolioPerformance,
|
||||||
|
PortfolioPublicDetails,
|
||||||
PortfolioReport,
|
PortfolioReport,
|
||||||
PortfolioSummary
|
PortfolioSummary
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
@ -38,6 +42,8 @@ import { PortfolioService } from './portfolio.service';
|
|||||||
@Controller('portfolio')
|
@Controller('portfolio')
|
||||||
export class PortfolioController {
|
export class PortfolioController {
|
||||||
public constructor(
|
public constructor(
|
||||||
|
private readonly accessService: AccessService,
|
||||||
|
private readonly configurationService: ConfigurationService,
|
||||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||||
private readonly portfolioService: PortfolioService,
|
private readonly portfolioService: PortfolioService,
|
||||||
@Inject(REQUEST) private readonly request: RequestWithUser,
|
@Inject(REQUEST) private readonly request: RequestWithUser,
|
||||||
@ -47,8 +53,17 @@ export class PortfolioController {
|
|||||||
@Get('investments')
|
@Get('investments')
|
||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
public async findAll(
|
public async findAll(
|
||||||
@Headers('impersonation-id') impersonationId
|
@Headers('impersonation-id') impersonationId,
|
||||||
|
@Res() res: Response
|
||||||
): Promise<InvestmentItem[]> {
|
): Promise<InvestmentItem[]> {
|
||||||
|
if (
|
||||||
|
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
|
||||||
|
this.request.user.subscription.type === 'Basic'
|
||||||
|
) {
|
||||||
|
res.status(StatusCodes.FORBIDDEN);
|
||||||
|
return <any>res.json([]);
|
||||||
|
}
|
||||||
|
|
||||||
let investments = await this.portfolioService.getInvestments(
|
let investments = await this.portfolioService.getInvestments(
|
||||||
impersonationId
|
impersonationId
|
||||||
);
|
);
|
||||||
@ -68,7 +83,7 @@ export class PortfolioController {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
return investments;
|
return <any>res.json(investments);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('chart')
|
@Get('chart')
|
||||||
@ -125,8 +140,20 @@ export class PortfolioController {
|
|||||||
@Query('range') range,
|
@Query('range') range,
|
||||||
@Res() res: Response
|
@Res() res: Response
|
||||||
): Promise<PortfolioDetails> {
|
): Promise<PortfolioDetails> {
|
||||||
|
if (
|
||||||
|
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
|
||||||
|
this.request.user.subscription.type === 'Basic'
|
||||||
|
) {
|
||||||
|
res.status(StatusCodes.FORBIDDEN);
|
||||||
|
return <any>res.json({ accounts: {}, holdings: {} });
|
||||||
|
}
|
||||||
|
|
||||||
const { accounts, holdings, hasErrors } =
|
const { accounts, holdings, hasErrors } =
|
||||||
await this.portfolioService.getDetails(impersonationId, range);
|
await this.portfolioService.getDetails(
|
||||||
|
impersonationId,
|
||||||
|
this.request.user.id,
|
||||||
|
range
|
||||||
|
);
|
||||||
|
|
||||||
if (hasErrors || hasNotDefinedValuesInObject(holdings)) {
|
if (hasErrors || hasNotDefinedValuesInObject(holdings)) {
|
||||||
res.status(StatusCodes.ACCEPTED);
|
res.status(StatusCodes.ACCEPTED);
|
||||||
@ -156,8 +183,9 @@ export class PortfolioController {
|
|||||||
portfolioPosition.grossPerformance = null;
|
portfolioPosition.grossPerformance = null;
|
||||||
portfolioPosition.investment =
|
portfolioPosition.investment =
|
||||||
portfolioPosition.investment / totalInvestment;
|
portfolioPosition.investment / totalInvestment;
|
||||||
|
portfolioPosition.netPerformance = null;
|
||||||
portfolioPosition.quantity = null;
|
portfolioPosition.quantity = null;
|
||||||
|
portfolioPosition.value = portfolioPosition.value / totalValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [name, { current, original }] of Object.entries(accounts)) {
|
for (const [name, { current, original }] of Object.entries(accounts)) {
|
||||||
@ -232,6 +260,65 @@ export class PortfolioController {
|
|||||||
return <any>res.json(result);
|
return <any>res.json(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('public/:accessId')
|
||||||
|
public async getPublic(
|
||||||
|
@Param('accessId') accessId,
|
||||||
|
@Res() res: Response
|
||||||
|
): Promise<PortfolioPublicDetails> {
|
||||||
|
const access = await this.accessService.access({ id: accessId });
|
||||||
|
const user = await this.userService.user({
|
||||||
|
id: access.userId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!access) {
|
||||||
|
res.status(StatusCodes.NOT_FOUND);
|
||||||
|
return <any>res.json({ accounts: {}, holdings: {} });
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasDetails = true;
|
||||||
|
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
||||||
|
hasDetails = user.subscription.type === 'Premium';
|
||||||
|
}
|
||||||
|
|
||||||
|
const { holdings } = await this.portfolioService.getDetails(
|
||||||
|
access.userId,
|
||||||
|
access.userId
|
||||||
|
);
|
||||||
|
|
||||||
|
const portfolioPublicDetails: PortfolioPublicDetails = {
|
||||||
|
hasDetails,
|
||||||
|
holdings: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalValue = Object.values(holdings)
|
||||||
|
.filter((holding) => {
|
||||||
|
return holding.assetClass === 'EQUITY';
|
||||||
|
})
|
||||||
|
.map((portfolioPosition) => {
|
||||||
|
return this.exchangeRateDataService.toCurrency(
|
||||||
|
portfolioPosition.quantity * portfolioPosition.marketPrice,
|
||||||
|
portfolioPosition.currency,
|
||||||
|
this.request.user?.Settings?.currency ?? baseCurrency
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
|
for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
|
||||||
|
if (portfolioPosition.assetClass === 'EQUITY') {
|
||||||
|
portfolioPublicDetails.holdings[symbol] = {
|
||||||
|
allocationCurrent: portfolioPosition.allocationCurrent,
|
||||||
|
countries: hasDetails ? portfolioPosition.countries : [],
|
||||||
|
currency: portfolioPosition.currency,
|
||||||
|
name: portfolioPosition.name,
|
||||||
|
sectors: hasDetails ? portfolioPosition.sectors : [],
|
||||||
|
value: portfolioPosition.value / totalValue
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <any>res.json(portfolioPublicDetails);
|
||||||
|
}
|
||||||
|
|
||||||
@Get('summary')
|
@Get('summary')
|
||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
public async getSummary(
|
public async getSummary(
|
||||||
@ -295,8 +382,19 @@ export class PortfolioController {
|
|||||||
@Get('report')
|
@Get('report')
|
||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
public async getReport(
|
public async getReport(
|
||||||
@Headers('impersonation-id') impersonationId
|
@Headers('impersonation-id') impersonationId,
|
||||||
|
@Res() res: Response
|
||||||
): Promise<PortfolioReport> {
|
): Promise<PortfolioReport> {
|
||||||
return await this.portfolioService.getReport(impersonationId);
|
if (
|
||||||
|
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
|
||||||
|
this.request.user.subscription.type === 'Basic'
|
||||||
|
) {
|
||||||
|
res.status(StatusCodes.FORBIDDEN);
|
||||||
|
return <any>res.json({ rules: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
return <any>(
|
||||||
|
res.json(await this.portfolioService.getReport(impersonationId))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { AccessModule } from '@ghostfolio/api/app/access/access.module';
|
||||||
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||||
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
|
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
|
||||||
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
||||||
@ -18,6 +19,7 @@ import { RulesService } from './rules.service';
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
AccessModule,
|
||||||
ConfigurationModule,
|
ConfigurationModule,
|
||||||
DataGatheringModule,
|
DataGatheringModule,
|
||||||
DataProviderModule,
|
DataProviderModule,
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
// TODO ///////////
|
||||||
|
|
||||||
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||||
import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details.interface';
|
import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details.interface';
|
||||||
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
||||||
@ -21,7 +23,11 @@ import { ImpersonationService } from '@ghostfolio/api/services/impersonation.ser
|
|||||||
import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces';
|
import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { EnhancedSymbolProfile } from '@ghostfolio/api/services/interfaces/symbol-profile.interface';
|
import { EnhancedSymbolProfile } from '@ghostfolio/api/services/interfaces/symbol-profile.interface';
|
||||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
||||||
import { UNKNOWN_KEY, ghostfolioCashSymbol } from '@ghostfolio/common/config';
|
import {
|
||||||
|
UNKNOWN_KEY,
|
||||||
|
baseCurrency,
|
||||||
|
ghostfolioCashSymbol
|
||||||
|
} from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
||||||
import {
|
import {
|
||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
@ -39,15 +45,9 @@ import type {
|
|||||||
} from '@ghostfolio/common/types';
|
} from '@ghostfolio/common/types';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
import {
|
import { AssetClass, DataSource, Type as TypeOfOrder } from '@prisma/client';
|
||||||
AssetClass,
|
|
||||||
Currency,
|
|
||||||
DataSource,
|
|
||||||
Type as TypeOfOrder
|
|
||||||
} from '@prisma/client';
|
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import {
|
import {
|
||||||
differenceInDays,
|
|
||||||
endOfToday,
|
endOfToday,
|
||||||
format,
|
format,
|
||||||
isAfter,
|
isAfter,
|
||||||
@ -59,7 +59,7 @@ import {
|
|||||||
subDays,
|
subDays,
|
||||||
subYears
|
subYears
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import { isEmpty, isNumber } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
HistoricalDataItem,
|
HistoricalDataItem,
|
||||||
@ -84,7 +84,7 @@ export class PortfolioService {
|
|||||||
public async getInvestments(
|
public async getInvestments(
|
||||||
aImpersonationId: string
|
aImpersonationId: string
|
||||||
): Promise<InvestmentItem[]> {
|
): Promise<InvestmentItem[]> {
|
||||||
const userId = await this.getUserId(aImpersonationId);
|
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
this.currentRateService,
|
this.currentRateService,
|
||||||
@ -112,7 +112,7 @@ export class PortfolioService {
|
|||||||
aImpersonationId: string,
|
aImpersonationId: string,
|
||||||
aDateRange: DateRange = 'max'
|
aDateRange: DateRange = 'max'
|
||||||
): Promise<HistoricalDataItem[]> {
|
): Promise<HistoricalDataItem[]> {
|
||||||
const userId = await this.getUserId(aImpersonationId);
|
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
this.currentRateService,
|
this.currentRateService,
|
||||||
@ -154,11 +154,12 @@ export class PortfolioService {
|
|||||||
|
|
||||||
public async getDetails(
|
public async getDetails(
|
||||||
aImpersonationId: string,
|
aImpersonationId: string,
|
||||||
|
aUserId: string,
|
||||||
aDateRange: DateRange = 'max'
|
aDateRange: DateRange = 'max'
|
||||||
): Promise<PortfolioDetails & { hasErrors: boolean }> {
|
): Promise<PortfolioDetails & { hasErrors: boolean }> {
|
||||||
const userId = await this.getUserId(aImpersonationId);
|
const userId = await this.getUserId(aImpersonationId, aUserId);
|
||||||
|
|
||||||
const userCurrency = this.request.user.Settings.currency;
|
const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency;
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
this.currentRateService,
|
this.currentRateService,
|
||||||
userCurrency
|
userCurrency
|
||||||
@ -271,7 +272,7 @@ export class PortfolioService {
|
|||||||
aImpersonationId: string,
|
aImpersonationId: string,
|
||||||
aSymbol: string
|
aSymbol: string
|
||||||
): Promise<PortfolioPositionDetail> {
|
): Promise<PortfolioPositionDetail> {
|
||||||
const userId = await this.getUserId(aImpersonationId);
|
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
||||||
|
|
||||||
const orders = (await this.orderService.getOrders({ userId })).filter(
|
const orders = (await this.orderService.getOrders({ userId })).filter(
|
||||||
(order) => order.symbol === aSymbol
|
(order) => order.symbol === aSymbol
|
||||||
@ -490,7 +491,7 @@ export class PortfolioService {
|
|||||||
aImpersonationId: string,
|
aImpersonationId: string,
|
||||||
aDateRange: DateRange = 'max'
|
aDateRange: DateRange = 'max'
|
||||||
): Promise<{ hasErrors: boolean; positions: Position[] }> {
|
): Promise<{ hasErrors: boolean; positions: Position[] }> {
|
||||||
const userId = await this.getUserId(aImpersonationId);
|
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
this.currentRateService,
|
this.currentRateService,
|
||||||
@ -561,7 +562,7 @@ export class PortfolioService {
|
|||||||
aImpersonationId: string,
|
aImpersonationId: string,
|
||||||
aDateRange: DateRange = 'max'
|
aDateRange: DateRange = 'max'
|
||||||
): Promise<{ hasErrors: boolean; performance: PortfolioPerformance }> {
|
): Promise<{ hasErrors: boolean; performance: PortfolioPerformance }> {
|
||||||
const userId = await this.getUserId(aImpersonationId);
|
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
this.currentRateService,
|
this.currentRateService,
|
||||||
@ -634,8 +635,8 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getReport(impersonationId: string): Promise<PortfolioReport> {
|
public async getReport(impersonationId: string): Promise<PortfolioReport> {
|
||||||
const userId = await this.getUserId(impersonationId);
|
const currency = this.request.user.Settings.currency;
|
||||||
const baseCurrency = this.request.user.Settings.currency;
|
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
||||||
|
|
||||||
const { orders, transactionPoints } = await this.getTransactionPoints({
|
const { orders, transactionPoints } = await this.getTransactionPoints({
|
||||||
userId
|
userId
|
||||||
@ -649,7 +650,7 @@ export class PortfolioService {
|
|||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
this.currentRateService,
|
this.currentRateService,
|
||||||
this.request.user.Settings.currency
|
currency
|
||||||
);
|
);
|
||||||
portfolioCalculator.setTransactionPoints(transactionPoints);
|
portfolioCalculator.setTransactionPoints(transactionPoints);
|
||||||
|
|
||||||
@ -665,7 +666,7 @@ export class PortfolioService {
|
|||||||
const accounts = await this.getAccounts(
|
const accounts = await this.getAccounts(
|
||||||
orders,
|
orders,
|
||||||
portfolioItemsNow,
|
portfolioItemsNow,
|
||||||
baseCurrency,
|
currency,
|
||||||
userId
|
userId
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
@ -685,7 +686,7 @@ export class PortfolioService {
|
|||||||
accounts
|
accounts
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
{ baseCurrency }
|
{ baseCurrency: currency }
|
||||||
),
|
),
|
||||||
currencyClusterRisk: await this.rulesService.evaluate(
|
currencyClusterRisk: await this.rulesService.evaluate(
|
||||||
[
|
[
|
||||||
@ -706,7 +707,7 @@ export class PortfolioService {
|
|||||||
currentPositions
|
currentPositions
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
{ baseCurrency }
|
{ baseCurrency: currency }
|
||||||
),
|
),
|
||||||
fees: await this.rulesService.evaluate(
|
fees: await this.rulesService.evaluate(
|
||||||
[
|
[
|
||||||
@ -716,7 +717,7 @@ export class PortfolioService {
|
|||||||
this.getFees(orders)
|
this.getFees(orders)
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
{ baseCurrency }
|
{ baseCurrency: currency }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -724,7 +725,7 @@ export class PortfolioService {
|
|||||||
|
|
||||||
public async getSummary(aImpersonationId: string): Promise<PortfolioSummary> {
|
public async getSummary(aImpersonationId: string): Promise<PortfolioSummary> {
|
||||||
const currency = this.request.user.Settings.currency;
|
const currency = this.request.user.Settings.currency;
|
||||||
const userId = await this.getUserId(aImpersonationId);
|
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
||||||
|
|
||||||
const performanceInformation = await this.getPerformance(aImpersonationId);
|
const performanceInformation = await this.getPerformance(aImpersonationId);
|
||||||
|
|
||||||
@ -775,7 +776,7 @@ export class PortfolioService {
|
|||||||
assetClass: AssetClass.CASH,
|
assetClass: AssetClass.CASH,
|
||||||
assetSubClass: AssetClass.CASH,
|
assetSubClass: AssetClass.CASH,
|
||||||
countries: [],
|
countries: [],
|
||||||
currency: Currency.CHF,
|
currency: 'CHF',
|
||||||
grossPerformance: 0,
|
grossPerformance: 0,
|
||||||
grossPerformancePercent: 0,
|
grossPerformancePercent: 0,
|
||||||
investment: cashValue.toNumber(),
|
investment: cashValue.toNumber(),
|
||||||
@ -826,7 +827,7 @@ export class PortfolioService {
|
|||||||
return { transactionPoints: [], orders: [] };
|
return { transactionPoints: [], orders: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const userCurrency = this.request.user.Settings.currency;
|
const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency;
|
||||||
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
|
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
|
||||||
currency: order.currency,
|
currency: order.currency,
|
||||||
dataSource: order.dataSource,
|
dataSource: order.dataSource,
|
||||||
@ -865,7 +866,7 @@ export class PortfolioService {
|
|||||||
private async getAccounts(
|
private async getAccounts(
|
||||||
orders: OrderWithAccount[],
|
orders: OrderWithAccount[],
|
||||||
portfolioItemsNow: { [p: string]: TimelinePosition },
|
portfolioItemsNow: { [p: string]: TimelinePosition },
|
||||||
userCurrency: Currency,
|
userCurrency: string,
|
||||||
userId: string
|
userId: string
|
||||||
) {
|
) {
|
||||||
const accounts: PortfolioDetails['accounts'] = {};
|
const accounts: PortfolioDetails['accounts'] = {};
|
||||||
@ -926,19 +927,19 @@ export class PortfolioService {
|
|||||||
return accounts;
|
return accounts;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getUserId(aImpersonationId: string) {
|
private async getUserId(aImpersonationId: string, aUserId: string) {
|
||||||
const impersonationUserId =
|
const impersonationUserId =
|
||||||
await this.impersonationService.validateImpersonationId(
|
await this.impersonationService.validateImpersonationId(
|
||||||
aImpersonationId,
|
aImpersonationId,
|
||||||
this.request.user.id
|
aUserId
|
||||||
);
|
);
|
||||||
|
|
||||||
return impersonationUserId || this.request.user.id;
|
return impersonationUserId || aUserId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getTotalByType(
|
private getTotalByType(
|
||||||
orders: OrderWithAccount[],
|
orders: OrderWithAccount[],
|
||||||
currency: Currency,
|
currency: string,
|
||||||
type: TypeOfOrder
|
type: TypeOfOrder
|
||||||
) {
|
) {
|
||||||
return orders
|
return orders
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
import { Rule } from '@ghostfolio/api/models/rule';
|
import { Rule } from '@ghostfolio/api/models/rule';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Currency } from '@prisma/client';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RulesService {
|
export class RulesService {
|
||||||
@ -9,7 +8,7 @@ export class RulesService {
|
|||||||
|
|
||||||
public async evaluate<T extends RuleSettings>(
|
public async evaluate<T extends RuleSettings>(
|
||||||
aRules: Rule<T>[],
|
aRules: Rule<T>[],
|
||||||
aUserSettings: { baseCurrency: Currency }
|
aUserSettings: { baseCurrency: string }
|
||||||
) {
|
) {
|
||||||
return aRules
|
return aRules
|
||||||
.filter((rule) => {
|
.filter((rule) => {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Currency, DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
|
|
||||||
export interface LookupItem {
|
export interface LookupItem {
|
||||||
currency: Currency;
|
currency: string;
|
||||||
dataSource: DataSource;
|
dataSource: DataSource;
|
||||||
name: string;
|
name: string;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Currency, DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
|
|
||||||
export interface SymbolItem {
|
export interface SymbolItem {
|
||||||
currency: Currency;
|
currency: string;
|
||||||
dataSource: DataSource;
|
dataSource: DataSource;
|
||||||
marketPrice: number;
|
marketPrice: number;
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data
|
|||||||
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Currency, DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
|
|
||||||
import { LookupItem } from './interfaces/lookup-item.interface';
|
import { LookupItem } from './interfaces/lookup-item.interface';
|
||||||
import { SymbolItem } from './interfaces/symbol-item.interface';
|
import { SymbolItem } from './interfaces/symbol-item.interface';
|
||||||
@ -20,8 +20,8 @@ export class SymbolService {
|
|||||||
|
|
||||||
if (dataGatheringItem.dataSource && marketPrice) {
|
if (dataGatheringItem.dataSource && marketPrice) {
|
||||||
return {
|
return {
|
||||||
|
currency,
|
||||||
marketPrice,
|
marketPrice,
|
||||||
currency: <Currency>(<unknown>currency),
|
|
||||||
dataSource: dataGatheringItem.dataSource
|
dataSource: dataGatheringItem.dataSource
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Currency, ViewMode } from '@prisma/client';
|
import { ViewMode } from '@prisma/client';
|
||||||
|
|
||||||
export interface UserSettingsParams {
|
export interface UserSettingsParams {
|
||||||
currency?: Currency;
|
currency?: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
viewMode?: ViewMode;
|
viewMode?: ViewMode;
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { Currency, ViewMode } from '@prisma/client';
|
import { ViewMode } from '@prisma/client';
|
||||||
import { IsString } from 'class-validator';
|
import { IsString } from 'class-validator';
|
||||||
|
|
||||||
export class UpdateUserSettingsDto {
|
export class UpdateUserSettingsDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
baseCurrency: Currency;
|
baseCurrency: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
viewMode: ViewMode;
|
viewMode: ViewMode;
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
|
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { locale } from '@ghostfolio/common/config';
|
import { baseCurrency, locale } from '@ghostfolio/common/config';
|
||||||
import { User as IUser, UserWithSettings } from '@ghostfolio/common/interfaces';
|
import { User as IUser, UserWithSettings } from '@ghostfolio/common/interfaces';
|
||||||
import { getPermissions, permissions } from '@ghostfolio/common/permissions';
|
import { getPermissions, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { SubscriptionType } from '@ghostfolio/common/types/subscription.type';
|
import { SubscriptionType } from '@ghostfolio/common/types/subscription.type';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Currency, Prisma, Provider, User, ViewMode } from '@prisma/client';
|
import { Prisma, Provider, User, ViewMode } from '@prisma/client';
|
||||||
|
|
||||||
import { UserSettingsParams } from './interfaces/user-settings-params.interface';
|
import { UserSettingsParams } from './interfaces/user-settings-params.interface';
|
||||||
import { UserSettings } from './interfaces/user-settings.interface';
|
import { UserSettings } from './interfaces/user-settings.interface';
|
||||||
@ -15,7 +15,7 @@ const crypto = require('crypto');
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserService {
|
export class UserService {
|
||||||
public static DEFAULT_CURRENCY = Currency.USD;
|
public static DEFAULT_CURRENCY = 'USD';
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly configurationService: ConfigurationService,
|
private readonly configurationService: ConfigurationService,
|
||||||
@ -144,9 +144,15 @@ export class UserService {
|
|||||||
...data,
|
...data,
|
||||||
Account: {
|
Account: {
|
||||||
create: {
|
create: {
|
||||||
|
currency: baseCurrency,
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
name: 'Default Account'
|
name: 'Default Account'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
Settings: {
|
||||||
|
create: {
|
||||||
|
currency: baseCurrency
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import { Currency } from '@prisma/client';
|
|
||||||
|
|
||||||
export interface UserSettings {
|
export interface UserSettings {
|
||||||
baseCurrency: Currency;
|
baseCurrency: string;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Account, Currency, SymbolProfile } from '@prisma/client';
|
import { Account, SymbolProfile } from '@prisma/client';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import { IOrder } from '../services/interfaces/interfaces';
|
import { IOrder } from '../services/interfaces/interfaces';
|
||||||
@ -6,7 +6,7 @@ import { OrderType } from './order-type';
|
|||||||
|
|
||||||
export class Order {
|
export class Order {
|
||||||
private account: Account;
|
private account: Account;
|
||||||
private currency: Currency;
|
private currency: string;
|
||||||
private fee: number;
|
private fee: number;
|
||||||
private date: string;
|
private date: string;
|
||||||
private id: string;
|
private id: string;
|
||||||
|
@ -3,7 +3,6 @@ import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.in
|
|||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { groupBy } from '@ghostfolio/common/helper';
|
import { groupBy } from '@ghostfolio/common/helper';
|
||||||
import { TimelinePosition } from '@ghostfolio/common/interfaces';
|
import { TimelinePosition } from '@ghostfolio/common/interfaces';
|
||||||
import { Currency } from '@prisma/client';
|
|
||||||
|
|
||||||
import { EvaluationResult } from './interfaces/evaluation-result.interface';
|
import { EvaluationResult } from './interfaces/evaluation-result.interface';
|
||||||
import { RuleInterface } from './interfaces/rule.interface';
|
import { RuleInterface } from './interfaces/rule.interface';
|
||||||
@ -29,7 +28,7 @@ export abstract class Rule<T extends RuleSettings> implements RuleInterface<T> {
|
|||||||
public groupCurrentPositionsByAttribute(
|
public groupCurrentPositionsByAttribute(
|
||||||
positions: TimelinePosition[],
|
positions: TimelinePosition[],
|
||||||
attribute: keyof TimelinePosition,
|
attribute: keyof TimelinePosition,
|
||||||
baseCurrency: Currency
|
baseCurrency: string
|
||||||
) {
|
) {
|
||||||
return Array.from(groupBy(attribute, positions).entries()).map(
|
return Array.from(groupBy(attribute, positions).entries()).map(
|
||||||
([attributeValue, objs]) => ({
|
([attributeValue, objs]) => ({
|
||||||
|
@ -2,8 +2,6 @@ import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/curre
|
|||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
|
||||||
import { Currency } from '@prisma/client';
|
|
||||||
|
|
||||||
import { Rule } from '../../rule';
|
import { Rule } from '../../rule';
|
||||||
|
|
||||||
@ -69,5 +67,5 @@ export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule<Setti
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Settings extends RuleSettings {
|
interface Settings extends RuleSettings {
|
||||||
baseCurrency: Currency;
|
baseCurrency: string;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
|
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
|
||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
||||||
import { Currency } from '@prisma/client';
|
|
||||||
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
||||||
|
|
||||||
import { Rule } from '../../rule';
|
import { Rule } from '../../rule';
|
||||||
@ -69,5 +68,5 @@ export class CurrencyClusterRiskBaseCurrencyInitialInvestment extends Rule<Setti
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Settings extends RuleSettings {
|
interface Settings extends RuleSettings {
|
||||||
baseCurrency: Currency;
|
baseCurrency: string;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
|
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
|
||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
||||||
import { Currency } from '@prisma/client';
|
|
||||||
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
||||||
|
|
||||||
import { Rule } from '../../rule';
|
import { Rule } from '../../rule';
|
||||||
@ -69,6 +68,6 @@ export class CurrencyClusterRiskCurrentInvestment extends Rule<Settings> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Settings extends RuleSettings {
|
interface Settings extends RuleSettings {
|
||||||
baseCurrency: Currency;
|
baseCurrency: string;
|
||||||
threshold: number;
|
threshold: number;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
|
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
|
||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
||||||
import { Currency } from '@prisma/client';
|
|
||||||
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
||||||
|
|
||||||
import { Rule } from '../../rule';
|
import { Rule } from '../../rule';
|
||||||
@ -69,6 +68,6 @@ export class CurrencyClusterRiskInitialInvestment extends Rule<Settings> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Settings extends RuleSettings {
|
interface Settings extends RuleSettings {
|
||||||
baseCurrency: Currency;
|
baseCurrency: string;
|
||||||
threshold: number;
|
threshold: number;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
||||||
import { Currency } from '@prisma/client';
|
|
||||||
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
||||||
|
|
||||||
import { Rule } from '../../rule';
|
import { Rule } from '../../rule';
|
||||||
@ -46,6 +45,6 @@ export class FeeRatioInitialInvestment extends Rule<Settings> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Settings extends RuleSettings {
|
interface Settings extends RuleSettings {
|
||||||
baseCurrency: Currency;
|
baseCurrency: string;
|
||||||
threshold: number;
|
threshold: number;
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,11 @@ export class CronService {
|
|||||||
|
|
||||||
@Cron(CronExpression.EVERY_12_HOURS)
|
@Cron(CronExpression.EVERY_12_HOURS)
|
||||||
public async runEveryTwelveHours() {
|
public async runEveryTwelveHours() {
|
||||||
await this.dataGatheringService.gatherProfileData();
|
|
||||||
await this.exchangeRateDataService.loadCurrencies();
|
await this.exchangeRateDataService.loadCurrencies();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Cron(CronExpression.EVERY_WEEKEND)
|
||||||
|
public async runEveryWeekend() {
|
||||||
|
await this.dataGatheringService.gatherProfileData();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { CryptocurrencyService } from './cryptocurrency.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [CryptocurrencyService],
|
||||||
|
exports: [CryptocurrencyService]
|
||||||
|
})
|
||||||
|
export class CryptocurrencyModule {}
|
@ -0,0 +1,28 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
const cryptocurrencies = require('cryptocurrencies');
|
||||||
|
|
||||||
|
const customCryptocurrencies = require('./custom-cryptocurrencies.json');
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CryptocurrencyService {
|
||||||
|
private combinedCryptocurrencies: string[];
|
||||||
|
|
||||||
|
public constructor() {}
|
||||||
|
|
||||||
|
public isCrypto(aSymbol = '') {
|
||||||
|
const cryptocurrencySymbol = aSymbol.substring(0, aSymbol.length - 3);
|
||||||
|
return this.getCryptocurrencies().includes(cryptocurrencySymbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCryptocurrencies() {
|
||||||
|
if (!this.combinedCryptocurrencies) {
|
||||||
|
this.combinedCryptocurrencies = [
|
||||||
|
...cryptocurrencies.symbols(),
|
||||||
|
...Object.keys(customCryptocurrencies)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.combinedCryptocurrencies;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"1INCH": "1inch",
|
||||||
|
"AVAX": "Avalanche",
|
||||||
|
"MATIC": "Polygon",
|
||||||
|
"SHIB": "Shiba Inu"
|
||||||
|
}
|
@ -1,12 +1,21 @@
|
|||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||||
|
import { DataEnhancerModule } from '@ghostfolio/api/services/data-provider/data-enhancer/data-enhancer.module';
|
||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { ExchangeRateDataModule } from './exchange-rate-data.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ConfigurationModule, DataProviderModule, PrismaModule],
|
imports: [
|
||||||
|
ConfigurationModule,
|
||||||
|
DataEnhancerModule,
|
||||||
|
DataProviderModule,
|
||||||
|
ExchangeRateDataModule,
|
||||||
|
PrismaModule
|
||||||
|
],
|
||||||
providers: [DataGatheringService],
|
providers: [DataGatheringService],
|
||||||
exports: [DataGatheringService]
|
exports: [DataEnhancerModule, DataGatheringService]
|
||||||
})
|
})
|
||||||
export class DataGatheringModule {}
|
export class DataGatheringModule {}
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
benchmarks,
|
benchmarks,
|
||||||
currencyPairs,
|
|
||||||
ghostfolioFearAndGreedIndexSymbol
|
ghostfolioFearAndGreedIndexSymbol
|
||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT, getUtc, resetHours } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, resetHours } from '@ghostfolio/common/helper';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
import {
|
import {
|
||||||
differenceInHours,
|
differenceInHours,
|
||||||
@ -19,6 +18,8 @@ import {
|
|||||||
import { ConfigurationService } from './configuration.service';
|
import { ConfigurationService } from './configuration.service';
|
||||||
import { DataProviderService } from './data-provider/data-provider.service';
|
import { DataProviderService } from './data-provider/data-provider.service';
|
||||||
import { GhostfolioScraperApiService } from './data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
import { GhostfolioScraperApiService } from './data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
||||||
|
import { DataEnhancerInterface } from './data-provider/interfaces/data-enhancer.interface';
|
||||||
|
import { ExchangeRateDataService } from './exchange-rate-data.service';
|
||||||
import { IDataGatheringItem } from './interfaces/interfaces';
|
import { IDataGatheringItem } from './interfaces/interfaces';
|
||||||
import { PrismaService } from './prisma.service';
|
import { PrismaService } from './prisma.service';
|
||||||
|
|
||||||
@ -26,7 +27,10 @@ import { PrismaService } from './prisma.service';
|
|||||||
export class DataGatheringService {
|
export class DataGatheringService {
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly configurationService: ConfigurationService,
|
private readonly configurationService: ConfigurationService,
|
||||||
|
@Inject('DataEnhancers')
|
||||||
|
private readonly dataEnhancers: DataEnhancerInterface[],
|
||||||
private readonly dataProviderService: DataProviderService,
|
private readonly dataProviderService: DataProviderService,
|
||||||
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||||
private readonly ghostfolioScraperApi: GhostfolioScraperApiService,
|
private readonly ghostfolioScraperApi: GhostfolioScraperApiService,
|
||||||
private readonly prismaService: PrismaService
|
private readonly prismaService: PrismaService
|
||||||
) {}
|
) {}
|
||||||
@ -129,10 +133,28 @@ export class DataGatheringService {
|
|||||||
|
|
||||||
const currentData = await this.dataProviderService.get(dataGatheringItems);
|
const currentData = await this.dataProviderService.get(dataGatheringItems);
|
||||||
|
|
||||||
for (const [
|
for (const [symbol, response] of Object.entries(currentData)) {
|
||||||
symbol,
|
for (const dataEnhancer of this.dataEnhancers) {
|
||||||
{ assetClass, assetSubClass, countries, currency, dataSource, name }
|
try {
|
||||||
] of Object.entries(currentData)) {
|
currentData[symbol] = await dataEnhancer.enhance({
|
||||||
|
response,
|
||||||
|
symbol
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to enhance data for symbol ${symbol}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
assetClass,
|
||||||
|
assetSubClass,
|
||||||
|
countries,
|
||||||
|
currency,
|
||||||
|
dataSource,
|
||||||
|
name,
|
||||||
|
sectors
|
||||||
|
} = currentData[symbol];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.prismaService.symbolProfile.upsert({
|
await this.prismaService.symbolProfile.upsert({
|
||||||
create: {
|
create: {
|
||||||
@ -142,6 +164,7 @@ export class DataGatheringService {
|
|||||||
currency,
|
currency,
|
||||||
dataSource,
|
dataSource,
|
||||||
name,
|
name,
|
||||||
|
sectors,
|
||||||
symbol
|
symbol
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
@ -149,7 +172,8 @@ export class DataGatheringService {
|
|||||||
assetSubClass,
|
assetSubClass,
|
||||||
countries,
|
countries,
|
||||||
currency,
|
currency,
|
||||||
name
|
name,
|
||||||
|
sectors
|
||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
dataSource_symbol: {
|
dataSource_symbol: {
|
||||||
@ -230,6 +254,8 @@ export class DataGatheringService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.exchangeRateDataService.initialize();
|
||||||
|
|
||||||
if (hasError) {
|
if (hasError) {
|
||||||
throw '';
|
throw '';
|
||||||
}
|
}
|
||||||
@ -316,15 +342,15 @@ export class DataGatheringService {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const currencyPairsToGather = currencyPairs.map(
|
const currencyPairsToGather = this.exchangeRateDataService
|
||||||
({ dataSource, symbol }) => {
|
.getCurrencyPairs()
|
||||||
|
.map(({ dataSource, symbol }) => {
|
||||||
return {
|
return {
|
||||||
dataSource,
|
dataSource,
|
||||||
symbol,
|
symbol,
|
||||||
date: startDate
|
date: startDate
|
||||||
};
|
};
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const customSymbolsToGather =
|
const customSymbolsToGather =
|
||||||
await this.ghostfolioScraperApi.getCustomSymbolsToGather(startDate);
|
await this.ghostfolioScraperApi.getCustomSymbolsToGather(startDate);
|
||||||
@ -338,28 +364,45 @@ export class DataGatheringService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getSymbolsMax(): Promise<IDataGatheringItem[]> {
|
private async getSymbolsMax(): Promise<IDataGatheringItem[]> {
|
||||||
const startDate = new Date(getUtc('2015-01-01'));
|
const startDate =
|
||||||
|
(
|
||||||
|
await this.prismaService.order.findFirst({
|
||||||
|
orderBy: [{ date: 'asc' }]
|
||||||
|
})
|
||||||
|
)?.date ?? new Date();
|
||||||
|
|
||||||
const customSymbolsToGather =
|
const customSymbolsToGather =
|
||||||
await this.ghostfolioScraperApi.getCustomSymbolsToGather(startDate);
|
await this.ghostfolioScraperApi.getCustomSymbolsToGather(startDate);
|
||||||
|
|
||||||
const currencyPairsToGather = currencyPairs.map(
|
const currencyPairsToGather = this.exchangeRateDataService
|
||||||
({ dataSource, symbol }) => {
|
.getCurrencyPairs()
|
||||||
|
.map(({ dataSource, symbol }) => {
|
||||||
return {
|
return {
|
||||||
dataSource,
|
dataSource,
|
||||||
symbol,
|
symbol,
|
||||||
date: startDate
|
date: startDate
|
||||||
};
|
};
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const symbolProfilesToGather =
|
const symbolProfilesToGather = (
|
||||||
await this.prismaService.symbolProfile.findMany({
|
await this.prismaService.symbolProfile.findMany({
|
||||||
orderBy: [{ symbol: 'asc' }],
|
orderBy: [{ symbol: 'asc' }],
|
||||||
select: {
|
select: {
|
||||||
dataSource: true,
|
dataSource: true,
|
||||||
|
Order: {
|
||||||
|
orderBy: [{ date: 'asc' }],
|
||||||
|
select: { date: true },
|
||||||
|
take: 1
|
||||||
|
},
|
||||||
symbol: true
|
symbol: true
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
).map((item) => {
|
||||||
|
return {
|
||||||
|
dataSource: item.dataSource,
|
||||||
|
date: item.Order?.[0]?.date ?? startDate,
|
||||||
|
symbol: item.symbol
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
@ -6,11 +6,11 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
import { isAfter, isBefore, parse } from 'date-fns';
|
import { isAfter, isBefore, parse } from 'date-fns';
|
||||||
|
|
||||||
import { DataProviderInterface } from '../../interfaces/data-provider.interface';
|
|
||||||
import {
|
import {
|
||||||
IDataProviderHistoricalResponse,
|
IDataProviderHistoricalResponse,
|
||||||
IDataProviderResponse
|
IDataProviderResponse
|
||||||
} from '../../interfaces/interfaces';
|
} from '../../interfaces/interfaces';
|
||||||
|
import { DataProviderInterface } from '../interfaces/data-provider.interface';
|
||||||
import { IAlphaVantageHistoricalResponse } from './interfaces/interfaces';
|
import { IAlphaVantageHistoricalResponse } from './interfaces/interfaces';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -84,6 +84,10 @@ export class AlphaVantageService implements DataProviderInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getName(): DataSource {
|
||||||
|
return DataSource.ALPHA_VANTAGE;
|
||||||
|
}
|
||||||
|
|
||||||
public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
|
public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
|
||||||
const result = await this.alphaVantage.data.search(aSymbol);
|
const result = await this.alphaVantage.data.search(aSymbol);
|
||||||
|
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
import { TrackinsightDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/trackinsight/trackinsight.service';
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
exports: ['DataEnhancers', TrackinsightDataEnhancerService],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
inject: [TrackinsightDataEnhancerService],
|
||||||
|
provide: 'DataEnhancers',
|
||||||
|
useFactory: (trackinsight) => [trackinsight]
|
||||||
|
},
|
||||||
|
TrackinsightDataEnhancerService
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class DataEnhancerModule {}
|
@ -0,0 +1,73 @@
|
|||||||
|
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
|
||||||
|
import { IDataProviderResponse } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
|
import bent from 'bent';
|
||||||
|
|
||||||
|
const getJSON = bent('json');
|
||||||
|
|
||||||
|
export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
||||||
|
private static baseUrl = 'https://data.trackinsight.com/holdings';
|
||||||
|
private static countries = require('countries-list/dist/countries.json');
|
||||||
|
private static sectorsMapping = {
|
||||||
|
'Consumer Discretionary': 'Consumer Cyclical',
|
||||||
|
'Consumer Defensive': 'Consumer Staples',
|
||||||
|
'Health Care': 'Healthcare',
|
||||||
|
'Information Technology': 'Technology'
|
||||||
|
};
|
||||||
|
|
||||||
|
public async enhance({
|
||||||
|
response,
|
||||||
|
symbol
|
||||||
|
}: {
|
||||||
|
response: IDataProviderResponse;
|
||||||
|
symbol: string;
|
||||||
|
}): Promise<IDataProviderResponse> {
|
||||||
|
if (
|
||||||
|
!(response.assetClass === 'EQUITY' && response.assetSubClass === 'ETF')
|
||||||
|
) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const holdings = await getJSON(
|
||||||
|
`${TrackinsightDataEnhancerService.baseUrl}/${symbol}.json`
|
||||||
|
).catch(() => {
|
||||||
|
return getJSON(
|
||||||
|
`${TrackinsightDataEnhancerService.baseUrl}/${
|
||||||
|
symbol.split('.')[0]
|
||||||
|
}.json`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.countries || response.countries.length === 0) {
|
||||||
|
response.countries = [];
|
||||||
|
for (const [name, value] of Object.entries<any>(holdings.countries)) {
|
||||||
|
let countryCode: string;
|
||||||
|
|
||||||
|
for (const [key, country] of Object.entries<any>(
|
||||||
|
TrackinsightDataEnhancerService.countries
|
||||||
|
)) {
|
||||||
|
if (country.name === name) {
|
||||||
|
countryCode = key;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response.countries.push({
|
||||||
|
code: countryCode,
|
||||||
|
weight: value.weight
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.sectors || response.sectors.length === 0) {
|
||||||
|
response.sectors = [];
|
||||||
|
for (const [name, value] of Object.entries<any>(holdings.sectors)) {
|
||||||
|
response.sectors.push({
|
||||||
|
name: TrackinsightDataEnhancerService.sectorsMapping[name] ?? name,
|
||||||
|
weight: value.weight
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(response);
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||||
|
import { CryptocurrencyModule } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.module';
|
||||||
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.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 { 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 { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
|
||||||
@ -9,14 +10,34 @@ import { AlphaVantageService } from './alpha-vantage/alpha-vantage.service';
|
|||||||
import { DataProviderService } from './data-provider.service';
|
import { DataProviderService } from './data-provider.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ConfigurationModule, PrismaModule],
|
imports: [ConfigurationModule, CryptocurrencyModule, PrismaModule],
|
||||||
providers: [
|
providers: [
|
||||||
AlphaVantageService,
|
AlphaVantageService,
|
||||||
DataProviderService,
|
DataProviderService,
|
||||||
GhostfolioScraperApiService,
|
GhostfolioScraperApiService,
|
||||||
RakutenRapidApiService,
|
RakutenRapidApiService,
|
||||||
|
YahooFinanceService,
|
||||||
|
{
|
||||||
|
inject: [
|
||||||
|
AlphaVantageService,
|
||||||
|
GhostfolioScraperApiService,
|
||||||
|
RakutenRapidApiService,
|
||||||
YahooFinanceService
|
YahooFinanceService
|
||||||
],
|
],
|
||||||
|
provide: 'DataProviderInterfaces',
|
||||||
|
useFactory: (
|
||||||
|
alphaVantageService,
|
||||||
|
ghostfolioScraperApiService,
|
||||||
|
rakutenRapidApiService,
|
||||||
|
yahooFinanceService
|
||||||
|
) => [
|
||||||
|
alphaVantageService,
|
||||||
|
ghostfolioScraperApiService,
|
||||||
|
rakutenRapidApiService,
|
||||||
|
yahooFinanceService
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
exports: [DataProviderService, GhostfolioScraperApiService]
|
exports: [DataProviderService, GhostfolioScraperApiService]
|
||||||
})
|
})
|
||||||
export class DataProviderModule {}
|
export class DataProviderModule {}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
|
import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
|
||||||
import {
|
import {
|
||||||
IDataGatheringItem,
|
IDataGatheringItem,
|
||||||
IDataProviderHistoricalResponse,
|
IDataProviderHistoricalResponse,
|
||||||
@ -8,30 +9,19 @@ import {
|
|||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||||
import { Granularity } from '@ghostfolio/common/types';
|
import { Granularity } from '@ghostfolio/common/types';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { DataSource, MarketData } from '@prisma/client';
|
import { DataSource, MarketData } from '@prisma/client';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
import { AlphaVantageService } from './alpha-vantage/alpha-vantage.service';
|
|
||||||
import { GhostfolioScraperApiService } from './ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
|
||||||
import { RakutenRapidApiService } from './rakuten-rapid-api/rakuten-rapid-api.service';
|
|
||||||
import {
|
|
||||||
YahooFinanceService,
|
|
||||||
convertToYahooFinanceSymbol
|
|
||||||
} from './yahoo-finance/yahoo-finance.service';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DataProviderService {
|
export class DataProviderService {
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly alphaVantageService: AlphaVantageService,
|
|
||||||
private readonly configurationService: ConfigurationService,
|
private readonly configurationService: ConfigurationService,
|
||||||
private readonly ghostfolioScraperApiService: GhostfolioScraperApiService,
|
@Inject('DataProviderInterfaces')
|
||||||
private readonly prismaService: PrismaService,
|
private readonly dataProviderInterfaces: DataProviderInterface[],
|
||||||
private readonly rakutenRapidApiService: RakutenRapidApiService,
|
private readonly prismaService: PrismaService
|
||||||
private readonly yahooFinanceService: YahooFinanceService
|
) {}
|
||||||
) {
|
|
||||||
this.rakutenRapidApiService?.setPrisma(this.prismaService);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async get(items: IDataGatheringItem[]): Promise<{
|
public async get(items: IDataGatheringItem[]): Promise<{
|
||||||
[symbol: string]: IDataProviderResponse;
|
[symbol: string]: IDataProviderResponse;
|
||||||
@ -41,27 +31,22 @@ export class DataProviderService {
|
|||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (item.dataSource === DataSource.ALPHA_VANTAGE) {
|
const dataProvider = this.getDataProvider(item.dataSource);
|
||||||
response[item.symbol] = (
|
response[item.symbol] = (await dataProvider.get([item.symbol]))[
|
||||||
await this.alphaVantageService.get([item.symbol])
|
item.symbol
|
||||||
)[item.symbol];
|
];
|
||||||
} else if (item.dataSource === DataSource.GHOSTFOLIO) {
|
|
||||||
response[item.symbol] = (
|
|
||||||
await this.ghostfolioScraperApiService.get([item.symbol])
|
|
||||||
)[item.symbol];
|
|
||||||
} else if (item.dataSource === DataSource.RAKUTEN) {
|
|
||||||
response[item.symbol] = (
|
|
||||||
await this.rakutenRapidApiService.get([item.symbol])
|
|
||||||
)[item.symbol];
|
|
||||||
} else if (item.dataSource === DataSource.YAHOO) {
|
|
||||||
response[item.symbol] = (
|
|
||||||
await this.yahooFinanceService.get([
|
|
||||||
convertToYahooFinanceSymbol(item.symbol)
|
|
||||||
])
|
|
||||||
)[item.symbol];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const promises = [];
|
||||||
|
for (const symbol of Object.keys(response)) {
|
||||||
|
const promise = Promise.resolve(response[symbol]);
|
||||||
|
promises.push(
|
||||||
|
promise.then((currentResponse) => (response[symbol] = currentResponse))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,6 +62,10 @@ export class DataProviderService {
|
|||||||
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
|
if (isEmpty(aItems)) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
const granularityQuery =
|
const granularityQuery =
|
||||||
aGranularity === 'month'
|
aGranularity === 'month'
|
||||||
? `AND (date_part('day', date) = 1 OR date >= TIMESTAMP 'yesterday')`
|
? `AND (date_part('day', date) = 1 OR date >= TIMESTAMP 'yesterday')`
|
||||||
@ -98,11 +87,13 @@ export class DataProviderService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const queryRaw = `SELECT * FROM "MarketData" WHERE "dataSource" IN ('${dataSources.join(
|
const queryRaw = `SELECT *
|
||||||
|
FROM "MarketData"
|
||||||
|
WHERE "dataSource" IN ('${dataSources.join(`','`)}')
|
||||||
|
AND "symbol" IN ('${symbols.join(
|
||||||
`','`
|
`','`
|
||||||
)}') AND "symbol" IN ('${symbols.join(
|
)}') ${granularityQuery} ${rangeQuery}
|
||||||
`','`
|
ORDER BY date;`;
|
||||||
)}') ${granularityQuery} ${rangeQuery} ORDER BY date;`;
|
|
||||||
|
|
||||||
const marketDataByGranularity: MarketData[] =
|
const marketDataByGranularity: MarketData[] =
|
||||||
await this.prismaService.$queryRaw(queryRaw);
|
await this.prismaService.$queryRaw(queryRaw);
|
||||||
@ -184,18 +175,16 @@ export class DataProviderService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getPrimaryDataSource(): DataSource {
|
||||||
|
return DataSource[this.configurationService.get('DATA_SOURCES')[0]];
|
||||||
|
}
|
||||||
|
|
||||||
private getDataProvider(providerName: DataSource) {
|
private getDataProvider(providerName: DataSource) {
|
||||||
switch (providerName) {
|
for (const dataProviderInterface of this.dataProviderInterfaces) {
|
||||||
case DataSource.ALPHA_VANTAGE:
|
if (dataProviderInterface.getName() === providerName) {
|
||||||
return this.alphaVantageService;
|
return dataProviderInterface;
|
||||||
case DataSource.GHOSTFOLIO:
|
}
|
||||||
return this.ghostfolioScraperApiService;
|
}
|
||||||
case DataSource.RAKUTEN:
|
|
||||||
return this.rakutenRapidApiService;
|
|
||||||
case DataSource.YAHOO:
|
|
||||||
return this.yahooFinanceService;
|
|
||||||
default:
|
|
||||||
throw new Error('No data provider has been found.');
|
throw new Error('No data provider has been found.');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -12,13 +12,13 @@ import * as bent from 'bent';
|
|||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
import { DataProviderInterface } from '../../interfaces/data-provider.interface';
|
|
||||||
import {
|
import {
|
||||||
IDataGatheringItem,
|
IDataGatheringItem,
|
||||||
IDataProviderHistoricalResponse,
|
IDataProviderHistoricalResponse,
|
||||||
IDataProviderResponse,
|
IDataProviderResponse,
|
||||||
MarketState
|
MarketState
|
||||||
} from '../../interfaces/interfaces';
|
} from '../../interfaces/interfaces';
|
||||||
|
import { DataProviderInterface } from '../interfaces/data-provider.interface';
|
||||||
import { ScraperConfig } from './interfaces/scraper-config.interface';
|
import { ScraperConfig } from './interfaces/scraper-config.interface';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -144,6 +144,10 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getName(): DataSource {
|
||||||
|
return DataSource.GHOSTFOLIO;
|
||||||
|
}
|
||||||
|
|
||||||
public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
|
public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
|
||||||
return { items: [] };
|
return { items: [] };
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import { Currency } from '@prisma/client';
|
|
||||||
|
|
||||||
export interface ScraperConfig {
|
export interface ScraperConfig {
|
||||||
currency: Currency;
|
currency: string;
|
||||||
selector: string;
|
selector: string;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
import { IDataProviderResponse } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
|
|
||||||
|
export interface DataEnhancerInterface {
|
||||||
|
enhance({
|
||||||
|
response,
|
||||||
|
symbol
|
||||||
|
}: {
|
||||||
|
response: IDataProviderResponse;
|
||||||
|
symbol: string;
|
||||||
|
}): Promise<IDataProviderResponse>;
|
||||||
|
}
|
@ -1,10 +1,11 @@
|
|||||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||||
import { Granularity } from '@ghostfolio/common/types';
|
import { Granularity } from '@ghostfolio/common/types';
|
||||||
|
import { DataSource } from '@prisma/client';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IDataProviderHistoricalResponse,
|
IDataProviderHistoricalResponse,
|
||||||
IDataProviderResponse
|
IDataProviderResponse
|
||||||
} from './interfaces';
|
} from '../../interfaces/interfaces';
|
||||||
|
|
||||||
export interface DataProviderInterface {
|
export interface DataProviderInterface {
|
||||||
canHandle(symbol: string): boolean;
|
canHandle(symbol: string): boolean;
|
||||||
@ -20,5 +21,7 @@ export interface DataProviderInterface {
|
|||||||
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
getName(): DataSource;
|
||||||
|
|
||||||
search(aSymbol: string): Promise<{ items: LookupItem[] }>;
|
search(aSymbol: string): Promise<{ items: LookupItem[] }>;
|
||||||
}
|
}
|
@ -14,21 +14,20 @@ import { DataSource } from '@prisma/client';
|
|||||||
import * as bent from 'bent';
|
import * as bent from 'bent';
|
||||||
import { format, subMonths, subWeeks, subYears } from 'date-fns';
|
import { format, subMonths, subWeeks, subYears } from 'date-fns';
|
||||||
|
|
||||||
import { DataProviderInterface } from '../../interfaces/data-provider.interface';
|
|
||||||
import {
|
import {
|
||||||
IDataProviderHistoricalResponse,
|
IDataProviderHistoricalResponse,
|
||||||
IDataProviderResponse,
|
IDataProviderResponse,
|
||||||
MarketState
|
MarketState
|
||||||
} from '../../interfaces/interfaces';
|
} from '../../interfaces/interfaces';
|
||||||
|
import { DataProviderInterface } from '../interfaces/data-provider.interface';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RakutenRapidApiService implements DataProviderInterface {
|
export class RakutenRapidApiService implements DataProviderInterface {
|
||||||
public static FEAR_AND_GREED_INDEX_NAME = 'Fear & Greed Index';
|
public static FEAR_AND_GREED_INDEX_NAME = 'Fear & Greed Index';
|
||||||
|
|
||||||
private prismaService: PrismaService;
|
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly configurationService: ConfigurationService
|
private readonly configurationService: ConfigurationService,
|
||||||
|
private readonly prismaService: PrismaService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public canHandle(symbol: string) {
|
public canHandle(symbol: string) {
|
||||||
@ -134,12 +133,12 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
|
public getName(): DataSource {
|
||||||
return { items: [] };
|
return DataSource.RAKUTEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setPrisma(aPrismaService: PrismaService) {
|
public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
|
||||||
this.prismaService = aPrismaService;
|
return { items: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getFearAndGreedIndex(): Promise<{
|
private async getFearAndGreedIndex(): Promise<{
|
||||||
|
@ -1,31 +1,22 @@
|
|||||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||||
|
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
|
||||||
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||||
import {
|
import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper';
|
||||||
DATE_FORMAT,
|
|
||||||
isCrypto,
|
|
||||||
isCurrency,
|
|
||||||
parseCurrency
|
|
||||||
} from '@ghostfolio/common/helper';
|
|
||||||
import { Granularity } from '@ghostfolio/common/types';
|
import { Granularity } from '@ghostfolio/common/types';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import {
|
import { AssetClass, AssetSubClass, DataSource } from '@prisma/client';
|
||||||
AssetClass,
|
|
||||||
AssetSubClass,
|
|
||||||
Currency,
|
|
||||||
DataSource
|
|
||||||
} from '@prisma/client';
|
|
||||||
import * as bent from 'bent';
|
import * as bent from 'bent';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import { countries } from 'countries-list';
|
import { countries } from 'countries-list';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import * as yahooFinance from 'yahoo-finance';
|
import * as yahooFinance from 'yahoo-finance';
|
||||||
|
|
||||||
import { DataProviderInterface } from '../../interfaces/data-provider.interface';
|
|
||||||
import {
|
import {
|
||||||
IDataProviderHistoricalResponse,
|
IDataProviderHistoricalResponse,
|
||||||
IDataProviderResponse,
|
IDataProviderResponse,
|
||||||
MarketState
|
MarketState
|
||||||
} from '../../interfaces/interfaces';
|
} from '../../interfaces/interfaces';
|
||||||
|
import { DataProviderInterface } from '../interfaces/data-provider.interface';
|
||||||
import {
|
import {
|
||||||
IYahooFinanceHistoricalResponse,
|
IYahooFinanceHistoricalResponse,
|
||||||
IYahooFinancePrice,
|
IYahooFinancePrice,
|
||||||
@ -36,18 +27,23 @@ import {
|
|||||||
export class YahooFinanceService implements DataProviderInterface {
|
export class YahooFinanceService implements DataProviderInterface {
|
||||||
private yahooFinanceHostname = 'https://query1.finance.yahoo.com';
|
private yahooFinanceHostname = 'https://query1.finance.yahoo.com';
|
||||||
|
|
||||||
public constructor() {}
|
public constructor(
|
||||||
|
private readonly cryptocurrencyService: CryptocurrencyService
|
||||||
|
) {}
|
||||||
|
|
||||||
public canHandle(symbol: string) {
|
public canHandle(symbol: string) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async get(
|
public async get(
|
||||||
aYahooFinanceSymbols: string[]
|
aSymbols: string[]
|
||||||
): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
||||||
if (aYahooFinanceSymbols.length <= 0) {
|
if (aSymbols.length <= 0) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
const yahooFinanceSymbols = aSymbols.map((symbol) =>
|
||||||
|
this.convertToYahooFinanceSymbol(symbol)
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response: { [symbol: string]: IDataProviderResponse } = {};
|
const response: { [symbol: string]: IDataProviderResponse } = {};
|
||||||
@ -56,23 +52,24 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
[symbol: string]: IYahooFinanceQuoteResponse;
|
[symbol: string]: IYahooFinanceQuoteResponse;
|
||||||
} = await yahooFinance.quote({
|
} = await yahooFinance.quote({
|
||||||
modules: ['price', 'summaryProfile'],
|
modules: ['price', 'summaryProfile'],
|
||||||
symbols: aYahooFinanceSymbols
|
symbols: yahooFinanceSymbols
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const [yahooFinanceSymbol, value] of Object.entries(data)) {
|
for (const [yahooFinanceSymbol, value] of Object.entries(data)) {
|
||||||
// Convert symbols back
|
// Convert symbols back
|
||||||
const symbol = convertFromYahooFinanceSymbol(yahooFinanceSymbol);
|
const symbol = this.convertFromYahooFinanceSymbol(yahooFinanceSymbol);
|
||||||
|
|
||||||
const { assetClass, assetSubClass } = this.parseAssetClass(value.price);
|
const { assetClass, assetSubClass } = this.parseAssetClass(value.price);
|
||||||
|
|
||||||
response[symbol] = {
|
response[symbol] = {
|
||||||
assetClass,
|
assetClass,
|
||||||
assetSubClass,
|
assetSubClass,
|
||||||
currency: parseCurrency(value.price?.currency),
|
currency: value.price?.currency,
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
exchange: this.parseExchange(value.price?.exchangeName),
|
exchange: this.parseExchange(value.price?.exchangeName),
|
||||||
marketState:
|
marketState:
|
||||||
value.price?.marketState === 'REGULAR' || isCrypto(symbol)
|
value.price?.marketState === 'REGULAR' ||
|
||||||
|
this.cryptocurrencyService.isCrypto(symbol)
|
||||||
? MarketState.open
|
? MarketState.open
|
||||||
: MarketState.closed,
|
: MarketState.closed,
|
||||||
marketPrice: value.price?.regularMarketPrice || 0,
|
marketPrice: value.price?.regularMarketPrice || 0,
|
||||||
@ -81,7 +78,7 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
|
|
||||||
if (value.price?.currency === 'GBp') {
|
if (value.price?.currency === 'GBp') {
|
||||||
// Convert GBp (pence) to GBP
|
// Convert GBp (pence) to GBP
|
||||||
response[symbol].currency = Currency.GBP;
|
response[symbol].currency = 'GBP';
|
||||||
response[symbol].marketPrice = new Big(
|
response[symbol].marketPrice = new Big(
|
||||||
value.price?.regularMarketPrice ?? 0
|
value.price?.regularMarketPrice ?? 0
|
||||||
)
|
)
|
||||||
@ -103,6 +100,12 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
response[symbol].countries = [{ code, weight: 1 }];
|
response[symbol].countries = [{ code, weight: 1 }];
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
|
if (value.summaryProfile?.sector) {
|
||||||
|
response[symbol].sectors = [
|
||||||
|
{ name: value.summaryProfile?.sector, weight: 1 }
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add url if available
|
// Add url if available
|
||||||
@ -133,7 +136,7 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const yahooFinanceSymbols = aSymbols.map((symbol) => {
|
const yahooFinanceSymbols = aSymbols.map((symbol) => {
|
||||||
return convertToYahooFinanceSymbol(symbol);
|
return this.convertToYahooFinanceSymbol(symbol);
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -153,7 +156,7 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
historicalData
|
historicalData
|
||||||
)) {
|
)) {
|
||||||
// Convert symbols back
|
// Convert symbols back
|
||||||
const symbol = convertFromYahooFinanceSymbol(yahooFinanceSymbol);
|
const symbol = this.convertFromYahooFinanceSymbol(yahooFinanceSymbol);
|
||||||
response[symbol] = {};
|
response[symbol] = {};
|
||||||
|
|
||||||
timeSeries.forEach((timeSerie) => {
|
timeSeries.forEach((timeSerie) => {
|
||||||
@ -172,6 +175,10 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getName(): DataSource {
|
||||||
|
return DataSource.YAHOO;
|
||||||
|
}
|
||||||
|
|
||||||
public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
|
public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
|
||||||
const items: LookupItem[] = [];
|
const items: LookupItem[] = [];
|
||||||
|
|
||||||
@ -200,7 +207,7 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
.filter(({ quoteType, symbol }) => {
|
.filter(({ quoteType, symbol }) => {
|
||||||
if (quoteType === 'CRYPTOCURRENCY') {
|
if (quoteType === 'CRYPTOCURRENCY') {
|
||||||
// Only allow cryptocurrencies in USD
|
// Only allow cryptocurrencies in USD
|
||||||
return symbol.includes(Currency.USD);
|
return symbol.includes('USD');
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -224,6 +231,43 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
return { items };
|
return { items };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private convertFromYahooFinanceSymbol(aYahooFinanceSymbol: string) {
|
||||||
|
const symbol = aYahooFinanceSymbol.replace('-', '');
|
||||||
|
return symbol.replace('=X', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a symbol to a Yahoo Finance symbol
|
||||||
|
*
|
||||||
|
* Currency: USDCHF -> USDCHF=X
|
||||||
|
* Cryptocurrency: BTCUSD -> BTC-USD
|
||||||
|
* DOGEUSD -> DOGE-USD
|
||||||
|
* SOL1USD -> SOL1-USD
|
||||||
|
*/
|
||||||
|
private convertToYahooFinanceSymbol(aSymbol: string) {
|
||||||
|
if (
|
||||||
|
(aSymbol.includes('CHF') ||
|
||||||
|
aSymbol.includes('EUR') ||
|
||||||
|
aSymbol.includes('USD')) &&
|
||||||
|
aSymbol.length >= 6
|
||||||
|
) {
|
||||||
|
if (isCurrency(aSymbol.substring(0, aSymbol.length - 3))) {
|
||||||
|
return `${aSymbol}=X`;
|
||||||
|
} else if (
|
||||||
|
this.cryptocurrencyService.isCrypto(aSymbol) ||
|
||||||
|
this.cryptocurrencyService.isCrypto(aSymbol.replace('1', ''))
|
||||||
|
) {
|
||||||
|
// Add a dash before the last three characters
|
||||||
|
// BTCUSD -> BTC-USD
|
||||||
|
// DOGEUSD -> DOGE-USD
|
||||||
|
// SOL1USD -> SOL1-USD
|
||||||
|
return aSymbol.replace('USD', '-USD');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return aSymbol;
|
||||||
|
}
|
||||||
|
|
||||||
private parseAssetClass(aPrice: IYahooFinancePrice): {
|
private parseAssetClass(aPrice: IYahooFinancePrice): {
|
||||||
assetClass: AssetClass;
|
assetClass: AssetClass;
|
||||||
assetSubClass: AssetSubClass;
|
assetSubClass: AssetSubClass;
|
||||||
@ -257,31 +301,3 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
return aString;
|
return aString;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const convertFromYahooFinanceSymbol = (aYahooFinanceSymbol: string) => {
|
|
||||||
const symbol = aYahooFinanceSymbol.replace('-', '');
|
|
||||||
return symbol.replace('=X', '');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a symbol to a Yahoo Finance symbol
|
|
||||||
*
|
|
||||||
* Currency: USDCHF=X
|
|
||||||
* Cryptocurrency: BTC-USD
|
|
||||||
*/
|
|
||||||
export const convertToYahooFinanceSymbol = (aSymbol: string) => {
|
|
||||||
if (isCurrency(aSymbol)) {
|
|
||||||
if (isCrypto(aSymbol)) {
|
|
||||||
// Add a dash before the last three characters
|
|
||||||
// BTCUSD -> BTC-USD
|
|
||||||
// DOGEUSD -> DOGE-USD
|
|
||||||
return `${aSymbol.substring(0, aSymbol.length - 3)}-${aSymbol.substring(
|
|
||||||
aSymbol.length - 3
|
|
||||||
)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${aSymbol}=X`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return aSymbol;
|
|
||||||
};
|
|
||||||
|
@ -2,8 +2,10 @@ import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-
|
|||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { PrismaModule } from './prisma.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [DataProviderModule],
|
imports: [DataProviderModule, PrismaModule],
|
||||||
providers: [ExchangeRateDataService],
|
providers: [ExchangeRateDataService],
|
||||||
exports: [ExchangeRateDataService]
|
exports: [ExchangeRateDataService]
|
||||||
})
|
})
|
||||||
|
@ -1,27 +1,45 @@
|
|||||||
import { currencyPairs } from '@ghostfolio/common/config';
|
import { baseCurrency } from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Currency, DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { isEmpty, isNumber } from 'lodash';
|
import { isEmpty, isNumber, uniq } from 'lodash';
|
||||||
|
|
||||||
import { DataProviderService } from './data-provider/data-provider.service';
|
import { DataProviderService } from './data-provider/data-provider.service';
|
||||||
import { IDataGatheringItem } from './interfaces/interfaces';
|
import { IDataGatheringItem } from './interfaces/interfaces';
|
||||||
|
import { PrismaService } from './prisma.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ExchangeRateDataService {
|
export class ExchangeRateDataService {
|
||||||
|
private currencies: string[] = [];
|
||||||
private currencyPairs: IDataGatheringItem[] = [];
|
private currencyPairs: IDataGatheringItem[] = [];
|
||||||
private exchangeRates: { [currencyPair: string]: number } = {};
|
private exchangeRates: { [currencyPair: string]: number } = {};
|
||||||
|
|
||||||
public constructor(private dataProviderService: DataProviderService) {
|
public constructor(
|
||||||
|
private readonly dataProviderService: DataProviderService,
|
||||||
|
private readonly prismaService: PrismaService
|
||||||
|
) {
|
||||||
this.initialize();
|
this.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getCurrencies() {
|
||||||
|
return this.currencies?.length > 0 ? this.currencies : [baseCurrency];
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCurrencyPairs() {
|
||||||
|
return this.currencyPairs;
|
||||||
|
}
|
||||||
|
|
||||||
public async initialize() {
|
public async initialize() {
|
||||||
|
this.currencies = await this.prepareCurrencies();
|
||||||
this.currencyPairs = [];
|
this.currencyPairs = [];
|
||||||
this.exchangeRates = {};
|
this.exchangeRates = {};
|
||||||
|
|
||||||
for (const { currency1, currency2, dataSource } of currencyPairs) {
|
for (const {
|
||||||
|
currency1,
|
||||||
|
currency2,
|
||||||
|
dataSource
|
||||||
|
} of this.prepareCurrencyPairs(this.currencies)) {
|
||||||
this.addCurrencyPairs({ currency1, currency2, dataSource });
|
this.addCurrencyPairs({ currency1, currency2, dataSource });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,8 +95,8 @@ export class ExchangeRateDataService {
|
|||||||
if (!this.exchangeRates[symbol]) {
|
if (!this.exchangeRates[symbol]) {
|
||||||
// Not found, calculate indirectly via USD
|
// Not found, calculate indirectly via USD
|
||||||
this.exchangeRates[symbol] =
|
this.exchangeRates[symbol] =
|
||||||
resultExtended[`${currency1}${Currency.USD}`]?.[date]?.marketPrice *
|
resultExtended[`${currency1}${'USD'}`]?.[date]?.marketPrice *
|
||||||
resultExtended[`${Currency.USD}${currency2}`]?.[date]?.marketPrice;
|
resultExtended[`${'USD'}${currency2}`]?.[date]?.marketPrice;
|
||||||
|
|
||||||
// Calculate the opposite direction
|
// Calculate the opposite direction
|
||||||
this.exchangeRates[`${currency2}${currency1}`] =
|
this.exchangeRates[`${currency2}${currency1}`] =
|
||||||
@ -89,10 +107,14 @@ export class ExchangeRateDataService {
|
|||||||
|
|
||||||
public toCurrency(
|
public toCurrency(
|
||||||
aValue: number,
|
aValue: number,
|
||||||
aFromCurrency: Currency,
|
aFromCurrency: string,
|
||||||
aToCurrency: Currency
|
aToCurrency: string
|
||||||
) {
|
) {
|
||||||
if (isNaN(this.exchangeRates[`${Currency.USD}${Currency.CHF}`])) {
|
const hasNaN = Object.values(this.exchangeRates).some((exchangeRate) => {
|
||||||
|
return isNaN(exchangeRate);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasNaN) {
|
||||||
// Reinitialize if data is not loaded correctly
|
// Reinitialize if data is not loaded correctly
|
||||||
this.initialize();
|
this.initialize();
|
||||||
}
|
}
|
||||||
@ -104,8 +126,8 @@ export class ExchangeRateDataService {
|
|||||||
factor = this.exchangeRates[`${aFromCurrency}${aToCurrency}`];
|
factor = this.exchangeRates[`${aFromCurrency}${aToCurrency}`];
|
||||||
} else {
|
} else {
|
||||||
// Calculate indirectly via USD
|
// Calculate indirectly via USD
|
||||||
const factor1 = this.exchangeRates[`${aFromCurrency}${Currency.USD}`];
|
const factor1 = this.exchangeRates[`${aFromCurrency}${'USD'}`];
|
||||||
const factor2 = this.exchangeRates[`${Currency.USD}${aToCurrency}`];
|
const factor2 = this.exchangeRates[`${'USD'}${aToCurrency}`];
|
||||||
|
|
||||||
factor = factor1 * factor2;
|
factor = factor1 * factor2;
|
||||||
|
|
||||||
@ -113,7 +135,7 @@ export class ExchangeRateDataService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNumber(factor)) {
|
if (isNumber(factor) && !isNaN(factor)) {
|
||||||
return factor * aValue;
|
return factor * aValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,8 +151,8 @@ export class ExchangeRateDataService {
|
|||||||
currency2,
|
currency2,
|
||||||
dataSource
|
dataSource
|
||||||
}: {
|
}: {
|
||||||
currency1: Currency;
|
currency1: string;
|
||||||
currency2: Currency;
|
currency2: string;
|
||||||
dataSource: DataSource;
|
dataSource: DataSource;
|
||||||
}) {
|
}) {
|
||||||
this.currencyPairs.push({
|
this.currencyPairs.push({
|
||||||
@ -142,4 +164,55 @@ export class ExchangeRateDataService {
|
|||||||
symbol: `${currency2}${currency1}`
|
symbol: `${currency2}${currency1}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async prepareCurrencies(): Promise<string[]> {
|
||||||
|
const currencies: string[] = [];
|
||||||
|
|
||||||
|
(
|
||||||
|
await this.prismaService.account.findMany({
|
||||||
|
distinct: ['currency'],
|
||||||
|
orderBy: [{ currency: 'asc' }],
|
||||||
|
select: { currency: true }
|
||||||
|
})
|
||||||
|
).forEach((account) => {
|
||||||
|
currencies.push(account.currency);
|
||||||
|
});
|
||||||
|
|
||||||
|
(
|
||||||
|
await this.prismaService.settings.findMany({
|
||||||
|
distinct: ['currency'],
|
||||||
|
orderBy: [{ currency: 'asc' }],
|
||||||
|
select: { currency: true }
|
||||||
|
})
|
||||||
|
).forEach((userSettings) => {
|
||||||
|
currencies.push(userSettings.currency);
|
||||||
|
});
|
||||||
|
|
||||||
|
(
|
||||||
|
await this.prismaService.symbolProfile.findMany({
|
||||||
|
distinct: ['currency'],
|
||||||
|
orderBy: [{ currency: 'asc' }],
|
||||||
|
select: { currency: true }
|
||||||
|
})
|
||||||
|
).forEach((symbolProfile) => {
|
||||||
|
currencies.push(symbolProfile.currency);
|
||||||
|
});
|
||||||
|
|
||||||
|
return uniq(currencies).sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
private prepareCurrencyPairs(aCurrencies: string[]) {
|
||||||
|
return aCurrencies
|
||||||
|
.filter((currency) => {
|
||||||
|
return currency !== baseCurrency;
|
||||||
|
})
|
||||||
|
.map((currency) => {
|
||||||
|
return {
|
||||||
|
currency1: baseCurrency,
|
||||||
|
currency2: currency,
|
||||||
|
dataSource: this.dataProviderService.getPrimaryDataSource(),
|
||||||
|
symbol: `${baseCurrency}${currency}`
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ import {
|
|||||||
Account,
|
Account,
|
||||||
AssetClass,
|
AssetClass,
|
||||||
AssetSubClass,
|
AssetSubClass,
|
||||||
Currency,
|
|
||||||
DataSource,
|
DataSource,
|
||||||
SymbolProfile
|
SymbolProfile
|
||||||
} from '@prisma/client';
|
} from '@prisma/client';
|
||||||
@ -17,7 +16,7 @@ export const MarketState = {
|
|||||||
|
|
||||||
export interface IOrder {
|
export interface IOrder {
|
||||||
account: Account;
|
account: Account;
|
||||||
currency: Currency;
|
currency: string;
|
||||||
date: string;
|
date: string;
|
||||||
fee: number;
|
fee: number;
|
||||||
id?: string;
|
id?: string;
|
||||||
@ -38,7 +37,7 @@ export interface IDataProviderResponse {
|
|||||||
assetClass?: AssetClass;
|
assetClass?: AssetClass;
|
||||||
assetSubClass?: AssetSubClass;
|
assetSubClass?: AssetSubClass;
|
||||||
countries?: { code: string; weight: number }[];
|
countries?: { code: string; weight: number }[];
|
||||||
currency: Currency;
|
currency: string;
|
||||||
dataSource: DataSource;
|
dataSource: DataSource;
|
||||||
exchange?: string;
|
exchange?: string;
|
||||||
marketChange?: number;
|
marketChange?: number;
|
||||||
@ -46,6 +45,7 @@ export interface IDataProviderResponse {
|
|||||||
marketPrice: number;
|
marketPrice: number;
|
||||||
marketState: MarketState;
|
marketState: MarketState;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
sectors?: { name: string; weight: number }[];
|
||||||
url?: string;
|
url?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,17 +1,12 @@
|
|||||||
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
||||||
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
||||||
import {
|
import { AssetClass, AssetSubClass, DataSource } from '@prisma/client';
|
||||||
AssetClass,
|
|
||||||
AssetSubClass,
|
|
||||||
Currency,
|
|
||||||
DataSource
|
|
||||||
} from '@prisma/client';
|
|
||||||
|
|
||||||
export interface EnhancedSymbolProfile {
|
export interface EnhancedSymbolProfile {
|
||||||
assetClass: AssetClass;
|
assetClass: AssetClass;
|
||||||
assetSubClass: AssetSubClass;
|
assetSubClass: AssetSubClass;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
currency: Currency | null;
|
currency: string | null;
|
||||||
dataSource: DataSource;
|
dataSource: DataSource;
|
||||||
id: string;
|
id: string;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
|
@ -52,6 +52,13 @@ const routes: Routes = [
|
|||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./pages/home/home-page.module').then((m) => m.HomePageModule)
|
import('./pages/home/home-page.module').then((m) => m.HomePageModule)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'p',
|
||||||
|
loadChildren: () =>
|
||||||
|
import('./pages/public/public-page.module').then(
|
||||||
|
(m) => m.PublicPageModule
|
||||||
|
)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'portfolio',
|
path: 'portfolio',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<header>
|
<header>
|
||||||
<gf-header
|
<gf-header
|
||||||
class="position-fixed px-2 w-100"
|
class="position-fixed w-100"
|
||||||
[currentRoute]="currentRoute"
|
[currentRoute]="currentRoute"
|
||||||
[info]="info"
|
[info]="info"
|
||||||
[user]="user"
|
[user]="user"
|
||||||
@ -28,7 +28,10 @@
|
|||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer *ngIf="!user" class="footer d-flex justify-content-center w-100">
|
<footer
|
||||||
|
*ngIf="currentRoute === 'start'"
|
||||||
|
class="footer d-flex justify-content-center w-100"
|
||||||
|
>
|
||||||
<div class="container text-center">
|
<div class="container text-center">
|
||||||
<div>
|
<div>
|
||||||
© {{ currentYear }} <a href="https://ghostfol.io">Ghostfolio</a>
|
© {{ currentYear }} <a href="https://ghostfol.io">Ghostfolio</a>
|
||||||
|
@ -1,18 +1,52 @@
|
|||||||
<table class="gf-table w-100" mat-table [dataSource]="dataSource">
|
<table class="gf-table w-100" mat-table [dataSource]="dataSource">
|
||||||
<ng-container matColumnDef="granteeAlias">
|
<ng-container matColumnDef="granteeAlias">
|
||||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>User</th>
|
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Grantee</th>
|
||||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
{{ element.granteeAlias }}
|
{{ element.granteeAlias }}
|
||||||
</td></ng-container
|
</td>
|
||||||
>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="type">
|
<ng-container matColumnDef="type">
|
||||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Type</th>
|
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Type</th>
|
||||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell>
|
||||||
|
<ng-container>
|
||||||
<ion-icon class="mr-1" name="lock-closed-outline"></ion-icon>
|
<ion-icon class="mr-1" name="lock-closed-outline"></ion-icon>
|
||||||
Restricted View
|
Restricted View
|
||||||
</td></ng-container
|
</ng-container>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="details">
|
||||||
|
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Details</th>
|
||||||
|
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell>
|
||||||
|
<ng-container *ngIf="element.type === 'PUBLIC'">
|
||||||
|
<ion-icon class="mr-1" name="link-outline"></ion-icon>
|
||||||
|
<a href="{{ baseUrl }}/p/{{ element.id }}" target="_blank"
|
||||||
|
>{{ baseUrl }}/p/{{ element.id }}</a
|
||||||
>
|
>
|
||||||
|
</ng-container>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="actions">
|
||||||
|
<th *matHeaderCellDef class="px-1 text-center" mat-header-cell></th>
|
||||||
|
|
||||||
|
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
||||||
|
<button
|
||||||
|
class="mx-1 no-min-width px-2"
|
||||||
|
mat-button
|
||||||
|
[matMenuTriggerFor]="transactionMenu"
|
||||||
|
(click)="$event.stopPropagation()"
|
||||||
|
>
|
||||||
|
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||||
|
</button>
|
||||||
|
<mat-menu #transactionMenu="matMenu" xPosition="before">
|
||||||
|
<button i18n mat-menu-item (click)="onDeleteAccess(element.id)">
|
||||||
|
Revoke
|
||||||
|
</button>
|
||||||
|
</mat-menu>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||||
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
||||||
|
@ -2,4 +2,12 @@
|
|||||||
|
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: rgba(var(--palette-primary-500), 1);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: rgba(var(--palette-primary-300), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
|
EventEmitter,
|
||||||
Input,
|
Input,
|
||||||
OnChanges,
|
OnChanges,
|
||||||
OnInit
|
OnInit,
|
||||||
|
Output
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MatTableDataSource } from '@angular/material/table';
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
import { Access } from '@ghostfolio/common/interfaces';
|
import { Access } from '@ghostfolio/common/interfaces';
|
||||||
@ -16,17 +18,37 @@ import { Access } from '@ghostfolio/common/interfaces';
|
|||||||
})
|
})
|
||||||
export class AccessTableComponent implements OnChanges, OnInit {
|
export class AccessTableComponent implements OnChanges, OnInit {
|
||||||
@Input() accesses: Access[];
|
@Input() accesses: Access[];
|
||||||
|
@Input() showActions: boolean;
|
||||||
|
|
||||||
|
@Output() accessDeleted = new EventEmitter<string>();
|
||||||
|
|
||||||
|
public baseUrl = window.location.origin;
|
||||||
public dataSource: MatTableDataSource<Access>;
|
public dataSource: MatTableDataSource<Access>;
|
||||||
public displayedColumns = ['granteeAlias', 'type'];
|
public displayedColumns = [];
|
||||||
|
|
||||||
public constructor() {}
|
public constructor() {}
|
||||||
|
|
||||||
public ngOnInit() {}
|
public ngOnInit() {}
|
||||||
|
|
||||||
public ngOnChanges() {
|
public ngOnChanges() {
|
||||||
|
this.displayedColumns = ['granteeAlias', 'type', 'details'];
|
||||||
|
|
||||||
|
if (this.showActions) {
|
||||||
|
this.displayedColumns.push('actions');
|
||||||
|
}
|
||||||
|
|
||||||
if (this.accesses) {
|
if (this.accesses) {
|
||||||
this.dataSource = new MatTableDataSource(this.accesses);
|
this.dataSource = new MatTableDataSource(this.accesses);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onDeleteAccess(aId: string) {
|
||||||
|
const confirmation = confirm(
|
||||||
|
'Do you really want to revoke this granted access?'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmation) {
|
||||||
|
this.accessDeleted.emit(aId);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
import { MatTableModule } from '@angular/material/table';
|
import { MatTableModule } from '@angular/material/table';
|
||||||
|
|
||||||
import { AccessTableComponent } from './access-table.component';
|
import { AccessTableComponent } from './access-table.component';
|
||||||
@ -7,7 +9,7 @@ import { AccessTableComponent } from './access-table.component';
|
|||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [AccessTableComponent],
|
declarations: [AccessTableComponent],
|
||||||
exports: [AccessTableComponent],
|
exports: [AccessTableComponent],
|
||||||
imports: [CommonModule, MatTableModule],
|
imports: [CommonModule, MatButtonModule, MatMenuModule, MatTableModule],
|
||||||
providers: [],
|
providers: [],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
|
@ -51,7 +51,7 @@
|
|||||||
|
|
||||||
<ng-container matColumnDef="balance">
|
<ng-container matColumnDef="balance">
|
||||||
<th *matHeaderCellDef class="px-1 text-right" i18n mat-header-cell>
|
<th *matHeaderCellDef class="px-1 text-right" i18n mat-header-cell>
|
||||||
Balance
|
Cash Balance
|
||||||
</th>
|
</th>
|
||||||
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
||||||
<gf-value
|
<gf-value
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
<mat-toolbar class="p-0">
|
<mat-toolbar class="px-2">
|
||||||
<ng-container *ngIf="user">
|
<ng-container *ngIf="user">
|
||||||
<a [routerLink]="['/']" class="no-min-width px-2" mat-button>
|
<a
|
||||||
|
[routerLink]="['/']"
|
||||||
|
class="align-items-center d-flex h-100 mx-2 no-min-width px-2 rounded-0"
|
||||||
|
mat-button
|
||||||
|
>
|
||||||
<gf-logo></gf-logo>
|
<gf-logo></gf-logo>
|
||||||
</a>
|
</a>
|
||||||
<span class="spacer"></span>
|
<span class="spacer"></span>
|
||||||
@ -17,7 +21,6 @@
|
|||||||
>Overview</a
|
>Overview</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
*ngIf="user?.settings?.viewMode === 'DEFAULT'"
|
|
||||||
class="d-none d-sm-block mx-1"
|
class="d-none d-sm-block mx-1"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
@ -28,18 +31,6 @@
|
|||||||
[routerLink]="['/portfolio']"
|
[routerLink]="['/portfolio']"
|
||||||
>Portfolio</a
|
>Portfolio</a
|
||||||
>
|
>
|
||||||
<a
|
|
||||||
*ngIf="user?.settings?.viewMode !== 'DEFAULT'"
|
|
||||||
class="d-none d-sm-block mx-1"
|
|
||||||
i18n
|
|
||||||
mat-flat-button
|
|
||||||
[ngClass]="{
|
|
||||||
'font-weight-bold': currentRoute === 'portfolio',
|
|
||||||
'text-decoration-underline': currentRoute === 'portfolio'
|
|
||||||
}"
|
|
||||||
[routerLink]="['/portfolio', 'transactions']"
|
|
||||||
>Transactions</a
|
|
||||||
>
|
|
||||||
<a
|
<a
|
||||||
class="d-none d-sm-block mx-1"
|
class="d-none d-sm-block mx-1"
|
||||||
i18n
|
i18n
|
||||||
@ -166,7 +157,6 @@
|
|||||||
>Overview</a
|
>Overview</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
*ngIf="user?.settings?.viewMode === 'DEFAULT'"
|
|
||||||
class="d-block d-sm-none"
|
class="d-block d-sm-none"
|
||||||
i18n
|
i18n
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
@ -176,17 +166,6 @@
|
|||||||
[routerLink]="['/portfolio']"
|
[routerLink]="['/portfolio']"
|
||||||
>Portfolio</a
|
>Portfolio</a
|
||||||
>
|
>
|
||||||
<a
|
|
||||||
*ngIf="user?.settings?.viewMode !== 'DEFAULT'"
|
|
||||||
class="d-block d-sm-none"
|
|
||||||
i18n
|
|
||||||
mat-menu-item
|
|
||||||
[ngClass]="{
|
|
||||||
'font-weight-bold': currentRoute === 'portfolio'
|
|
||||||
}"
|
|
||||||
[routerLink]="['/portfolio', 'transactions']"
|
|
||||||
>Transactions</a
|
|
||||||
>
|
|
||||||
<a
|
<a
|
||||||
class="d-block d-sm-none"
|
class="d-block d-sm-none"
|
||||||
i18n
|
i18n
|
||||||
@ -246,12 +225,11 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="user === null">
|
<ng-container *ngIf="user === null">
|
||||||
<a
|
<a
|
||||||
*ngIf="currentRoute && currentRoute !== 'start'"
|
class="align-items-center d-flex h-100 mx-2 no-min-width px-2 rounded-0"
|
||||||
class="mx-2 no-min-width px-2"
|
|
||||||
mat-button
|
mat-button
|
||||||
[routerLink]="['/']"
|
[routerLink]="['/']"
|
||||||
>
|
>
|
||||||
<gf-logo></gf-logo>
|
<gf-logo [hideName]="!currentRoute || currentRoute === 'start'"></gf-logo>
|
||||||
</a>
|
</a>
|
||||||
<span class="spacer"></span>
|
<span class="spacer"></span>
|
||||||
<a
|
<a
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { LineChartItem } from '../../line-chart/interfaces/line-chart.interface';
|
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
|
||||||
|
|
||||||
export interface PositionDetailDialogParams {
|
export interface PositionDetailDialogParams {
|
||||||
deviceType: string;
|
deviceType: string;
|
||||||
|
@ -7,11 +7,11 @@ import {
|
|||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||||
|
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
|
||||||
import { isToday, parse } from 'date-fns';
|
import { isToday, parse } from 'date-fns';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
import { LineChartItem } from '../line-chart/interfaces/line-chart.interface';
|
|
||||||
import { PositionDetailDialogParams } from './interfaces/interfaces';
|
import { PositionDetailDialogParams } from './interfaces/interfaces';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
[benchmarkDataItems]="benchmarkDataItems"
|
[benchmarkDataItems]="benchmarkDataItems"
|
||||||
[benchmarkLabel]="benchmarkLabel"
|
[benchmarkLabel]="benchmarkLabel"
|
||||||
[historicalDataItems]="historicalDataItems"
|
[historicalDataItems]="historicalDataItems"
|
||||||
|
[showGradient]="true"
|
||||||
[showLegend]="true"
|
[showLegend]="true"
|
||||||
[showXAxis]="true"
|
[showXAxis]="true"
|
||||||
[showYAxis]="false"
|
[showYAxis]="false"
|
||||||
|
@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatDialogModule } from '@angular/material/dialog';
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
|
import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
|
||||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
|
@ -8,7 +8,6 @@ import {
|
|||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { PortfolioPerformance } from '@ghostfolio/common/interfaces';
|
import { PortfolioPerformance } from '@ghostfolio/common/interfaces';
|
||||||
import { Currency } from '@prisma/client';
|
|
||||||
import { CountUp } from 'countup.js';
|
import { CountUp } from 'countup.js';
|
||||||
import { isNumber } from 'lodash';
|
import { isNumber } from 'lodash';
|
||||||
|
|
||||||
@ -19,7 +18,7 @@ import { isNumber } from 'lodash';
|
|||||||
styleUrls: ['./portfolio-performance.component.scss']
|
styleUrls: ['./portfolio-performance.component.scss']
|
||||||
})
|
})
|
||||||
export class PortfolioPerformanceComponent implements OnChanges, OnInit {
|
export class PortfolioPerformanceComponent implements OnChanges, OnInit {
|
||||||
@Input() baseCurrency: Currency;
|
@Input() baseCurrency: string;
|
||||||
@Input() isLoading: boolean;
|
@Input() isLoading: boolean;
|
||||||
@Input() locale: string;
|
@Input() locale: string;
|
||||||
@Input() performance: PortfolioPerformance;
|
@Input() performance: PortfolioPerformance;
|
||||||
|
@ -6,7 +6,6 @@ import {
|
|||||||
OnInit
|
OnInit
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { PortfolioSummary } from '@ghostfolio/common/interfaces';
|
import { PortfolioSummary } from '@ghostfolio/common/interfaces';
|
||||||
import { Currency } from '@prisma/client';
|
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -16,7 +15,7 @@ import { formatDistanceToNow } from 'date-fns';
|
|||||||
styleUrls: ['./portfolio-summary.component.scss']
|
styleUrls: ['./portfolio-summary.component.scss']
|
||||||
})
|
})
|
||||||
export class PortfolioSummaryComponent implements OnChanges, OnInit {
|
export class PortfolioSummaryComponent implements OnChanges, OnInit {
|
||||||
@Input() baseCurrency: Currency;
|
@Input() baseCurrency: string;
|
||||||
@Input() isLoading: boolean;
|
@Input() isLoading: boolean;
|
||||||
@Input() locale: string;
|
@Input() locale: string;
|
||||||
@Input() summary: PortfolioSummary;
|
@Input() summary: PortfolioSummary;
|
||||||
|
@ -8,11 +8,11 @@ import {
|
|||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||||
|
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
|
||||||
import { format, isSameMonth, isToday, parseISO } from 'date-fns';
|
import { format, isSameMonth, isToday, parseISO } from 'date-fns';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
import { LineChartItem } from '../../line-chart/interfaces/line-chart.interface';
|
|
||||||
import { PositionDetailDialogParams } from './interfaces/interfaces';
|
import { PositionDetailDialogParams } from './interfaces/interfaces';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
benchmarkLabel="Buy Price"
|
benchmarkLabel="Buy Price"
|
||||||
[benchmarkDataItems]="benchmarkDataItems"
|
[benchmarkDataItems]="benchmarkDataItems"
|
||||||
[historicalDataItems]="historicalDataItems"
|
[historicalDataItems]="historicalDataItems"
|
||||||
|
[showGradient]="true"
|
||||||
[showXAxis]="true"
|
[showXAxis]="true"
|
||||||
[showYAxis]="true"
|
[showYAxis]="true"
|
||||||
[symbol]="data.symbol"
|
[symbol]="data.symbol"
|
||||||
|
@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatDialogModule } from '@angular/material/dialog';
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
|
import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
|
||||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
|
@ -83,10 +83,10 @@
|
|||||||
*matRowDef="let row; columns: displayedColumns"
|
*matRowDef="let row; columns: displayedColumns"
|
||||||
mat-row
|
mat-row
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
'cursor-pointer': !this.ignoreAssetClasses.includes(row.assetClass)
|
'cursor-pointer': !ignoreAssetSubClasses.includes(row.assetSubClass)
|
||||||
}"
|
}"
|
||||||
(click)="
|
(click)="
|
||||||
!this.ignoreAssetClasses.includes(row.assetClass) &&
|
!ignoreAssetSubClasses.includes(row.assetSubClass) &&
|
||||||
onOpenPositionDialog({ symbol: row.symbol })
|
onOpenPositionDialog({ symbol: row.symbol })
|
||||||
"
|
"
|
||||||
></tr>
|
></tr>
|
||||||
|
@ -42,7 +42,7 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
public dataSource: MatTableDataSource<PortfolioPosition> =
|
public dataSource: MatTableDataSource<PortfolioPosition> =
|
||||||
new MatTableDataSource();
|
new MatTableDataSource();
|
||||||
public displayedColumns = [];
|
public displayedColumns = [];
|
||||||
public ignoreAssetClasses = [AssetClass.CASH.toString()];
|
public ignoreAssetSubClasses = [AssetClass.CASH.toString()];
|
||||||
public isLoading = true;
|
public isLoading = true;
|
||||||
public pageSize = 7;
|
public pageSize = 7;
|
||||||
public routeQueryParams: Subscription;
|
public routeQueryParams: Subscription;
|
||||||
|
@ -7,7 +7,6 @@ import {
|
|||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit
|
OnInit
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { Currency } from '@prisma/client';
|
|
||||||
import svgMap from 'svgmap';
|
import svgMap from 'svgmap';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -17,8 +16,9 @@ import svgMap from 'svgmap';
|
|||||||
styleUrls: ['./world-map-chart.component.scss']
|
styleUrls: ['./world-map-chart.component.scss']
|
||||||
})
|
})
|
||||||
export class WorldMapChartComponent implements OnChanges, OnDestroy, OnInit {
|
export class WorldMapChartComponent implements OnChanges, OnDestroy, OnInit {
|
||||||
@Input() baseCurrency: Currency;
|
@Input() baseCurrency: string;
|
||||||
@Input() countries: { [code: string]: { name: string; value: number } };
|
@Input() countries: { [code: string]: { name: string; value: number } };
|
||||||
|
@Input() isInPercent = false;
|
||||||
|
|
||||||
public isLoading = true;
|
public isLoading = true;
|
||||||
public svgMapElement;
|
public svgMapElement;
|
||||||
@ -42,6 +42,27 @@ export class WorldMapChartComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private initialize() {
|
private initialize() {
|
||||||
|
if (this.isInPercent) {
|
||||||
|
// Convert value of countries to percentage
|
||||||
|
let sum = 0;
|
||||||
|
Object.keys(this.countries).map((country) => {
|
||||||
|
sum += this.countries[country].value;
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(this.countries).map((country) => {
|
||||||
|
this.countries[country].value = Number(
|
||||||
|
((this.countries[country].value * 100) / sum).toFixed(2)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Convert value to fixed-point notation
|
||||||
|
Object.keys(this.countries).map((country) => {
|
||||||
|
this.countries[country].value = Number(
|
||||||
|
this.countries[country].value.toFixed(2)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.svgMapElement = new svgMap({
|
this.svgMapElement = new svgMap({
|
||||||
colorMax: '#22bdb9',
|
colorMax: '#22bdb9',
|
||||||
colorMin: '#c3f1f0',
|
colorMin: '#c3f1f0',
|
||||||
@ -50,7 +71,7 @@ export class WorldMapChartComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
applyData: 'value',
|
applyData: 'value',
|
||||||
data: {
|
data: {
|
||||||
value: {
|
value: {
|
||||||
format: `{0} ${this.baseCurrency}`
|
format: this.isInPercent ? `{0}%` : `{0} ${this.baseCurrency}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
values: this.countries
|
values: this.countries
|
||||||
|
@ -18,6 +18,7 @@ export class AuthGuard implements CanActivate {
|
|||||||
'/about',
|
'/about',
|
||||||
'/de/blog',
|
'/de/blog',
|
||||||
'/en/blog',
|
'/en/blog',
|
||||||
|
'/p',
|
||||||
'/pricing',
|
'/pricing',
|
||||||
'/register',
|
'/register',
|
||||||
'/resources'
|
'/resources'
|
||||||
|
@ -61,7 +61,23 @@ export class HttpResponseInterceptor implements HttpInterceptor {
|
|||||||
return event;
|
return event;
|
||||||
}),
|
}),
|
||||||
catchError((error: HttpErrorResponse) => {
|
catchError((error: HttpErrorResponse) => {
|
||||||
if (error.status === StatusCodes.INTERNAL_SERVER_ERROR) {
|
if (error.status === StatusCodes.FORBIDDEN) {
|
||||||
|
if (!this.snackBarRef) {
|
||||||
|
this.snackBarRef = this.snackBar.open(
|
||||||
|
'This feature requires a subscription.',
|
||||||
|
'Upgrade Plan',
|
||||||
|
{ duration: 6000 }
|
||||||
|
);
|
||||||
|
|
||||||
|
this.snackBarRef.afterDismissed().subscribe(() => {
|
||||||
|
this.snackBarRef = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.snackBarRef.onAction().subscribe(() => {
|
||||||
|
this.router.navigate(['/pricing']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (error.status === StatusCodes.INTERNAL_SERVER_ERROR) {
|
||||||
if (!this.snackBarRef) {
|
if (!this.snackBarRef) {
|
||||||
this.snackBarRef = this.snackBar.open(
|
this.snackBarRef = this.snackBar.open(
|
||||||
'Oops! Something went wrong. Please try again later.',
|
'Oops! Something went wrong. Please try again later.',
|
||||||
@ -85,7 +101,7 @@ export class HttpResponseInterceptor implements HttpInterceptor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return throwError('');
|
return throwError(error);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -11,9 +11,10 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
host: { class: 'mb-5' },
|
||||||
selector: 'gf-about-page',
|
selector: 'gf-about-page',
|
||||||
templateUrl: './about-page.html',
|
styleUrls: ['./about-page.scss'],
|
||||||
styleUrls: ['./about-page.scss']
|
templateUrl: './about-page.html'
|
||||||
})
|
})
|
||||||
export class AboutPageComponent implements OnDestroy, OnInit {
|
export class AboutPageComponent implements OnDestroy, OnInit {
|
||||||
public baseCurrency = baseCurrency;
|
public baseCurrency = baseCurrency;
|
||||||
|
@ -107,7 +107,7 @@
|
|||||||
<mat-card>
|
<mat-card>
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 col-md-3 my-2">
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
<h3 class="mb-0" [hidden]="!statistics?.activeUsers1d">
|
<h3 class="mb-0" [hidden]="!statistics?.activeUsers1d">
|
||||||
{{ statistics?.activeUsers1d ?? '-' }}
|
{{ statistics?.activeUsers1d ?? '-' }}
|
||||||
</h3>
|
</h3>
|
||||||
@ -117,7 +117,17 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-3 my-2">
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
|
<h3 class="mb-0" [hidden]="!statistics?.activeUsers7d">
|
||||||
|
{{ statistics?.activeUsers7d ?? '-' }}
|
||||||
|
</h3>
|
||||||
|
<div class="h6 mb-0">
|
||||||
|
<span i18n>Active Users</span> <small class="text-muted"
|
||||||
|
>(Last 7 days)</small
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
<h3 class="mb-0" [hidden]="!statistics?.activeUsers30d">
|
<h3 class="mb-0" [hidden]="!statistics?.activeUsers30d">
|
||||||
{{ statistics?.activeUsers30d ?? '-' }}
|
{{ statistics?.activeUsers30d ?? '-' }}
|
||||||
</h3>
|
</h3>
|
||||||
@ -127,13 +137,23 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-3 my-2">
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
|
<h3 class="mb-0" [hidden]="!statistics?.newUsers30d">
|
||||||
|
{{ statistics?.newUsers30d ?? '-' }}
|
||||||
|
</h3>
|
||||||
|
<div class="h6 mb-0">
|
||||||
|
<span i18n>New Users</span> <small class="text-muted"
|
||||||
|
>(Last 30 days)</small
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
<h3 class="mb-0" [hidden]="!statistics?.gitHubContributors">
|
<h3 class="mb-0" [hidden]="!statistics?.gitHubContributors">
|
||||||
{{ statistics?.gitHubContributors ?? '-' }}
|
{{ statistics?.gitHubContributors ?? '-' }}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="h6 mb-0" i18n>Contributors on GitHub</div>
|
<div class="h6 mb-0" i18n>Contributors on GitHub</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-3 my-2">
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
<h3 class="mb-0" [hidden]="!statistics?.gitHubStargazers">
|
<h3 class="mb-0" [hidden]="!statistics?.gitHubStargazers">
|
||||||
{{ statistics?.gitHubStargazers ?? '-' }}
|
{{ statistics?.gitHubStargazers ?? '-' }}
|
||||||
</h3>
|
</h3>
|
||||||
@ -198,7 +218,7 @@
|
|||||||
<h3 class="mb-3 text-center" i18n>Changelog</h3>
|
<h3 class="mb-3 text-center" i18n>Changelog</h3>
|
||||||
<mat-card class="changelog">
|
<mat-card class="changelog">
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<markdown [src]="'CHANGELOG.md'"></markdown>
|
<markdown [src]="'assets/CHANGELOG.md'"></markdown>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
@ -209,7 +229,7 @@
|
|||||||
<h3 class="mb-3 text-center" i18n>License</h3>
|
<h3 class="mb-3 text-center" i18n>License</h3>
|
||||||
<mat-card>
|
<mat-card>
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<markdown [src]="'LICENSE'"></markdown>
|
<markdown [src]="'assets/LICENSE'"></markdown>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,25 +5,31 @@ import {
|
|||||||
OnInit,
|
OnInit,
|
||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import {
|
import {
|
||||||
MatSlideToggle,
|
MatSlideToggle,
|
||||||
MatSlideToggleChange
|
MatSlideToggleChange
|
||||||
} from '@angular/material/slide-toggle';
|
} from '@angular/material/slide-toggle';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
|
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
|
||||||
import { DEFAULT_DATE_FORMAT, baseCurrency } from '@ghostfolio/common/config';
|
import { DEFAULT_DATE_FORMAT, baseCurrency } from '@ghostfolio/common/config';
|
||||||
import { Access, User } from '@ghostfolio/common/interfaces';
|
import { Access, User } from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { Currency } from '@prisma/client';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { StripeService } from 'ngx-stripe';
|
import { StripeService } from 'ngx-stripe';
|
||||||
import { EMPTY, Subject } from 'rxjs';
|
import { EMPTY, Subject } from 'rxjs';
|
||||||
import { catchError, switchMap, takeUntil } from 'rxjs/operators';
|
import { catchError, switchMap, takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { CreateOrUpdateAccessDialog } from './create-or-update-access-dialog/create-or-update-access-dialog.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
host: { class: 'mb-5' },
|
||||||
selector: 'gf-account-page',
|
selector: 'gf-account-page',
|
||||||
templateUrl: './account-page.html',
|
styleUrls: ['./account-page.scss'],
|
||||||
styleUrls: ['./account-page.scss']
|
templateUrl: './account-page.html'
|
||||||
})
|
})
|
||||||
export class AccountPageComponent implements OnDestroy, OnInit {
|
export class AccountPageComponent implements OnDestroy, OnInit {
|
||||||
@ViewChild('toggleSignInWithFingerprintEnabledElement')
|
@ViewChild('toggleSignInWithFingerprintEnabledElement')
|
||||||
@ -33,9 +39,12 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
|||||||
public baseCurrency = baseCurrency;
|
public baseCurrency = baseCurrency;
|
||||||
public coupon: number;
|
public coupon: number;
|
||||||
public couponId: string;
|
public couponId: string;
|
||||||
public currencies: Currency[] = [];
|
public currencies: string[] = [];
|
||||||
public defaultDateFormat = DEFAULT_DATE_FORMAT;
|
public defaultDateFormat = DEFAULT_DATE_FORMAT;
|
||||||
|
public deviceType: string;
|
||||||
public hasPermissionForSubscription: boolean;
|
public hasPermissionForSubscription: boolean;
|
||||||
|
public hasPermissionToCreateAccess: boolean;
|
||||||
|
public hasPermissionToDeleteAccess: boolean;
|
||||||
public hasPermissionToUpdateViewMode: boolean;
|
public hasPermissionToUpdateViewMode: boolean;
|
||||||
public hasPermissionToUpdateUserSettings: boolean;
|
public hasPermissionToUpdateUserSettings: boolean;
|
||||||
public price: number;
|
public price: number;
|
||||||
@ -50,6 +59,10 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
|||||||
public constructor(
|
public constructor(
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
|
private deviceService: DeviceDetectorService,
|
||||||
|
private dialog: MatDialog,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private router: Router,
|
||||||
private stripeService: StripeService,
|
private stripeService: StripeService,
|
||||||
private userService: UserService,
|
private userService: UserService,
|
||||||
public webAuthnService: WebAuthnService
|
public webAuthnService: WebAuthnService
|
||||||
@ -65,6 +78,11 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
|||||||
permissions.enableSubscription
|
permissions.enableSubscription
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.hasPermissionToDeleteAccess = hasPermission(
|
||||||
|
globalPermissions,
|
||||||
|
permissions.deleteAccess
|
||||||
|
);
|
||||||
|
|
||||||
this.price = subscriptions?.[0]?.price;
|
this.price = subscriptions?.[0]?.price;
|
||||||
this.priceId = subscriptions?.[0]?.priceId;
|
this.priceId = subscriptions?.[0]?.priceId;
|
||||||
|
|
||||||
@ -74,6 +92,16 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
|||||||
if (state?.user) {
|
if (state?.user) {
|
||||||
this.user = state.user;
|
this.user = state.user;
|
||||||
|
|
||||||
|
this.hasPermissionToCreateAccess = hasPermission(
|
||||||
|
this.user.permissions,
|
||||||
|
permissions.createAccess
|
||||||
|
);
|
||||||
|
|
||||||
|
this.hasPermissionToDeleteAccess = hasPermission(
|
||||||
|
this.user.permissions,
|
||||||
|
permissions.deleteAccess
|
||||||
|
);
|
||||||
|
|
||||||
this.hasPermissionToUpdateUserSettings = hasPermission(
|
this.hasPermissionToUpdateUserSettings = hasPermission(
|
||||||
this.user.permissions,
|
this.user.permissions,
|
||||||
permissions.updateUserSettings
|
permissions.updateUserSettings
|
||||||
@ -87,12 +115,22 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
|||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.route.queryParams
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((params) => {
|
||||||
|
if (params['createDialog']) {
|
||||||
|
this.openCreateAccessDialog();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the controller
|
* Initializes the controller
|
||||||
*/
|
*/
|
||||||
public ngOnInit() {
|
public ngOnInit() {
|
||||||
|
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||||
|
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,6 +174,17 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onDeleteAccess(aId: string) {
|
||||||
|
this.dataService
|
||||||
|
.deleteAccess(aId)
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public onRestrictedViewChange(aEvent: MatSlideToggleChange) {
|
public onRestrictedViewChange(aEvent: MatSlideToggleChange) {
|
||||||
this.dataService
|
this.dataService
|
||||||
.putUserSetting({ isRestrictedView: aEvent.checked })
|
.putUserSetting({ isRestrictedView: aEvent.checked })
|
||||||
@ -175,6 +224,38 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
|||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private openCreateAccessDialog(): void {
|
||||||
|
const dialogRef = this.dialog.open(CreateOrUpdateAccessDialog, {
|
||||||
|
data: {
|
||||||
|
access: {
|
||||||
|
type: 'PUBLIC'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||||
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef
|
||||||
|
.afterClosed()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((data: any) => {
|
||||||
|
const access: CreateAccessDto = data?.access;
|
||||||
|
|
||||||
|
if (access) {
|
||||||
|
this.dataService
|
||||||
|
.postAccess({})
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.router.navigate(['.'], { relativeTo: this.route });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private deregisterDevice() {
|
private deregisterDevice() {
|
||||||
this.webAuthnService
|
this.webAuthnService
|
||||||
.deregister()
|
.deregister()
|
||||||
|
@ -132,10 +132,26 @@
|
|||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="accesses?.length > 0" class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h3 class="mb-3 text-center" i18n>Granted Access</h3>
|
<h3 class="mb-3 text-center" i18n>Granted Access</h3>
|
||||||
<gf-access-table [accesses]="accesses"></gf-access-table>
|
<gf-access-table
|
||||||
|
[accesses]="accesses"
|
||||||
|
[showActions]="hasPermissionToDeleteAccess"
|
||||||
|
(accessDeleted)="onDeleteAccess($event)"
|
||||||
|
></gf-access-table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="hasPermissionToCreateAccess" class="fab-container">
|
||||||
|
<a
|
||||||
|
class="align-items-center d-flex justify-content-center"
|
||||||
|
color="primary"
|
||||||
|
mat-fab
|
||||||
|
[routerLink]="[]"
|
||||||
|
[queryParams]="{ createDialog: true }"
|
||||||
|
>
|
||||||
|
<ion-icon name="add-outline" size="large"></ion-icon>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -12,6 +12,7 @@ import { GfPortfolioAccessTableModule } from '@ghostfolio/client/components/acce
|
|||||||
|
|
||||||
import { AccountPageRoutingModule } from './account-page-routing.module';
|
import { AccountPageRoutingModule } from './account-page-routing.module';
|
||||||
import { AccountPageComponent } from './account-page.component';
|
import { AccountPageComponent } from './account-page.component';
|
||||||
|
import { GfCreateOrUpdateAccessDialogModule } from './create-or-update-access-dialog/create-or-update-access-dialog.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [AccountPageComponent],
|
declarations: [AccountPageComponent],
|
||||||
@ -20,6 +21,7 @@ import { AccountPageComponent } from './account-page.component';
|
|||||||
AccountPageRoutingModule,
|
AccountPageRoutingModule,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
|
GfCreateOrUpdateAccessDialogModule,
|
||||||
GfPortfolioAccessTableModule,
|
GfPortfolioAccessTableModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
|
@ -2,6 +2,26 @@
|
|||||||
color: rgb(var(--dark-primary-text));
|
color: rgb(var(--dark-primary-text));
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
|
gf-access-table {
|
||||||
|
overflow-x: auto;
|
||||||
|
|
||||||
|
table {
|
||||||
|
min-width: 100%;
|
||||||
|
|
||||||
|
.mat-row,
|
||||||
|
.mat-header-row {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fab-container {
|
||||||
|
position: fixed;
|
||||||
|
right: 2rem;
|
||||||
|
bottom: 2rem;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
.hint-text {
|
.hint-text {
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user