Compare commits

...

61 Commits

Author SHA1 Message Date
7d03c373ac Release 1.68.0 (#450) 2021-11-01 21:27:23 +01:00
edb66bb166 Feature/extend statistics (#449)
* Extend statistics

* Update changelog
2021-11-01 21:15:09 +01:00
54bbc8446b Feature/prettify scraper symbol in chart (#448)
* Prettify scraper symbol in chart

* Update changelog
2021-11-01 20:29:16 +01:00
9933967e42 Release 1.67.0 (#447) 2021-11-01 19:33:45 +01:00
5618513d07 Update changelog (#446) 2021-10-31 17:02:41 +01:00
1397cd62a8 Feature/extend public page (#445)
* Extend public page

* Update changelog
2021-10-31 17:00:59 +01:00
e7fb31d1a6 add Dockerfile with all in one docker image (#431)
* Add Dockerfile with all in one docker image

* Change to alpine image and reduce node_modules size

* Improve documentation and fix changelog and license

* Update changelog

Co-authored-by: Valentin Zickner <ghostfolio@zickner.ch>
Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2021-10-30 18:54:14 +02:00
51e7b94ad0 Release 1.66.0 (#443) 2021-10-30 11:22:50 +02:00
9133ea38f3 Feature/order accesses (#441)
* Order accesses

* Update changelog
2021-10-30 10:49:17 +02:00
a864c617b9 Feature/improve landing page (#442)
* Add intro screen

* Update changelog
2021-10-30 10:46:54 +02:00
442df9d6f8 Release 1.65.0 (#440) 2021-10-25 21:03:36 +02:00
2de0e75cb8 Feature/add user interface for granting and revoking public access (#439)
* Add user interface for granting and revoking public access

* Update changelog
2021-10-25 20:46:36 +02:00
1296f95602 Feature/move data enhancer from data provider to data gathering (#437)
* Move data enhancers from data provider to data enhancer module

* Update changelog
2021-10-24 10:49:17 +02:00
e98dff877a Add link to Visual Studio Code (#436) 2021-10-22 20:20:59 +02:00
964b37af30 Add Visual Studio Code link (#435) 2021-10-22 20:17:33 +02:00
2d89d12d31 Release 1.64.0 (#434) 2021-10-21 22:25:58 +02:00
19e2d54791 Refactor custom cryptocurrencies (#433) 2021-10-21 22:11:12 +02:00
e24e5e1c44 Add custom crypto currencies (#432)
* Add custom cryptocurrencies
2021-10-21 21:20:01 +02:00
a0d4ff7920 Clean up ts-node script (#430)
Co-Authored-By: Davin <52686922+Frozies@users.noreply.github.com>
2021-10-20 22:46:58 +02:00
099ad18aaf refactor data-provider.service.ts to have dynamic list of services (#429)
* Refactor data-provider.service.ts to have dynamic list of services

* Update changelog

Co-authored-by: Valentin Zickner <ghostfolio@zickner.ch>
Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2021-10-20 21:41:40 +02:00
e3cd99f5d2 Release 1.63.0 (#427) 2021-10-19 18:49:11 +02:00
6dea9093ba Feature/add public portfolio (#426)
* Setup public portfolio page

* Update changelog
2021-10-19 18:27:50 +02:00
43104f81d0 Add test data (#424) 2021-10-17 11:56:35 +02:00
6d2e3b6e40 Feature/improve skeleton loader size (#425)
* Improve skeleton loader size

* Update changelog
2021-10-17 11:53:27 +02:00
1d9a31dbb8 Improve styling (#423) 2021-10-17 11:42:27 +02:00
ea0e92220c Release 1.62.0 (#422) 2021-10-17 10:52:08 +02:00
b57301ef50 Feature/extend import validation message (#421)
* Extend import validation message

* Update changelog
2021-10-16 21:23:15 +02:00
67dbc6b014 Release 1.61.0 (#420) 2021-10-15 22:30:44 +02:00
2e5176bacf Feature/extend import by csv files (#419)
* Support import of csv files

* Update changelog
2021-10-15 22:22:45 +02:00
060846023f Release 1.60.0 (#418) 2021-10-13 12:10:12 +02:00
f06a0fbbee Feature/validate duplicate orders for import (#416)
* Validate duplicate orders

* Update changelog
2021-10-13 11:52:04 +02:00
4ab6a1a071 Feature/harmonize page layouts (#417)
* Harmonize page layouts

* Update changelog
2021-10-13 11:51:33 +02:00
93dcbeb6c7 Feature/add validation for import (#415)
* Valid data types
* Maximum number of orders
* Data provider service returns data for the dataSource / symbol pair
2021-10-12 22:19:32 +02:00
b9f0a57522 Bugfix/unregister chartjs plugin datalabels (#414)
* Unregister chartjs-plugin-datalabels

* Update changelog
2021-10-12 09:23:37 +02:00
174c1d1a62 Release 1.59.0 (#413) 2021-10-11 20:35:06 +02:00
f308ae7a13 add sectors and countries for ETFs (#410)
* Update changelog

Co-Authored-By: Valentin Zickner <valentin@coderworks.de>

Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
2021-10-11 19:32:21 +02:00
a7a6b0608b Add ghostfolio-docker (#411)
Co-Authored-By: psychowood <115389+psychowood@users.noreply.github.com>
2021-10-09 10:45:58 +02:00
15a61b7a20 Feature/improve values of global heat map (#408)
* Convert value

* Update changelog
2021-10-04 21:22:42 +02:00
d1eedf9726 Bugfix/various fixes (#407)
* Fix links

* Update column

* Fix impersonation mode

* Update changelog
2021-10-03 22:04:23 +02:00
30a592b524 Release 1.58.1 (#406) 2021-10-03 10:50:01 +02:00
de94494aa0 Bugfix/fix symbol conversion for yahoo finance (#405)
* Fix symbol conversion for Yahoo Finance

* Update changelog
2021-10-03 10:40:21 +02:00
d3c6788ad5 Release 1.58.0 (#404) 2021-10-02 20:45:02 +02:00
3ec4a73b35 Feature/improve tooltips (#403)
* Improve tooltips

* Update changelog
2021-10-02 20:38:41 +02:00
1050bfa098 Feature/improve yahoo finance symbol conversion (#402)
* Improve symbol conversion

* Update changelog
2021-10-02 10:28:06 +02:00
595ec1d7b4 Feature/upgrade envalid to version 7.2.1 (#401)
* Upgrade envalid

* Update changelog
2021-09-30 21:54:58 +02:00
c8389599b6 Release 1.57.0 (#400) 2021-09-29 21:34:50 +02:00
8769fe4c90 Improve styling (#399) 2021-09-29 21:10:04 +02:00
4219e1121e Improve style (#398) 2021-09-29 21:05:01 +02:00
f558eb8de8 Fix template (#397) 2021-09-29 21:04:41 +02:00
fe2bd6eea8 Feature/protect endpoints (#396)
* Protect endpoints

* Update changelog
2021-09-28 21:37:01 +02:00
035052be99 Feature/improve exchange rates table (#394)
* Improve exchange rates table

* Update changelog
2021-09-26 20:57:37 +02:00
bcdd2780b3 Release 1.56.0 (#393) 2021-09-25 18:02:02 +02:00
22d1ed7920 Extend data (#392)
* assetClass
* assetSubClass
* currency
* name
2021-09-25 17:42:45 +02:00
39d9828f9f Feature/respect account currency in exchange rate data service (#391)
* Respect the accounts' currencies

* Update changelog
2021-09-25 16:45:21 +02:00
6333aa972d Bugfix/fix data gathering after seed (#390)
* Fix data gathering after seed

* Update changelog
2021-09-25 16:44:24 +02:00
554f2f861f Upgrade @types (#389) 2021-09-25 09:20:47 +02:00
dcee651098 Feature/support unlimited currencies (#387)
* Support unlimited currencies

* Update changelog
2021-09-24 21:09:48 +02:00
508a48f4c3 Feature/hide actions in presenter view (#380)
* Hide actions if restricted view is active

* Update changelog
2021-09-24 20:15:10 +02:00
8466e3d73f Feature/always show adapted portfolio page (#388)
* Always show adapted portfolio page

* Update changelog
2021-09-24 20:12:17 +02:00
9ae9904389 Feature/add story for line chart component (#385)
* Add story for line chart component

* Update changelog
2021-09-20 21:44:47 +02:00
af022ae316 Feature/add story for portfolio proportion chart component (#384)
* Add story

* Use new component

* Update changelog
2021-09-20 20:45:58 +02:00
203 changed files with 3322 additions and 1120 deletions

View File

@ -1 +1,2 @@
/dist /dist
/test/import

View File

@ -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
View 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" ]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
export class CreateAccessDto {}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,4 @@
import { Currency } from '@prisma/client';
export interface Data { export interface Data {
currency: Currency; currency: string;
value: number; value: number;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,3 @@
import { Currency } from '@prisma/client';
export interface UserSettings { export interface UserSettings {
baseCurrency: Currency; baseCurrency: string;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { CryptocurrencyService } from './cryptocurrency.service';
@Module({
providers: [CryptocurrencyService],
exports: [CryptocurrencyService]
})
export class CryptocurrencyModule {}

View File

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

View File

@ -0,0 +1,6 @@
{
"1INCH": "1inch",
"AVAX": "Avalanche",
"MATIC": "Polygon",
"SHIB": "Shiba Inu"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: [] };
} }

View File

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

View File

@ -0,0 +1,11 @@
import { IDataProviderResponse } from '@ghostfolio/api/services/interfaces/interfaces';
export interface DataEnhancerInterface {
enhance({
response,
symbol
}: {
response: IDataProviderResponse;
symbol: string;
}): Promise<IDataProviderResponse>;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: () =>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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