Compare commits

..

34 Commits

Author SHA1 Message Date
e68ebdf76d Release 0.96.0 2021-04-30 21:18:42 +02:00
949cf58eef Release 0.96.0 (#56) 2021-04-30 21:14:28 +02:00
0816defb95 Feature/update slogan (#55)
* Harmonize slogan
2021-04-30 21:11:48 +02:00
a076a1c933 Feature/extend position detail dialog (#54)
* Extend position detail dialog

* Absolute change
* Number of transactions
2021-04-30 21:08:43 +02:00
40c95a541d Add data source to transaction model (#53) 2021-04-28 21:30:49 +02:00
2d04d7b8d5 Release 0.94.0 (#52) 2021-04-27 21:04:51 +02:00
94e0feac68 Add scraper symbols to symbol lookup results (#51) 2021-04-27 21:03:22 +02:00
cd9e974c40 Release 0.93.0 (#50) 2021-04-26 22:09:20 +02:00
11700f75d9 Improve colors in dark mode, improve user table (#49) 2021-04-26 21:54:19 +02:00
0439a7beaa Release 0.92.0 (#48) 2021-04-25 21:27:13 +02:00
608b195ba9 Extend user table (#47)
* Column: accounts
* Horizontal scrollbar
2021-04-25 21:25:23 +02:00
8cb5fd64dd Prepare for multi accounts support: store account for new transactions (#46) 2021-04-25 21:22:35 +02:00
ef317a86ed Setup angular compiler options (#45) 2021-04-25 17:41:53 +02:00
19ada83d0b Fix header and about page (#44) 2021-04-25 17:29:04 +02:00
6ecf66ea2a Fix types 2021-04-25 12:34:34 +02:00
65ba16c07c Fix tests 2021-04-25 12:30:28 +02:00
aee7c12c44 Release 0.91.0 (#43) 2021-04-25 12:15:40 +02:00
125956eb3e Refactor number extraction from string (#41) 2021-04-25 12:11:52 +02:00
954224401d Prepare for multi accounts support (#42) 2021-04-25 12:07:32 +02:00
d268de3e12 Feature/improve styling of x ray rules (#40) 2021-04-24 22:22:41 +02:00
39cfb4603b Extend support for feature flags (#39) 2021-04-24 22:01:38 +02:00
15a70abf67 Release 0.90.0 2021-04-22 22:02:52 +02:00
5cb69291f5 Introduce market states (#38)
* closed
* delayed (no live data)
* open
2021-04-22 22:00:46 +02:00
cf582b2e98 Add symbol icon to position detail dialog (#37) 2021-04-22 21:21:52 +02:00
82e159a083 Filter inactive users (#36) 2021-04-22 20:55:05 +02:00
2aff139982 Release 0.89.0 (#35) 2021-04-21 21:17:25 +02:00
6c7adb6193 Prettify generic scraper symbols (#34) 2021-04-21 21:15:49 +02:00
bb2cd1c85a Improve documentation (#31) 2021-04-21 21:14:06 +02:00
9d92c48ab7 Fix text truncation of buttons (#33) 2021-04-21 21:13:38 +02:00
dbed4ea527 Feature/improve imports with paths in tsconfig (#32)
* Improve imports
2021-04-21 20:27:39 +02:00
8d149b5e2b Reduce period of max data gathering (#30) 2021-04-21 18:58:21 +02:00
27f1ec5d8a Release 0.88.0 (#29) 2021-04-20 21:55:57 +02:00
c361143ba2 Fix interceptor for unauthorized http response (#27) 2021-04-20 21:54:01 +02:00
069006145a Improve scraper (#28) 2021-04-20 21:52:01 +02:00
157 changed files with 1384 additions and 802 deletions

View File

@ -5,6 +5,91 @@ 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).
## 0.96.0 - 30.04.2021
### Added
- Added the absolute change to the position detail dialog
- Added the number of transactions to the position detail dialog
### Changed
- Harmonized the slogan to "Open Source Portfolio Tracker"
## 0.95.0 - 28.04.2021
### Added
- Added a data source attribute to the transactions model
## 0.94.0 - 27.04.2021
### Added
- Added the generic scraper symbols to the symbol lookup results
## 0.93.0 - 26.04.2021
### Changed
- Improved the user table styling of the admin control panel
- Improved the background colors in the dark mode
## 0.92.0 - 25.04.2021
### Added
- Prepared further for multi accounts support: store account for new transactions
- Added a horizontal scrollbar to the user table of the admin control panel
### Fixed
- Fixed an issue in the header with outdated data
- Fixed an issue on the about page with outdated data
## 0.91.0 - 25.04.2021
### Added
- Extended the support for feature flags to simplify the initial project setup
- Prepared for multi accounts support
### Changed
- Improved the styling of the rules in the _X-ray_ section
## 0.90.0 - 22.04.2021
### Added
- Added the symbol logo to the position detail dialog
- Introduced a third option for the market state: `delayed` (besides `open` and `closed`)
### Changed
- Improved the user table of the admin control panel
## 0.89.0 - 21.04.2021
### Added
- Added a prettifier (pipe) for generic scraper symbols
### Fixed
- Fixed the text truncation in buttons of the admin control panel
## 0.88.0 - 20.04.2021
### Changed
- Reverted the restoring of the scroll position when opening a new page
### Fixed
- Fixed the frozen screen if the token has expired
- Fixed some issues in the generic scraper
## 0.87.0 - 19.04.2021 ## 0.87.0 - 19.04.2021
### Added ### Added
@ -63,7 +148,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- Fixed an issue in the portfolio update on deleting a transaction - Fixed an issue in the portfolio update on deleting a transaction
- Fixed an issue in the _X-Ray_ section (missing redirection on logout) - Fixed an issue in the _X-ray_ section (missing redirection on logout)
## 0.82.0 - 10.04.2021 ## 0.82.0 - 10.04.2021
@ -136,7 +221,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Grouped the _X-Ray_ section visually in _Currency Cluster Risk_ and _Platform Cluster Risk_ - Grouped the _X-ray_ section visually in _Currency Cluster Risk_ and _Platform Cluster Risk_
## 0.76.0 - 02.04.2021 ## 0.76.0 - 02.04.2021
@ -148,7 +233,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- Fixed an issue in the _X-Ray_ section (empty portfolio) - Fixed an issue in the _X-ray_ section (empty portfolio)
## 0.75.0 - 01.04.2021 ## 0.75.0 - 01.04.2021
@ -161,7 +246,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added a _Create Account_ message in the _Live Demo_ - Added a _Create Account_ message in the _Live Demo_
- Added skeleton loaders to the _X-Ray_ section - Added skeleton loaders to the _X-ray_ section
### Changed ### Changed
@ -177,7 +262,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Improved the intro text in the _X-Ray_ section - Improved the intro text in the _X-ray_ section
### Fixed ### Fixed
@ -193,7 +278,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added an intro text to the _X-Ray_ section - Added an intro text to the _X-ray_ section
### Changed ### Changed
@ -212,7 +297,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Improved the styling in the _X-Ray_ section - Improved the styling in the _X-ray_ section
## 0.70.0 - 27.03.2021 ## 0.70.0 - 27.03.2021
@ -220,7 +305,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added the current _Fear & Greed Index_ as text - Added the current _Fear & Greed Index_ as text
- Extended the landing page text: _Ghostfolio_ empowers busy folks... - Extended the landing page text: _Ghostfolio_ empowers busy folks...
- Added the first static portfolio analysis rule in the brand new _X-Ray_ section - Added the first static portfolio analysis rule in the brand new _X-ray_ section
### Changed ### Changed

View File

@ -61,7 +61,7 @@ The frontend is built with [Angular](https://angular.io).
### Prerequisites ### Prerequisites
- [Node.js](https://nodejs.org/en/download) - [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) - [Docker](https://www.docker.com/products/docker-desktop)
@ -71,12 +71,17 @@ The frontend is built with [Angular](https://angular.io).
2. Run `cd docker` 2. Run `cd docker`
3. Run `docker compose build` 3. Run `docker compose build`
4. Run `docker compose up -d` to start [PostgreSQL](https://www.postgresql.org) and [Redis](https://redis.io) 4. Run `docker compose up -d` to start [PostgreSQL](https://www.postgresql.org) and [Redis](https://redis.io)
5. Run `yarn setup:database` to initialize the database schema and populate your database with (example) data 5. Run `cd -` to go back to the project root directory
6. Start server and client (see _Development_) 6. Run `yarn setup:database` to initialize the database schema and populate your database with (example) data
7. Login as _Admin_ with the following _Security Token_: `ae76872ae8f3419c6d6f64bf51888ecbcc703927a342d815fafe486acdb938da07d0cf44fca211a0be74a423238f535362d390a41e81e633a9ce668a6e31cdf9` 7. Start server and client (see _Development_)
8. Login as _Admin_ with the following _Security Token_: `ae76872ae8f3419c6d6f64bf51888ecbcc703927a342d815fafe486acdb938da07d0cf44fca211a0be74a423238f535362d390a41e81e633a9ce668a6e31cdf9`
9. Go to the _Admin Control Panel_ and press _Gather All Data_ to fetch historical data
10. Press _Sign out_ and check out the _Live Demo_
## Development ## Development
Please make sure you have completed the instructions from _Setup_
### Start server ### Start server
- Debug: Run `yarn watch:server` and click "Launch Program" in _Visual Studio Code_ - Debug: Run `yarn watch:server` and click "Launch Program" in _Visual Studio Code_

View File

@ -1,7 +1,7 @@
import { RequestWithUser } from '@ghostfolio/api/app/interfaces/request-with-user.type';
import { Controller, Get, Inject, UseGuards } from '@nestjs/common'; import { Controller, Get, Inject, 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 { RequestWithUser } from 'apps/api/src/app/interfaces/request-with-user.type';
import { AccessService } from './access.service'; import { AccessService } from './access.service';
import { Access } from './interfaces/access.interface'; import { Access } from './interfaces/access.interface';

View File

@ -1,6 +1,6 @@
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { PrismaService } from '../../services/prisma.service';
import { AccessController } from './access.controller'; import { AccessController } from './access.controller';
import { AccessService } from './access.service'; import { AccessService } from './access.service';

View File

@ -1,7 +1,7 @@
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client'; import { Prisma } from '@prisma/client';
import { PrismaService } from '../../services/prisma.service';
import { AccessWithGranteeUser } from './interfaces/access-with-grantee-user.type'; import { AccessWithGranteeUser } from './interfaces/access-with-grantee-user.type';
@Injectable() @Injectable()

View File

@ -1,3 +1,5 @@
import { RequestWithUser } from '@ghostfolio/api/app/interfaces/request-with-user.type';
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
import { getPermissions, hasPermission, permissions } from '@ghostfolio/helper'; import { getPermissions, hasPermission, permissions } from '@ghostfolio/helper';
import { import {
Controller, Controller,
@ -9,10 +11,8 @@ import {
} from '@nestjs/common'; } from '@nestjs/common';
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { RequestWithUser } from 'apps/api/src/app/interfaces/request-with-user.type';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { DataGatheringService } from '../../services/data-gathering.service';
import { AdminService } from './admin.service'; import { AdminService } from './admin.service';
import { AdminData } from './interfaces/admin-data.interface'; import { AdminData } from './interfaces/admin-data.interface';

View File

@ -1,14 +1,14 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ConfigurationService } from '../../services/configuration.service';
import { DataGatheringService } from '../../services/data-gathering.service';
import { DataProviderService } from '../../services/data-provider.service';
import { AlphaVantageService } from '../../services/data-provider/alpha-vantage/alpha-vantage.service';
import { GhostfolioScraperApiService } from '../../services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
import { RakutenRapidApiService } from '../../services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
import { YahooFinanceService } from '../../services/data-provider/yahoo-finance/yahoo-finance.service';
import { ExchangeRateDataService } from '../../services/exchange-rate-data.service';
import { PrismaService } from '../../services/prisma.service';
import { AdminController } from './admin.controller'; import { AdminController } from './admin.controller';
import { AdminService } from './admin.service'; import { AdminService } from './admin.service';

View File

@ -1,8 +1,8 @@
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Currency } from '@prisma/client'; import { Currency } from '@prisma/client';
import { ExchangeRateDataService } from '../../services/exchange-rate-data.service';
import { PrismaService } from '../../services/prisma.service';
import { AdminData } from './interfaces/admin-data.interface'; import { AdminData } from './interfaces/admin-data.interface';
@Injectable() @Injectable()
@ -97,7 +97,7 @@ export class AdminService {
}, },
select: { select: {
_count: { _count: {
select: { Order: true } select: { Account: true, Order: true }
}, },
alias: true, alias: true,
Analytics: { Analytics: {
@ -109,7 +109,12 @@ export class AdminService {
createdAt: true, createdAt: true,
id: true id: true
}, },
take: 20 take: 20,
where: {
NOT: {
Analytics: null
}
}
}); });
} }
} }

View File

@ -1,3 +1,4 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { import {
Controller, Controller,
Get, Get,
@ -10,7 +11,6 @@ import {
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { ConfigurationService } from '../../services/configuration.service';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
@Controller('auth') @Controller('auth')

View File

@ -1,8 +1,8 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt'; import { JwtModule } from '@nestjs/jwt';
import { ConfigurationService } from '../../services/configuration.service';
import { PrismaService } from '../../services/prisma.service';
import { UserService } from '../user/user.service'; import { UserService } from '../user/user.service';
import { AuthController } from './auth.controller'; import { AuthController } from './auth.controller';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';

View File

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

View File

@ -1,9 +1,9 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport'; import { PassportStrategy } from '@nestjs/passport';
import { Provider } from '@prisma/client'; import { Provider } from '@prisma/client';
import { Strategy } from 'passport-google-oauth20'; import { Strategy } from 'passport-google-oauth20';
import { ConfigurationService } from '../../services/configuration.service';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
@Injectable() @Injectable()

View File

@ -1,9 +1,9 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { Injectable, UnauthorizedException } from '@nestjs/common'; import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport'; import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt'; import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigurationService } from '../../services/configuration.service';
import { PrismaService } from '../../services/prisma.service';
import { UserService } from '../user/user.service'; import { UserService } from '../user/user.service';
@Injectable() @Injectable()

View File

@ -1,7 +1,7 @@
import { RequestWithUser } from '@ghostfolio/api/app/interfaces/request-with-user.type';
import { Controller, Inject, Param, Post, UseGuards } from '@nestjs/common'; import { Controller, 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 { RequestWithUser } from 'apps/api/src/app/interfaces/request-with-user.type';
import { RedisCacheService } from '../redis-cache/redis-cache.service'; import { RedisCacheService } from '../redis-cache/redis-cache.service';
import { CacheService } from './cache.service'; import { CacheService } from './cache.service';

View File

@ -1,6 +1,6 @@
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { PrismaService } from '../../services/prisma.service';
import { RedisCacheModule } from '../redis-cache/redis-cache.module'; import { RedisCacheModule } from '../redis-cache/redis-cache.module';
import { CacheController } from './cache.controller'; import { CacheController } from './cache.controller';
import { CacheService } from './cache.service'; import { CacheService } from './cache.service';

View File

@ -1,7 +1,5 @@
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Prisma, User } from '@prisma/client';
import { PrismaService } from '../../services/prisma.service';
@Injectable() @Injectable()
export class CacheService { export class CacheService {

View File

@ -1,3 +1,4 @@
import { RequestWithUser } from '@ghostfolio/api/app/interfaces/request-with-user.type';
import { import {
baseCurrency, baseCurrency,
benchmarks, benchmarks,
@ -14,7 +15,6 @@ import {
Post Post
} from '@nestjs/common'; } from '@nestjs/common';
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';
import { RequestWithUser } from 'apps/api/src/app/interfaces/request-with-user.type';
import { parse } from 'date-fns'; import { parse } from 'date-fns';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';

View File

@ -1,14 +1,14 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { RulesService } from '@ghostfolio/api/services/rules.service';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ConfigurationService } from '../../services/configuration.service';
import { DataProviderService } from '../../services/data-provider.service';
import { AlphaVantageService } from '../../services/data-provider/alpha-vantage/alpha-vantage.service';
import { GhostfolioScraperApiService } from '../../services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
import { RakutenRapidApiService } from '../../services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
import { YahooFinanceService } from '../../services/data-provider/yahoo-finance/yahoo-finance.service';
import { ExchangeRateDataService } from '../../services/exchange-rate-data.service';
import { PrismaService } from '../../services/prisma.service';
import { RulesService } from '../../services/rules.service';
import { ExperimentalController } from './experimental.controller'; import { ExperimentalController } from './experimental.controller';
import { ExperimentalService } from './experimental.service'; import { ExperimentalService } from './experimental.service';

View File

@ -1,12 +1,12 @@
import { Portfolio } from '@ghostfolio/api/models/portfolio';
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { RulesService } from '@ghostfolio/api/services/rules.service';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Currency, Type } from '@prisma/client'; import { Currency, Type } from '@prisma/client';
import { parseISO } from 'date-fns'; import { parseISO } from 'date-fns';
import { Portfolio } from '../../models/portfolio';
import { DataProviderService } from '../../services/data-provider.service';
import { ExchangeRateDataService } from '../../services/exchange-rate-data.service';
import { PrismaService } from '../../services/prisma.service';
import { RulesService } from '../../services/rules.service';
import { OrderWithPlatform } from '../order/interfaces/order-with-platform.type'; import { OrderWithPlatform } from '../order/interfaces/order-with-platform.type';
import { CreateOrderDto } from './create-order.dto'; import { CreateOrderDto } from './create-order.dto';
import { Data } from './interfaces/data.interface'; import { Data } from './interfaces/data.interface';
@ -36,7 +36,10 @@ export class ExperimentalService {
const ordersWithPlatform: OrderWithPlatform[] = aOrders.map((order) => { const ordersWithPlatform: OrderWithPlatform[] = aOrders.map((order) => {
return { return {
...order, ...order,
accountId: undefined,
accountUserId: undefined,
createdAt: new Date(), createdAt: new Date(),
dataSource: undefined,
date: parseISO(order.date), date: parseISO(order.date),
fee: 0, fee: 0,
id: undefined, id: undefined,

View File

@ -1,8 +1,8 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt'; import { JwtModule } from '@nestjs/jwt';
import { ConfigurationService } from '../../services/configuration.service';
import { PrismaService } from '../../services/prisma.service';
import { InfoController } from './info.controller'; import { InfoController } from './info.controller';
import { InfoService } from './info.service'; import { InfoService } from './info.service';

View File

@ -1,10 +1,10 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { permissions } from '@ghostfolio/helper'; import { permissions } from '@ghostfolio/helper';
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 { Currency } from '@prisma/client';
import { ConfigurationService } from '../../services/configuration.service';
import { PrismaService } from '../../services/prisma.service';
import { InfoItem } from './interfaces/info-item.interface'; import { InfoItem } from './interfaces/info-item.interface';
@Injectable() @Injectable()
@ -26,7 +26,11 @@ export class InfoService {
const globalPermissions: string[] = []; const globalPermissions: string[] = [];
if (this.configurationService.get('ENABLE_FEATURE_SOCIAL_LOGIN')) { if (this.configurationService.get('ENABLE_FEATURE_SOCIAL_LOGIN')) {
globalPermissions.push(permissions.useSocialLogin); globalPermissions.push(permissions.enableSocialLogin);
}
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
globalPermissions.push(permissions.enableSubscription);
} }
return { return {

View File

@ -1,3 +1,6 @@
import { Settings, User } from '@prisma/client'; import { Account, Settings, User } from '@prisma/client';
export type UserWithSettings = User & { Settings: Settings }; export type UserWithSettings = User & {
Account: Account[];
Settings: Settings;
};

View File

@ -1,10 +1,16 @@
import { Currency, Type } from '@prisma/client'; import { Currency, DataSource, Type } from '@prisma/client';
import { IsISO8601, IsNumber, IsString, ValidateIf } from 'class-validator'; import { IsISO8601, IsNumber, IsString, ValidateIf } from 'class-validator';
export class CreateOrderDto { export class CreateOrderDto {
@IsString()
accountId: string;
@IsString() @IsString()
currency: Currency; currency: Currency;
@IsString()
dataSource: DataSource;
@IsISO8601() @IsISO8601()
date: string; date: string;

View File

@ -1,3 +1,6 @@
import { RequestWithUser } from '@ghostfolio/api/app/interfaces/request-with-user.type';
import { nullifyValuesInObjects } from '@ghostfolio/api/helper/object.helper';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
import { getPermissions, hasPermission, permissions } from '@ghostfolio/helper'; import { getPermissions, hasPermission, permissions } from '@ghostfolio/helper';
import { import {
Body, Body,
@ -15,12 +18,9 @@ import {
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { Order as OrderModel } from '@prisma/client'; import { Order as OrderModel } from '@prisma/client';
import { RequestWithUser } from 'apps/api/src/app/interfaces/request-with-user.type';
import { parseISO } from 'date-fns'; import { parseISO } from 'date-fns';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { nullifyValuesInObjects } from '../../helper/object.helper';
import { ImpersonationService } from '../../services/impersonation.service';
import { CreateOrderDto } from './create-order.dto'; import { CreateOrderDto } from './create-order.dto';
import { OrderService } from './order.service'; import { OrderService } from './order.service';
import { UpdateOrderDto } from './update-order.dto'; import { UpdateOrderDto } from './update-order.dto';
@ -118,6 +118,9 @@ export class OrderController {
const date = parseISO(data.date); const date = parseISO(data.date);
const accountId = data.accountId;
delete data.accountId;
if (data.platformId) { if (data.platformId) {
const platformId = data.platformId; const platformId = data.platformId;
delete data.platformId; delete data.platformId;
@ -126,6 +129,11 @@ export class OrderController {
{ {
...data, ...data,
date, date,
Account: {
connect: {
id_userId: { id: accountId, userId: this.request.user.id }
}
},
Platform: { connect: { id: platformId } }, Platform: { connect: { id: platformId } },
User: { connect: { id: this.request.user.id } } User: { connect: { id: this.request.user.id } }
}, },
@ -138,6 +146,11 @@ export class OrderController {
{ {
...data, ...data,
date, date,
Account: {
connect: {
id_userId: { id: accountId, userId: this.request.user.id }
}
},
User: { connect: { id: this.request.user.id } } User: { connect: { id: this.request.user.id } }
}, },
this.request.user.id this.request.user.id
@ -169,6 +182,9 @@ export class OrderController {
const date = parseISO(data.date); const date = parseISO(data.date);
const accountId = data.accountId;
delete data.accountId;
if (data.platformId) { if (data.platformId) {
const platformId = data.platformId; const platformId = data.platformId;
delete data.platformId; delete data.platformId;
@ -178,6 +194,11 @@ export class OrderController {
data: { data: {
...data, ...data,
date, date,
Account: {
connect: {
id_userId: { id: accountId, userId: this.request.user.id }
}
},
Platform: { connect: { id: platformId } }, Platform: { connect: { id: platformId } },
User: { connect: { id: this.request.user.id } } User: { connect: { id: this.request.user.id } }
}, },
@ -199,6 +220,11 @@ export class OrderController {
data: { data: {
...data, ...data,
date, date,
Account: {
connect: {
id_userId: { id: accountId, userId: this.request.user.id }
}
},
Platform: originalOrder.platformId Platform: originalOrder.platformId
? { disconnect: true } ? { disconnect: true }
: undefined, : undefined,

View File

@ -1,14 +1,14 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ConfigurationService } from '../../services/configuration.service';
import { DataGatheringService } from '../../services/data-gathering.service';
import { DataProviderService } from '../../services/data-provider.service';
import { AlphaVantageService } from '../../services/data-provider/alpha-vantage/alpha-vantage.service';
import { GhostfolioScraperApiService } from '../../services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
import { RakutenRapidApiService } from '../../services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
import { YahooFinanceService } from '../../services/data-provider/yahoo-finance/yahoo-finance.service';
import { ImpersonationService } from '../../services/impersonation.service';
import { PrismaService } from '../../services/prisma.service';
import { CacheService } from '../cache/cache.service'; import { CacheService } from '../cache/cache.service';
import { RedisCacheModule } from '../redis-cache/redis-cache.module'; import { RedisCacheModule } from '../redis-cache/redis-cache.module';
import { OrderController } from './order.controller'; import { OrderController } from './order.controller';

View File

@ -1,8 +1,8 @@
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Order, Prisma } from '@prisma/client'; import { Order, Prisma } from '@prisma/client';
import { DataGatheringService } from '../../services/data-gathering.service';
import { PrismaService } from '../../services/prisma.service';
import { CacheService } from '../cache/cache.service'; import { CacheService } from '../cache/cache.service';
import { RedisCacheService } from '../redis-cache/redis-cache.service'; import { RedisCacheService } from '../redis-cache/redis-cache.service';
import { OrderWithPlatform } from './interfaces/order-with-platform.type'; import { OrderWithPlatform } from './interfaces/order-with-platform.type';

View File

@ -1,10 +1,16 @@
import { Currency, Type } from '@prisma/client'; import { Currency, DataSource, Type } from '@prisma/client';
import { IsISO8601, IsNumber, IsString, ValidateIf } from 'class-validator'; import { IsISO8601, IsNumber, IsString, ValidateIf } from 'class-validator';
export class UpdateOrderDto { export class UpdateOrderDto {
@IsString()
accountId: string;
@IsString() @IsString()
currency: Currency; currency: Currency;
@IsString()
dataSource: DataSource;
@IsISO8601() @IsISO8601()
date: string; date: string;

View File

@ -16,4 +16,5 @@ export interface Position {
investmentInOriginalCurrency?: number; investmentInOriginalCurrency?: number;
marketPrice?: number; marketPrice?: number;
quantity: number; quantity: number;
transactionCount: number;
} }

View File

@ -1,6 +1,8 @@
import { Currency } from '@prisma/client';
export interface PortfolioPositionDetail { export interface PortfolioPositionDetail {
averagePrice: number; averagePrice: number;
currency: string; currency: Currency;
firstBuyDate: string; firstBuyDate: string;
grossPerformance: number; grossPerformance: number;
grossPerformancePercent: number; grossPerformancePercent: number;
@ -11,6 +13,7 @@ export interface PortfolioPositionDetail {
minPrice: number; minPrice: number;
quantity: number; quantity: number;
symbol: string; symbol: string;
transactionCount: number;
} }
export interface HistoricalDataItem { export interface HistoricalDataItem {

View File

@ -1,3 +1,4 @@
import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces';
import { Currency } from '@prisma/client'; import { Currency } from '@prisma/client';
export interface PortfolioPosition { export interface PortfolioPosition {
@ -7,10 +8,10 @@ export interface PortfolioPosition {
grossPerformancePercent: number; grossPerformancePercent: number;
industry?: string; industry?: string;
investment: number; investment: number;
isMarketOpen: boolean;
marketChange?: number; marketChange?: number;
marketChangePercent?: number; marketChangePercent?: number;
marketPrice: number; marketPrice: number;
marketState: MarketState;
name: string; name: string;
platforms: { platforms: {
[name: string]: { current: number; original: number }; [name: string]: { current: number; original: number };
@ -19,6 +20,7 @@ export interface PortfolioPosition {
sector?: string; sector?: string;
shareCurrent: number; shareCurrent: number;
shareInvestment: number; shareInvestment: number;
transactionCount: number;
symbol: string; symbol: string;
type?: string; type?: string;
url?: string; url?: string;

View File

@ -1,3 +1,9 @@
import {
hasNotDefinedValuesInObject,
nullifyValuesInObject
} from '@ghostfolio/api/helper/object.helper';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
import { getPermissions, hasPermission, permissions } from '@ghostfolio/helper'; import { getPermissions, hasPermission, permissions } from '@ghostfolio/helper';
import { import {
Controller, Controller,
@ -15,12 +21,6 @@ import { AuthGuard } from '@nestjs/passport';
import { Response } from 'express'; import { Response } from 'express';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import {
hasNotDefinedValuesInObject,
nullifyValuesInObject
} from '../../helper/object.helper';
import { ExchangeRateDataService } from '../../services/exchange-rate-data.service';
import { ImpersonationService } from '../../services/impersonation.service';
import { RequestWithUser } from '../interfaces/request-with-user.type'; import { RequestWithUser } from '../interfaces/request-with-user.type';
import { PortfolioItem } from './interfaces/portfolio-item.interface'; import { PortfolioItem } from './interfaces/portfolio-item.interface';
import { PortfolioOverview } from './interfaces/portfolio-overview.interface'; import { PortfolioOverview } from './interfaces/portfolio-overview.interface';

View File

@ -1,16 +1,16 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { RulesService } from '@ghostfolio/api/services/rules.service';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ConfigurationService } from '../../services/configuration.service';
import { DataGatheringService } from '../../services/data-gathering.service';
import { DataProviderService } from '../../services/data-provider.service';
import { AlphaVantageService } from '../../services/data-provider/alpha-vantage/alpha-vantage.service';
import { GhostfolioScraperApiService } from '../../services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
import { RakutenRapidApiService } from '../../services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
import { YahooFinanceService } from '../../services/data-provider/yahoo-finance/yahoo-finance.service';
import { ExchangeRateDataService } from '../../services/exchange-rate-data.service';
import { ImpersonationService } from '../../services/impersonation.service';
import { PrismaService } from '../../services/prisma.service';
import { RulesService } from '../../services/rules.service';
import { CacheService } from '../cache/cache.service'; import { CacheService } from '../cache/cache.service';
import { OrderService } from '../order/order.service'; import { OrderService } from '../order/order.service';
import { RedisCacheModule } from '../redis-cache/redis-cache.module'; import { RedisCacheModule } from '../redis-cache/redis-cache.module';

View File

@ -1,6 +1,12 @@
import { RequestWithUser } from '@ghostfolio/api/app/interfaces/request-with-user.type';
import { Portfolio } from '@ghostfolio/api/models/portfolio';
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
import { IOrder } from '@ghostfolio/api/services/interfaces/interfaces';
import { RulesService } from '@ghostfolio/api/services/rules.service';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';
import { RequestWithUser } from 'apps/api/src/app/interfaces/request-with-user.type';
import { import {
add, add,
format, format,
@ -9,7 +15,6 @@ import {
getYear, getYear,
isAfter, isAfter,
isSameDay, isSameDay,
parse,
parseISO, parseISO,
setDate, setDate,
setMonth, setMonth,
@ -18,12 +23,6 @@ import {
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import * as roundTo from 'round-to'; import * as roundTo from 'round-to';
import { Portfolio } from '../../models/portfolio';
import { DataProviderService } from '../../services/data-provider.service';
import { ExchangeRateDataService } from '../../services/exchange-rate-data.service';
import { ImpersonationService } from '../../services/impersonation.service';
import { IOrder } from '../../services/interfaces/interfaces';
import { RulesService } from '../../services/rules.service';
import { OrderService } from '../order/order.service'; import { OrderService } from '../order/order.service';
import { RedisCacheService } from '../redis-cache/redis-cache.service'; import { RedisCacheService } from '../redis-cache/redis-cache.service';
import { UserService } from '../user/user.service'; import { UserService } from '../user/user.service';
@ -48,53 +47,6 @@ export class PortfolioService {
private readonly userService: UserService private readonly userService: UserService
) {} ) {}
private convertDateRangeToDate(aDateRange: DateRange, aMinDate: Date) {
let currentDate = new Date();
const normalizedMinDate =
getDate(aMinDate) === 1
? aMinDate
: add(setDate(aMinDate, 1), { months: 1 });
const year = getYear(currentDate);
const month = getMonth(currentDate);
const day = getDate(currentDate);
currentDate = new Date(Date.UTC(year, month, day, 0));
switch (aDateRange) {
case '1d':
return sub(currentDate, {
days: 1
});
case 'ytd':
currentDate = setDate(currentDate, 1);
currentDate = setMonth(currentDate, 0);
return isAfter(currentDate, normalizedMinDate)
? currentDate
: undefined;
case '1y':
currentDate = setDate(currentDate, 1);
currentDate = sub(currentDate, {
years: 1
});
return isAfter(currentDate, normalizedMinDate)
? currentDate
: undefined;
case '5y':
currentDate = setDate(currentDate, 1);
currentDate = sub(currentDate, {
years: 5
});
return isAfter(currentDate, normalizedMinDate)
? currentDate
: undefined;
default:
// Gets handled as all data
return undefined;
}
}
public async createPortfolio(aUserId: string): Promise<Portfolio> { public async createPortfolio(aUserId: string): Promise<Portfolio> {
let portfolio: Portfolio; let portfolio: Portfolio;
let stringifiedPortfolio = await this.redisCacheService.get( let stringifiedPortfolio = await this.redisCacheService.get(
@ -257,7 +209,8 @@ export class PortfolioService {
firstBuyDate, firstBuyDate,
investment, investment,
marketPrice, marketPrice,
quantity quantity,
transactionCount
} = portfolio.getPositions(new Date())[aSymbol]; } = portfolio.getPositions(new Date())[aSymbol];
const historicalData = await this.dataProviderService.getHistorical( const historicalData = await this.dataProviderService.getHistorical(
@ -310,6 +263,7 @@ export class PortfolioService {
maxPrice, maxPrice,
minPrice, minPrice,
quantity, quantity,
transactionCount,
grossPerformance: this.exchangeRateDataService.toCurrency( grossPerformance: this.exchangeRateDataService.toCurrency(
marketPrice - averagePrice, marketPrice - averagePrice,
currency, currency,
@ -363,7 +317,8 @@ export class PortfolioService {
maxPrice: undefined, maxPrice: undefined,
minPrice: undefined, minPrice: undefined,
quantity: undefined, quantity: undefined,
symbol: aSymbol symbol: aSymbol,
transactionCount: undefined
}; };
} }
@ -379,7 +334,55 @@ export class PortfolioService {
maxPrice: undefined, maxPrice: undefined,
minPrice: undefined, minPrice: undefined,
quantity: undefined, quantity: undefined,
symbol: aSymbol symbol: aSymbol,
transactionCount: undefined
}; };
} }
private convertDateRangeToDate(aDateRange: DateRange, aMinDate: Date) {
let currentDate = new Date();
const normalizedMinDate =
getDate(aMinDate) === 1
? aMinDate
: add(setDate(aMinDate, 1), { months: 1 });
const year = getYear(currentDate);
const month = getMonth(currentDate);
const day = getDate(currentDate);
currentDate = new Date(Date.UTC(year, month, day, 0));
switch (aDateRange) {
case '1d':
return sub(currentDate, {
days: 1
});
case 'ytd':
currentDate = setDate(currentDate, 1);
currentDate = setMonth(currentDate, 0);
return isAfter(currentDate, normalizedMinDate)
? currentDate
: undefined;
case '1y':
currentDate = setDate(currentDate, 1);
currentDate = sub(currentDate, {
years: 1
});
return isAfter(currentDate, normalizedMinDate)
? currentDate
: undefined;
case '5y':
currentDate = setDate(currentDate, 1);
currentDate = sub(currentDate, {
years: 5
});
return isAfter(currentDate, normalizedMinDate)
? currentDate
: undefined;
default:
// Gets handled as all data
return undefined;
}
}
} }

View File

@ -1,8 +1,8 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { CacheModule, Module } from '@nestjs/common'; import { CacheModule, Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config'; import { ConfigModule, ConfigService } from '@nestjs/config';
import * as redisStore from 'cache-manager-redis-store'; import * as redisStore from 'cache-manager-redis-store';
import { ConfigurationService } from '../../services/configuration.service';
import { RedisCacheService } from './redis-cache.service'; import { RedisCacheService } from './redis-cache.service';
@Module({ @Module({

View File

@ -1,8 +1,7 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { CACHE_MANAGER, Inject, Injectable } from '@nestjs/common'; import { CACHE_MANAGER, Inject, Injectable } from '@nestjs/common';
import { Cache } from 'cache-manager'; import { Cache } from 'cache-manager';
import { ConfigurationService } from '../../services/configuration.service';
@Injectable() @Injectable()
export class RedisCacheService { export class RedisCacheService {
public constructor( public constructor(

View File

@ -1,6 +1,7 @@
import { Currency } from '@prisma/client'; import { Currency, DataSource } from '@prisma/client';
export interface SymbolItem { export interface SymbolItem {
currency: Currency; currency: Currency;
dataSource: DataSource;
marketPrice: number; marketPrice: number;
} }

View File

@ -1,3 +1,4 @@
import { RequestWithUser } from '@ghostfolio/api/app/interfaces/request-with-user.type';
import { import {
Controller, Controller,
Get, Get,
@ -9,7 +10,6 @@ import {
} from '@nestjs/common'; } from '@nestjs/common';
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { RequestWithUser } from 'apps/api/src/app/interfaces/request-with-user.type';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { LookupItem } from './interfaces/lookup-item.interface'; import { LookupItem } from './interfaces/lookup-item.interface';

View File

@ -1,12 +1,12 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ConfigurationService } from '../../services/configuration.service';
import { DataProviderService } from '../../services/data-provider.service';
import { AlphaVantageService } from '../../services/data-provider/alpha-vantage/alpha-vantage.service';
import { GhostfolioScraperApiService } from '../../services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
import { RakutenRapidApiService } from '../../services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
import { YahooFinanceService } from '../../services/data-provider/yahoo-finance/yahoo-finance.service';
import { PrismaService } from '../../services/prisma.service';
import { SymbolController } from './symbol.controller'; import { SymbolController } from './symbol.controller';
import { SymbolService } from './symbol.service'; import { SymbolService } from './symbol.service';

View File

@ -1,40 +1,62 @@
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
import { convertFromYahooSymbol } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Currency } from '@prisma/client'; import { Currency } from '@prisma/client';
import { convertFromYahooSymbol } from 'apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service';
import * as bent from 'bent'; import * as bent from 'bent';
import { DataProviderService } from '../../services/data-provider.service';
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';
@Injectable() @Injectable()
export class SymbolService { export class SymbolService {
public constructor( public constructor(
private readonly dataProviderService: DataProviderService private readonly dataProviderService: DataProviderService,
private readonly ghostfolioScraperApiService: GhostfolioScraperApiService
) {} ) {}
public async get(aSymbol: string): Promise<SymbolItem> { public async get(aSymbol: string): Promise<SymbolItem> {
const response = await this.dataProviderService.get([aSymbol]); const response = await this.dataProviderService.get([aSymbol]);
const { currency, marketPrice } = response[aSymbol]; const { currency, dataSource, marketPrice } = response[aSymbol];
return { return {
dataSource,
marketPrice, marketPrice,
currency: <Currency>(<unknown>currency) currency: <Currency>(<unknown>currency)
}; };
} }
public async lookup(aQuery: string): Promise<LookupItem[]> { public async lookup(aQuery = ''): Promise<LookupItem[]> {
const query = aQuery.toLowerCase();
const results: LookupItem[] = [];
if (!query) {
return results;
}
const get = bent( const get = bent(
`https://query1.finance.yahoo.com/v1/finance/search?q=${aQuery}&lang=en-US&region=US&quotesCount=8&newsCount=0&enableFuzzyQuery=false&quotesQueryId=tss_match_phrase_query&multiQuoteQueryId=multi_quote_single_token_query&newsQueryId=news_cie_vespa&enableCb=true&enableNavLinks=false&enableEnhancedTrivialQuery=true`, `https://query1.finance.yahoo.com/v1/finance/search?q=${query}&lang=en-US&region=US&quotesCount=8&newsCount=0&enableFuzzyQuery=false&quotesQueryId=tss_match_phrase_query&multiQuoteQueryId=multi_quote_single_token_query&newsQueryId=news_cie_vespa&enableCb=true&enableNavLinks=false&enableEnhancedTrivialQuery=true`,
'GET', 'GET',
'json', 'json',
200 200
); );
// Add custom symbols
const scraperConfigurations = await this.ghostfolioScraperApiService.getScraperConfigurations();
scraperConfigurations.forEach((scraperConfiguration) => {
if (scraperConfiguration.name.toLowerCase().startsWith(query)) {
results.push({
name: scraperConfiguration.name,
symbol: scraperConfiguration.symbol
});
}
});
try { try {
const { quotes } = await get(); const { quotes } = await get();
return quotes const searchResult = quotes
.filter(({ isYahooFinance }) => { .filter(({ isYahooFinance }) => {
return isYahooFinance; return isYahooFinance;
}) })
@ -59,6 +81,8 @@ export class SymbolService {
symbol: convertFromYahooSymbol(symbol) symbol: convertFromYahooSymbol(symbol)
}; };
}); });
return results.concat(searchResult);
} catch (error) { } catch (error) {
console.error(error); console.error(error);

View File

@ -1,9 +1,10 @@
import { Currency } from '@prisma/client'; import { Account, Currency } from '@prisma/client';
import { Access } from './access.interface'; import { Access } from './access.interface';
export interface User { export interface User {
access: Access[]; access: Access[];
accounts: Account[];
alias?: string; alias?: string;
id: string; id: string;
permissions: string[]; permissions: string[];

View File

@ -1,3 +1,4 @@
import { RequestWithUser } from '@ghostfolio/api/app/interfaces/request-with-user.type';
import { getPermissions, hasPermission, permissions } from '@ghostfolio/helper'; import { getPermissions, hasPermission, permissions } from '@ghostfolio/helper';
import { import {
Body, Body,
@ -14,7 +15,6 @@ import { REQUEST } from '@nestjs/core';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { Provider } from '@prisma/client'; import { Provider } from '@prisma/client';
import { RequestWithUser } from 'apps/api/src/app/interfaces/request-with-user.type';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { UserItem } from './interfaces/user-item.interface'; import { UserItem } from './interfaces/user-item.interface';

View File

@ -1,8 +1,8 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt'; import { JwtModule } from '@nestjs/jwt';
import { ConfigurationService } from '../../services/configuration.service';
import { PrismaService } from '../../services/prisma.service';
import { UserController } from './user.controller'; import { UserController } from './user.controller';
import { UserService } from './user.service'; import { UserService } from './user.service';

View File

@ -1,3 +1,5 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { import {
getPermissions, getPermissions,
locale, locale,
@ -8,8 +10,6 @@ import { Injectable } from '@nestjs/common';
import { Currency, Prisma, Provider, User } from '@prisma/client'; import { Currency, Prisma, Provider, User } from '@prisma/client';
import { add } from 'date-fns'; import { add } from 'date-fns';
import { ConfigurationService } from '../../services/configuration.service';
import { PrismaService } from '../../services/prisma.service';
import { UserWithSettings } from '../interfaces/user-with-settings'; import { UserWithSettings } from '../interfaces/user-with-settings';
import { User as IUser } from './interfaces/user.interface'; import { User as IUser } from './interfaces/user.interface';
@ -25,6 +25,7 @@ export class UserService {
) {} ) {}
public async getUser({ public async getUser({
Account,
alias, alias,
id, id,
role, role,
@ -44,10 +45,6 @@ export class UserService {
currentPermissions.push(permissions.accessFearAndGreedIndex); currentPermissions.push(permissions.accessFearAndGreedIndex);
} }
if (this.configurationService.get('ENABLE_FEATURE_SOCIAL_LOGIN')) {
currentPermissions.push(permissions.useSocialLogin);
}
return { return {
alias, alias,
id, id,
@ -57,6 +54,7 @@ export class UserService {
id: accessItem.id id: accessItem.id
}; };
}), }),
accounts: Account,
permissions: currentPermissions, permissions: currentPermissions,
settings: { settings: {
baseCurrency: Settings?.currency || UserService.DEFAULT_CURRENCY, baseCurrency: Settings?.currency || UserService.DEFAULT_CURRENCY,
@ -73,7 +71,7 @@ export class UserService {
userWhereUniqueInput: Prisma.UserWhereUniqueInput userWhereUniqueInput: Prisma.UserWhereUniqueInput
): Promise<UserWithSettings | null> { ): Promise<UserWithSettings | null> {
const user = await this.prisma.user.findUnique({ const user = await this.prisma.user.findUnique({
include: { Settings: true }, include: { Account: true, Settings: true },
where: userWhereUniqueInput where: userWhereUniqueInput
}); });
@ -120,7 +118,15 @@ export class UserService {
public async createUser(data?: Prisma.UserCreateInput): Promise<User> { public async createUser(data?: Prisma.UserCreateInput): Promise<User> {
let user = await this.prisma.user.create({ let user = await this.prisma.user.create({
data data: {
...data,
Account: {
create: {
isDefault: true,
name: 'Default Account'
}
}
}
}); });
if (data.provider === Provider.ANONYMOUS) { if (data.provider === Provider.ANONYMOUS) {
@ -162,18 +168,6 @@ export class UserService {
}); });
} }
private getRandomString(length: number) {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
const result = [];
for (let i = 0; i < length; i++) {
result.push(
characters.charAt(Math.floor(Math.random() * characters.length))
);
}
return result.join('');
}
public async updateUserSettings({ public async updateUserSettings({
currency, currency,
userId userId
@ -200,4 +194,16 @@ export class UserService {
return; return;
} }
private getRandomString(length: number) {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
const result = [];
for (let i = 0; i < length; i++) {
result.push(
characters.charAt(Math.floor(Math.random() * characters.length))
);
}
return result.join('');
}
} }

View File

@ -1,7 +1,7 @@
import { import {
PortfolioItem, PortfolioItem,
Position Position
} from 'apps/api/src/app/portfolio/interfaces/portfolio-item.interface'; } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-item.interface';
import { Order } from '../order'; import { Order } from '../order';

View File

@ -1,4 +1,5 @@
import { PortfolioPosition } from '../../app/portfolio/interfaces/portfolio-position.interface'; import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
import { EvaluationResult } from './evaluation-result.interface'; import { EvaluationResult } from './evaluation-result.interface';
export interface RuleInterface { export interface RuleInterface {

View File

@ -5,18 +5,24 @@ import { Currency, Role, Type } from '@prisma/client';
import { ConfigurationService } from '../services/configuration.service'; import { ConfigurationService } from '../services/configuration.service';
import { DataProviderService } from '../services/data-provider.service'; import { DataProviderService } from '../services/data-provider.service';
import { AlphaVantageService } from '../services/data-provider/alpha-vantage/alpha-vantage.service'; import { AlphaVantageService } from '../services/data-provider/alpha-vantage/alpha-vantage.service';
import { GhostfolioScraperApiService } from '../services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
import { RakutenRapidApiService } from '../services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service'; import { RakutenRapidApiService } from '../services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
import { YahooFinanceService } from '../services/data-provider/yahoo-finance/yahoo-finance.service'; import { YahooFinanceService } from '../services/data-provider/yahoo-finance/yahoo-finance.service';
import { ExchangeRateDataService } from '../services/exchange-rate-data.service'; import { ExchangeRateDataService } from '../services/exchange-rate-data.service';
import { MarketState } from '../services/interfaces/interfaces';
import { PrismaService } from '../services/prisma.service'; import { PrismaService } from '../services/prisma.service';
import { RulesService } from '../services/rules.service'; import { RulesService } from '../services/rules.service';
import { Portfolio } from './portfolio'; import { Portfolio } from './portfolio';
const DEFAULT_ACCOUNT_ID = '693a834b-eb89-42c9-ae47-35196c25d269';
const USER_ID = 'ca6ce867-5d31-495a-bce9-5942bbca9237';
describe('Portfolio', () => { describe('Portfolio', () => {
let alphaVantageService: AlphaVantageService; let alphaVantageService: AlphaVantageService;
let configurationService: ConfigurationService; let configurationService: ConfigurationService;
let dataProviderService: DataProviderService; let dataProviderService: DataProviderService;
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
let ghostfolioScraperApiService: GhostfolioScraperApiService;
let portfolio: Portfolio; let portfolio: Portfolio;
let prismaService: PrismaService; let prismaService: PrismaService;
let rakutenRapidApiService: RakutenRapidApiService; let rakutenRapidApiService: RakutenRapidApiService;
@ -31,6 +37,7 @@ describe('Portfolio', () => {
ConfigurationService, ConfigurationService,
DataProviderService, DataProviderService,
ExchangeRateDataService, ExchangeRateDataService,
GhostfolioScraperApiService,
PrismaService, PrismaService,
RakutenRapidApiService, RakutenRapidApiService,
RulesService, RulesService,
@ -44,6 +51,9 @@ describe('Portfolio', () => {
exchangeRateDataService = app.get<ExchangeRateDataService>( exchangeRateDataService = app.get<ExchangeRateDataService>(
ExchangeRateDataService ExchangeRateDataService
); );
ghostfolioScraperApiService = app.get<GhostfolioScraperApiService>(
GhostfolioScraperApiService
);
prismaService = app.get<PrismaService>(PrismaService); prismaService = app.get<PrismaService>(PrismaService);
rakutenRapidApiService = app.get<RakutenRapidApiService>( rakutenRapidApiService = app.get<RakutenRapidApiService>(
RakutenRapidApiService RakutenRapidApiService
@ -62,13 +72,13 @@ describe('Portfolio', () => {
accessToken: null, accessToken: null,
alias: 'Test', alias: 'Test',
createdAt: new Date(), createdAt: new Date(),
id: '', id: USER_ID,
provider: null, provider: null,
role: Role.USER, role: Role.USER,
Settings: { Settings: {
currency: Currency.CHF, currency: Currency.CHF,
updatedAt: new Date(), updatedAt: new Date(),
userId: '' userId: USER_ID
}, },
thirdPartyId: null, thirdPartyId: null,
updatedAt: new Date() updatedAt: new Date()
@ -119,6 +129,8 @@ describe('Portfolio', () => {
it('should return ["BTC"]', async () => { it('should return ["BTC"]', async () => {
await portfolio.setOrders([ await portfolio.setOrders([
{ {
accountId: DEFAULT_ACCOUNT_ID,
accountUserId: USER_ID,
createdAt: null, createdAt: null,
currency: Currency.USD, currency: Currency.USD,
fee: 0, fee: 0,
@ -130,7 +142,7 @@ describe('Portfolio', () => {
type: Type.BUY, type: Type.BUY,
unitPrice: 49631.24, unitPrice: 49631.24,
updatedAt: null, updatedAt: null,
userId: null userId: USER_ID
} }
]); ]);
@ -154,8 +166,8 @@ describe('Portfolio', () => {
Currency.USD, Currency.USD,
baseCurrency baseCurrency
), ),
isMarketOpen: true,
// marketPrice: 57973.008, // marketPrice: 57973.008,
marketState: MarketState.open,
name: 'Bitcoin USD', name: 'Bitcoin USD',
platforms: { platforms: {
Other: { Other: {
@ -217,6 +229,8 @@ describe('Portfolio', () => {
it('should return ["ETHUSD"]', async () => { it('should return ["ETHUSD"]', async () => {
await portfolio.setOrders([ await portfolio.setOrders([
{ {
accountId: DEFAULT_ACCOUNT_ID,
accountUserId: USER_ID,
createdAt: null, createdAt: null,
currency: Currency.USD, currency: Currency.USD,
fee: 0, fee: 0,
@ -228,7 +242,7 @@ describe('Portfolio', () => {
type: Type.BUY, type: Type.BUY,
unitPrice: 991.49, unitPrice: 991.49,
updatedAt: null, updatedAt: null,
userId: null userId: USER_ID
} }
]); ]);
@ -269,7 +283,7 @@ describe('Portfolio', () => {
} }
}, },
quantity: 0.2, quantity: 0.2,
shareCurrent: 1, // shareCurrent: 1,
shareInvestment: 1, shareInvestment: 1,
symbol: 'ETHUSD', symbol: 'ETHUSD',
type: 'Cryptocurrency' type: 'Cryptocurrency'
@ -309,6 +323,8 @@ describe('Portfolio', () => {
it('should return ["ETHUSD"]', async () => { it('should return ["ETHUSD"]', async () => {
await portfolio.setOrders([ await portfolio.setOrders([
{ {
accountId: DEFAULT_ACCOUNT_ID,
accountUserId: USER_ID,
createdAt: null, createdAt: null,
currency: Currency.USD, currency: Currency.USD,
fee: 0, fee: 0,
@ -320,9 +336,11 @@ describe('Portfolio', () => {
type: Type.BUY, type: Type.BUY,
unitPrice: 991.49, unitPrice: 991.49,
updatedAt: null, updatedAt: null,
userId: null userId: USER_ID
}, },
{ {
accountId: DEFAULT_ACCOUNT_ID,
accountUserId: USER_ID,
createdAt: null, createdAt: null,
currency: Currency.USD, currency: Currency.USD,
fee: 0, fee: 0,
@ -334,7 +352,7 @@ describe('Portfolio', () => {
type: Type.BUY, type: Type.BUY,
unitPrice: 1050, unitPrice: 1050,
updatedAt: null, updatedAt: null,
userId: null userId: USER_ID
} }
]); ]);
@ -381,6 +399,8 @@ describe('Portfolio', () => {
it('should return ["BTCUSD", "ETHUSD"]', async () => { it('should return ["BTCUSD", "ETHUSD"]', async () => {
await portfolio.setOrders([ await portfolio.setOrders([
{ {
accountId: DEFAULT_ACCOUNT_ID,
accountUserId: USER_ID,
createdAt: null, createdAt: null,
currency: Currency.EUR, currency: Currency.EUR,
date: new Date(getUtc('2017-08-16')), date: new Date(getUtc('2017-08-16')),
@ -392,9 +412,11 @@ describe('Portfolio', () => {
type: Type.BUY, type: Type.BUY,
unitPrice: 3562.089535970158, unitPrice: 3562.089535970158,
updatedAt: null, updatedAt: null,
userId: null userId: USER_ID
}, },
{ {
accountId: DEFAULT_ACCOUNT_ID,
accountUserId: USER_ID,
createdAt: null, createdAt: null,
currency: Currency.USD, currency: Currency.USD,
fee: 2.99, fee: 2.99,
@ -406,7 +428,7 @@ describe('Portfolio', () => {
type: Type.BUY, type: Type.BUY,
unitPrice: 991.49, unitPrice: 991.49,
updatedAt: null, updatedAt: null,
userId: null userId: USER_ID
} }
]); ]);
@ -466,6 +488,8 @@ describe('Portfolio', () => {
it('should work with buy and sell', async () => { it('should work with buy and sell', async () => {
await portfolio.setOrders([ await portfolio.setOrders([
{ {
accountId: DEFAULT_ACCOUNT_ID,
accountUserId: USER_ID,
createdAt: null, createdAt: null,
currency: Currency.USD, currency: Currency.USD,
fee: 1.0, fee: 1.0,
@ -477,9 +501,11 @@ describe('Portfolio', () => {
type: Type.BUY, type: Type.BUY,
unitPrice: 991.49, unitPrice: 991.49,
updatedAt: null, updatedAt: null,
userId: null userId: USER_ID
}, },
{ {
accountId: DEFAULT_ACCOUNT_ID,
accountUserId: USER_ID,
createdAt: null, createdAt: null,
currency: Currency.USD, currency: Currency.USD,
fee: 1.0, fee: 1.0,
@ -491,9 +517,11 @@ describe('Portfolio', () => {
type: Type.SELL, type: Type.SELL,
unitPrice: 1050, unitPrice: 1050,
updatedAt: null, updatedAt: null,
userId: null userId: USER_ID
}, },
{ {
accountId: DEFAULT_ACCOUNT_ID,
accountUserId: USER_ID,
createdAt: null, createdAt: null,
currency: Currency.USD, currency: Currency.USD,
fee: 1.0, fee: 1.0,
@ -505,7 +533,7 @@ describe('Portfolio', () => {
type: Type.BUY, type: Type.BUY,
unitPrice: 1050, unitPrice: 1050,
updatedAt: null, updatedAt: null,
userId: null userId: USER_ID
} }
]); ]);

View File

@ -1,8 +1,8 @@
import { getToday, getYesterday, resetHours } from '@ghostfolio/helper';
import { import {
PortfolioItem, PortfolioItem,
Position Position
} from 'apps/api/src/app/portfolio/interfaces/portfolio-item.interface'; } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-item.interface';
import { getToday, getYesterday, resetHours } from '@ghostfolio/helper';
import { import {
add, add,
format, format,
@ -57,7 +57,7 @@ export class Portfolio implements PortfolioInterface {
public async addCurrentPortfolioItems() { public async addCurrentPortfolioItems() {
const currentData = await this.dataProviderService.get(this.getSymbols()); const currentData = await this.dataProviderService.get(this.getSymbols());
let currentDate = new Date(); const currentDate = new Date();
const year = getYear(currentDate); const year = getYear(currentDate);
const month = getMonth(currentDate); const month = getMonth(currentDate);
@ -79,8 +79,12 @@ export class Portfolio implements PortfolioInterface {
investmentInOriginalCurrency: investmentInOriginalCurrency:
portfolioItemsYesterday?.positions[symbol] portfolioItemsYesterday?.positions[symbol]
?.investmentInOriginalCurrency, ?.investmentInOriginalCurrency,
marketPrice: currentData[symbol]?.marketPrice, marketPrice:
quantity: portfolioItemsYesterday?.positions[symbol]?.quantity currentData[symbol]?.marketPrice ??
portfolioItemsYesterday.positions[symbol]?.marketPrice,
quantity: portfolioItemsYesterday?.positions[symbol]?.quantity,
transactionCount:
portfolioItemsYesterday?.positions[symbol]?.transactionCount
}; };
}); });
@ -158,53 +162,6 @@ export class Portfolio implements PortfolioInterface {
return this; return this;
} }
private convertDateRangeToDate(aDateRange: DateRange, aMinDate: Date) {
let currentDate = new Date();
const normalizedMinDate =
getDate(aMinDate) === 1
? aMinDate
: add(setDate(aMinDate, 1), { months: 1 });
const year = getYear(currentDate);
const month = getMonth(currentDate);
const day = getDate(currentDate);
currentDate = new Date(Date.UTC(year, month, day, 0));
switch (aDateRange) {
case '1d':
return sub(currentDate, {
days: 1
});
case 'ytd':
currentDate = setDate(currentDate, 1);
currentDate = setMonth(currentDate, 0);
return isAfter(currentDate, normalizedMinDate)
? currentDate
: undefined;
case '1y':
currentDate = setDate(currentDate, 1);
currentDate = sub(currentDate, {
years: 1
});
return isAfter(currentDate, normalizedMinDate)
? currentDate
: undefined;
case '5y':
currentDate = setDate(currentDate, 1);
currentDate = sub(currentDate, {
years: 5
});
return isAfter(currentDate, normalizedMinDate)
? currentDate
: undefined;
default:
// Gets handled as all data
return undefined;
}
}
public get(aDate?: Date): PortfolioItem[] { public get(aDate?: Date): PortfolioItem[] {
if (aDate) { if (aDate) {
const filteredPortfolio = this.portfolioItems.find((item) => { const filteredPortfolio = this.portfolioItems.find((item) => {
@ -334,7 +291,9 @@ export class Portfolio implements PortfolioInterface {
data[symbol]?.currency, data[symbol]?.currency,
this.user.Settings.currency this.user.Settings.currency
) / value, ) / value,
shareInvestment: portfolioItem.positions[symbol].investment / investment shareInvestment:
portfolioItem.positions[symbol].investment / investment,
transactionCount: portfolioItem.positions[symbol].transactionCount
}; };
}); });
@ -528,12 +487,6 @@ export class Portfolio implements PortfolioInterface {
return this.orders; return this.orders;
} }
private getOrdersByType(aFilter: string[]) {
return this.orders.filter((order) => {
return aFilter.includes(order.getType());
});
}
public getValue(aDate = getToday()) { public getValue(aDate = getToday()) {
const positions = this.getPositions(aDate); const positions = this.getPositions(aDate);
let value = 0; let value = 0;
@ -633,7 +586,8 @@ export class Portfolio implements PortfolioInterface {
marketPrice: marketPrice:
historicalData[symbol]?.[format(currentDate, 'yyyy-MM-dd')] historicalData[symbol]?.[format(currentDate, 'yyyy-MM-dd')]
?.marketPrice || 0, ?.marketPrice || 0,
quantity: 0 quantity: 0,
transactionCount: 0
}; };
}); });
@ -674,7 +628,8 @@ export class Portfolio implements PortfolioInterface {
marketPrice: marketPrice:
historicalData[symbol]?.[format(yesterday, 'yyyy-MM-dd')] historicalData[symbol]?.[format(yesterday, 'yyyy-MM-dd')]
?.marketPrice || 0, ?.marketPrice || 0,
quantity: 0 quantity: 0,
transactionCount: 0
}; };
}); });
@ -692,6 +647,53 @@ export class Portfolio implements PortfolioInterface {
this.updatePortfolioItems(); this.updatePortfolioItems();
} }
private convertDateRangeToDate(aDateRange: DateRange, aMinDate: Date) {
let currentDate = new Date();
const normalizedMinDate =
getDate(aMinDate) === 1
? aMinDate
: add(setDate(aMinDate, 1), { months: 1 });
const year = getYear(currentDate);
const month = getMonth(currentDate);
const day = getDate(currentDate);
currentDate = new Date(Date.UTC(year, month, day, 0));
switch (aDateRange) {
case '1d':
return sub(currentDate, {
days: 1
});
case 'ytd':
currentDate = setDate(currentDate, 1);
currentDate = setMonth(currentDate, 0);
return isAfter(currentDate, normalizedMinDate)
? currentDate
: undefined;
case '1y':
currentDate = setDate(currentDate, 1);
currentDate = sub(currentDate, {
years: 1
});
return isAfter(currentDate, normalizedMinDate)
? currentDate
: undefined;
case '5y':
currentDate = setDate(currentDate, 1);
currentDate = sub(currentDate, {
years: 5
});
return isAfter(currentDate, normalizedMinDate)
? currentDate
: undefined;
default:
// Gets handled as all data
return undefined;
}
}
private updatePortfolioItems() { private updatePortfolioItems() {
// console.time('update-portfolio-items'); // console.time('update-portfolio-items');
@ -734,6 +736,10 @@ export class Portfolio implements PortfolioInterface {
order.getSymbol() order.getSymbol()
].currency = order.getCurrency(); ].currency = order.getCurrency();
this.portfolioItems[i].positions[
order.getSymbol()
].transactionCount += 1;
if (order.getType() === 'BUY') { if (order.getType() === 'BUY') {
if ( if (
!this.portfolioItems[i].positions[order.getSymbol()].firstBuyDate !this.portfolioItems[i].positions[order.getSymbol()].firstBuyDate

View File

@ -1,5 +1,5 @@
import { PortfolioPosition } from 'apps/api/src/app/portfolio/interfaces/portfolio-position.interface'; import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { Rule } from '../../rule'; import { Rule } from '../../rule';

View File

@ -1,4 +1,4 @@
import { PortfolioPosition } from 'apps/api/src/app/portfolio/interfaces/portfolio-position.interface'; import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service'; import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
import { Rule } from '../../rule'; import { Rule } from '../../rule';

View File

@ -1,4 +1,4 @@
import { PortfolioPosition } from 'apps/api/src/app/portfolio/interfaces/portfolio-position.interface'; import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service'; import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
import { Rule } from '../../rule'; import { Rule } from '../../rule';

View File

@ -1,4 +1,4 @@
import { PortfolioPosition } from 'apps/api/src/app/portfolio/interfaces/portfolio-position.interface'; import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service'; import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
import { Rule } from '../../rule'; import { Rule } from '../../rule';

View File

@ -1,4 +1,4 @@
import { PortfolioPosition } from 'apps/api/src/app/portfolio/interfaces/portfolio-position.interface'; import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service'; import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
import { Rule } from '../../rule'; import { Rule } from '../../rule';

View File

@ -1,5 +1,5 @@
import { PortfolioPosition } from 'apps/api/src/app/portfolio/interfaces/portfolio-position.interface'; import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { Rule } from '../../rule'; import { Rule } from '../../rule';

View File

@ -1,4 +1,4 @@
import { PortfolioPosition } from 'apps/api/src/app/portfolio/interfaces/portfolio-position.interface'; import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service'; import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
import { Rule } from '../../rule'; import { Rule } from '../../rule';

View File

@ -1,4 +1,4 @@
import { PortfolioPosition } from 'apps/api/src/app/portfolio/interfaces/portfolio-position.interface'; import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service'; import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
import { Rule } from '../../rule'; import { Rule } from '../../rule';

View File

@ -15,6 +15,7 @@ export class ConfigurationService {
ENABLE_FEATURE_CUSTOM_SYMBOLS: bool({ default: false }), ENABLE_FEATURE_CUSTOM_SYMBOLS: bool({ default: false }),
ENABLE_FEATURE_FEAR_AND_GREED_INDEX: bool({ default: false }), ENABLE_FEATURE_FEAR_AND_GREED_INDEX: bool({ default: false }),
ENABLE_FEATURE_SOCIAL_LOGIN: bool({ default: false }), ENABLE_FEATURE_SOCIAL_LOGIN: bool({ default: false }),
ENABLE_FEATURE_SUBSCRIPTION: bool({ default: false }),
GOOGLE_CLIENT_ID: str({ default: 'dummyClientId' }), GOOGLE_CLIENT_ID: str({ default: 'dummyClientId' }),
GOOGLE_SECRET: str({ default: 'dummySecret' }), GOOGLE_SECRET: str({ default: 'dummySecret' }),
JWT_SECRET_KEY: str({}), JWT_SECRET_KEY: str({}),

View File

@ -2,6 +2,7 @@ import {
benchmarks, benchmarks,
currencyPairs, currencyPairs,
getUtc, getUtc,
isGhostfolioScraperApiSymbol,
resetHours resetHours
} from '@ghostfolio/helper'; } from '@ghostfolio/helper';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
@ -17,6 +18,7 @@ import {
import { ConfigurationService } from './configuration.service'; import { ConfigurationService } from './configuration.service';
import { DataProviderService } from './data-provider.service'; import { DataProviderService } from './data-provider.service';
import { GhostfolioScraperApiService } from './data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
import { PrismaService } from './prisma.service'; import { PrismaService } from './prisma.service';
@Injectable() @Injectable()
@ -24,6 +26,7 @@ export class DataGatheringService {
public constructor( public constructor(
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly dataProviderService: DataProviderService, private readonly dataProviderService: DataProviderService,
private readonly ghostfolioScraperApi: GhostfolioScraperApiService,
private prisma: PrismaService private prisma: PrismaService
) {} ) {}
@ -182,6 +185,17 @@ export class DataGatheringService {
} }
} }
public async getCustomSymbolsToGather(startDate?: Date) {
const scraperConfigurations = await this.ghostfolioScraperApi.getScraperConfigurations();
return scraperConfigurations.map((scraperConfiguration) => {
return {
date: startDate,
symbol: scraperConfiguration.symbol
};
});
}
private getBenchmarksToGather(startDate: Date) { private getBenchmarksToGather(startDate: Date) {
const benchmarksToGather = benchmarks.map((symbol) => { const benchmarksToGather = benchmarks.map((symbol) => {
return { return {
@ -200,32 +214,6 @@ export class DataGatheringService {
return benchmarksToGather; return benchmarksToGather;
} }
private async getCustomSymbolsToGather(startDate: Date) {
const customSymbolsToGather = [];
if (this.configurationService.get('ENABLE_FEATURE_CUSTOM_SYMBOLS')) {
try {
const {
value: scraperConfigString
} = await this.prisma.property.findFirst({
select: {
value: true
},
where: { key: 'SCRAPER_CONFIG' }
});
JSON.parse(scraperConfigString).forEach((item) => {
customSymbolsToGather.push({
date: startDate,
symbol: item.symbol
});
});
} catch {}
}
return customSymbolsToGather;
}
private async getSymbols7D(): Promise<{ date: Date; symbol: string }[]> { private async getSymbols7D(): Promise<{ date: Date; symbol: string }[]> {
const startDate = subDays(resetHours(new Date()), 7); const startDate = subDays(resetHours(new Date()), 7);
@ -235,7 +223,11 @@ export class DataGatheringService {
select: { symbol: true } select: { symbol: true }
}); });
const distinctOrdersWithDate = distinctOrders.map((distinctOrder) => { const distinctOrdersWithDate = distinctOrders
.filter((distinctOrder) => {
return !isGhostfolioScraperApiSymbol(distinctOrder.symbol);
})
.map((distinctOrder) => {
return { return {
...distinctOrder, ...distinctOrder,
date: startDate date: startDate
@ -262,7 +254,7 @@ export class DataGatheringService {
} }
private async getSymbolsMax() { private async getSymbolsMax() {
const startDate = new Date(getUtc('2000-01-01')); const startDate = new Date(getUtc('2015-01-01'));
const customSymbolsToGather = await this.getCustomSymbolsToGather( const customSymbolsToGather = await this.getCustomSymbolsToGather(
startDate startDate

View File

@ -1,7 +1,7 @@
import { import {
isCrypto, isCrypto,
isGhostfolioScraperApi, isGhostfolioScraperApiSymbol,
isRakutenRapidApi isRakutenRapidApiSymbol
} from '@ghostfolio/helper'; } from '@ghostfolio/helper';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { MarketData } from '@prisma/client'; import { MarketData } from '@prisma/client';
@ -39,14 +39,33 @@ export class DataProviderService implements DataProviderInterface {
if (aSymbols.length === 1) { if (aSymbols.length === 1) {
const symbol = aSymbols[0]; const symbol = aSymbols[0];
if (isGhostfolioScraperApi(symbol)) { if (isGhostfolioScraperApiSymbol(symbol)) {
return this.ghostfolioScraperApiService.get(aSymbols); return this.ghostfolioScraperApiService.get(aSymbols);
} else if (isRakutenRapidApi(symbol)) { } else if (isRakutenRapidApiSymbol(symbol)) {
return this.rakutenRapidApiService.get(aSymbols); return this.rakutenRapidApiService.get(aSymbols);
} }
} }
return this.yahooFinanceService.get(aSymbols); const yahooFinanceSymbols = aSymbols.filter((symbol) => {
return !isGhostfolioScraperApiSymbol(symbol);
});
const response = await this.yahooFinanceService.get(yahooFinanceSymbols);
const ghostfolioScraperApiSymbols = aSymbols.filter((symbol) => {
return isGhostfolioScraperApiSymbol(symbol);
});
for (const symbol of ghostfolioScraperApiSymbols) {
if (symbol) {
const ghostfolioScraperApiResult = await this.ghostfolioScraperApiService.get(
[symbol]
);
response[symbol] = ghostfolioScraperApiResult[symbol];
}
}
return response;
} }
public async getHistorical( public async getHistorical(
@ -107,8 +126,12 @@ export class DataProviderService implements DataProviderInterface {
): Promise<{ ): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
}> { }> {
const filteredSymbols = aSymbols.filter((symbol) => {
return !isGhostfolioScraperApiSymbol(symbol);
});
const dataOfYahoo = await this.yahooFinanceService.getHistorical( const dataOfYahoo = await this.yahooFinanceService.getHistorical(
aSymbols, filteredSymbols,
undefined, undefined,
from, from,
to to
@ -135,7 +158,7 @@ export class DataProviderService implements DataProviderInterface {
...dataOfAlphaVantage[symbol] ...dataOfAlphaVantage[symbol]
} }
}; };
} else if (isGhostfolioScraperApi(symbol)) { } else if (isGhostfolioScraperApiSymbol(symbol)) {
const dataOfGhostfolioScraperApi = await this.ghostfolioScraperApiService.getHistorical( const dataOfGhostfolioScraperApi = await this.ghostfolioScraperApiService.getHistorical(
[symbol], [symbol],
undefined, undefined,
@ -145,7 +168,7 @@ export class DataProviderService implements DataProviderInterface {
return dataOfGhostfolioScraperApi; return dataOfGhostfolioScraperApi;
} else if ( } else if (
isRakutenRapidApi(symbol) && isRakutenRapidApiSymbol(symbol) &&
this.configurationService.get('RAKUTEN_RAPID_API_KEY') this.configurationService.get('RAKUTEN_RAPID_API_KEY')
) { ) {
const dataOfRakutenRapidApi = await this.rakutenRapidApiService.getHistorical( const dataOfRakutenRapidApi = await this.rakutenRapidApiService.getHistorical(

View File

@ -1,3 +1,4 @@
import { DataSource } from '.prisma/client';
import { getYesterday } from '@ghostfolio/helper'; import { getYesterday } from '@ghostfolio/helper';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import * as bent from 'bent'; import * as bent from 'bent';
@ -8,13 +9,16 @@ import { DataProviderInterface } from '../../interfaces/data-provider.interface'
import { Granularity } from '../../interfaces/granularity.type'; import { Granularity } from '../../interfaces/granularity.type';
import { import {
IDataProviderHistoricalResponse, IDataProviderHistoricalResponse,
IDataProviderResponse IDataProviderResponse,
MarketState
} from '../../interfaces/interfaces'; } from '../../interfaces/interfaces';
import { PrismaService } from '../../prisma.service'; import { PrismaService } from '../../prisma.service';
import { Currency } from '.prisma/client'; import { ScraperConfig } from './interfaces/scraper-config.interface';
@Injectable() @Injectable()
export class GhostfolioScraperApiService implements DataProviderInterface { export class GhostfolioScraperApiService implements DataProviderInterface {
private static NUMERIC_REGEXP = /[-]{0,1}[\d]*[.,]{0,1}[\d]+/g;
public constructor(private prisma: PrismaService) {} public constructor(private prisma: PrismaService) {}
public async get( public async get(
@ -26,6 +30,9 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
try { try {
const symbol = aSymbols[0]; const symbol = aSymbols[0];
const scraperConfig = await this.getScraperConfigurationBySymbol(symbol);
const { marketPrice } = await this.prisma.marketData.findFirst({ const { marketPrice } = await this.prisma.marketData.findFirst({
orderBy: { orderBy: {
date: 'desc' date: 'desc'
@ -38,9 +45,10 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
return { return {
[symbol]: { [symbol]: {
marketPrice, marketPrice,
currency: Currency.CHF, currency: scraperConfig?.currency,
isMarketOpen: true, dataSource: DataSource.GHOSTFOLIO,
name: symbol marketState: MarketState.delayed,
name: scraperConfig?.name
} }
}; };
} catch (error) { } catch (error) {
@ -65,27 +73,18 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
try { try {
const symbol = aSymbols[0]; const symbol = aSymbols[0];
const { const scraperConfiguration = await this.getScraperConfigurationBySymbol(
value: scraperConfigString symbol
} = await this.prisma.property.findFirst({ );
select: {
value: true
},
where: { key: 'SCRAPER_CONFIG' }
});
const scraperConfig = JSON.parse(scraperConfigString).find((item) => { const get = bent(scraperConfiguration?.url, 'GET', 'string', 200, {});
return item.symbol === symbol;
});
const get = bent(scraperConfig.url, 'GET', 'string', 200, {});
const html = await get(); const html = await get();
const $ = cheerio.load(html); const $ = cheerio.load(html);
const string = $(scraperConfig.selector).text().replace('CHF', '').trim(); const value = this.extractNumberFromString(
$(scraperConfiguration?.selector).text()
const value = parseFloat(string); );
return { return {
[symbol]: { [symbol]: {
@ -100,4 +99,39 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
return {}; return {};
} }
public async getScraperConfigurations(): Promise<ScraperConfig[]> {
try {
const {
value: scraperConfigString
} = await this.prisma.property.findFirst({
select: {
value: true
},
where: { key: 'SCRAPER_CONFIG' }
});
return JSON.parse(scraperConfigString);
} catch {}
return [];
}
private extractNumberFromString(aString: string): number {
try {
const [numberString] = aString.match(
GhostfolioScraperApiService.NUMERIC_REGEXP
);
return parseFloat(numberString.trim());
} catch {
return undefined;
}
}
private async getScraperConfigurationBySymbol(aSymbol: string) {
const scraperConfigurations = await this.getScraperConfigurations();
return scraperConfigurations.find((scraperConfiguration) => {
return scraperConfiguration.symbol === aSymbol;
});
}
} }

View File

@ -0,0 +1,9 @@
import { Currency } from '.prisma/client';
export interface ScraperConfig {
currency: Currency;
name: string;
selector: string;
symbol: string;
url: string;
}

View File

@ -1,3 +1,4 @@
import { DataSource } from '.prisma/client';
import { getToday, getYesterday } from '@ghostfolio/helper'; import { getToday, getYesterday } from '@ghostfolio/helper';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import * as bent from 'bent'; import * as bent from 'bent';
@ -8,7 +9,8 @@ import { DataProviderInterface } from '../../interfaces/data-provider.interface'
import { Granularity } from '../../interfaces/granularity.type'; import { Granularity } from '../../interfaces/granularity.type';
import { import {
IDataProviderHistoricalResponse, IDataProviderHistoricalResponse,
IDataProviderResponse IDataProviderResponse,
MarketState
} from '../../interfaces/interfaces'; } from '../../interfaces/interfaces';
import { PrismaService } from '../../prisma.service'; import { PrismaService } from '../../prisma.service';
@ -38,8 +40,9 @@ export class RakutenRapidApiService implements DataProviderInterface {
return { return {
'GF.FEAR_AND_GREED_INDEX': { 'GF.FEAR_AND_GREED_INDEX': {
currency: undefined, currency: undefined,
isMarketOpen: true, dataSource: DataSource.RAKUTEN,
marketPrice: fgi.now.value, marketPrice: fgi.now.value,
marketState: MarketState.open,
name: RakutenRapidApiService.FEAR_AND_GREED_INDEX_NAME name: RakutenRapidApiService.FEAR_AND_GREED_INDEX_NAME
} }
}; };

View File

@ -1,3 +1,4 @@
import { DataSource } from '.prisma/client';
import { isCrypto, isCurrency, parseCurrency } from '@ghostfolio/helper'; import { isCrypto, isCurrency, parseCurrency } from '@ghostfolio/helper';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { format } from 'date-fns'; import { format } from 'date-fns';
@ -9,6 +10,7 @@ import {
IDataProviderHistoricalResponse, IDataProviderHistoricalResponse,
IDataProviderResponse, IDataProviderResponse,
Industry, Industry,
MarketState,
Sector, Sector,
Type Type
} from '../../interfaces/interfaces'; } from '../../interfaces/interfaces';
@ -48,9 +50,12 @@ export class YahooFinanceService implements DataProviderInterface {
response[symbol] = { response[symbol] = {
currency: parseCurrency(value.price?.currency), currency: parseCurrency(value.price?.currency),
dataSource: DataSource.YAHOO,
exchange: this.parseExchange(value.price?.exchangeName), exchange: this.parseExchange(value.price?.exchangeName),
isMarketOpen: marketState:
value.price?.marketState === 'REGULAR' || isCrypto(symbol), value.price?.marketState === 'REGULAR' || isCrypto(symbol)
? MarketState.open
: MarketState.closed,
marketPrice: value.price?.regularMarketPrice || 0, marketPrice: value.price?.regularMarketPrice || 0,
name: value.price?.longName || value.price?.shortName || symbol, name: value.price?.longName || value.price?.shortName || symbol,
type: this.parseType(this.getType(symbol, value)) type: this.parseType(this.getType(symbol, value))

View File

@ -7,6 +7,7 @@ export interface Environment extends CleanedEnvAccessors {
ENABLE_FEATURE_CUSTOM_SYMBOLS: boolean; ENABLE_FEATURE_CUSTOM_SYMBOLS: boolean;
ENABLE_FEATURE_FEAR_AND_GREED_INDEX: boolean; ENABLE_FEATURE_FEAR_AND_GREED_INDEX: boolean;
ENABLE_FEATURE_SOCIAL_LOGIN: boolean; ENABLE_FEATURE_SOCIAL_LOGIN: boolean;
ENABLE_FEATURE_SUBSCRIPTION: boolean;
GOOGLE_CLIENT_ID: string; GOOGLE_CLIENT_ID: string;
GOOGLE_SECRET: string; GOOGLE_SECRET: string;
JWT_SECRET_KEY: string; JWT_SECRET_KEY: string;

View File

@ -1,4 +1,4 @@
import { Currency, Platform } from '@prisma/client'; import { Currency, DataSource, Platform } from '@prisma/client';
import { OrderType } from '../../models/order-type'; import { OrderType } from '../../models/order-type';
@ -12,6 +12,12 @@ export const Industry = {
Software: 'Software' Software: 'Software'
}; };
export const MarketState = {
closed: 'closed',
delayed: 'delayed',
open: 'open'
};
export const Sector = { export const Sector = {
Consumer: 'Consumer', Consumer: 'Consumer',
Healthcare: 'Healthcare', Healthcare: 'Healthcare',
@ -45,12 +51,13 @@ export interface IDataProviderHistoricalResponse {
export interface IDataProviderResponse { export interface IDataProviderResponse {
currency: Currency; currency: Currency;
dataSource: DataSource;
exchange?: string; exchange?: string;
industry?: Industry; industry?: Industry;
isMarketOpen: boolean;
marketChange?: number; marketChange?: number;
marketChangePercent?: number; marketChangePercent?: number;
marketPrice: number; marketPrice: number;
marketState: MarketState;
name: string; name: string;
sector?: Sector; sector?: Sector;
type?: Type; type?: Type;
@ -59,6 +66,8 @@ export interface IDataProviderResponse {
export type Industry = typeof Industry[keyof typeof Industry]; export type Industry = typeof Industry[keyof typeof Industry];
export type MarketState = typeof MarketState[keyof typeof MarketState];
export type Sector = typeof Sector[keyof typeof Sector]; export type Sector = typeof Sector[keyof typeof Sector];
export type Type = typeof Type[keyof typeof Type]; export type Type = typeof Type[keyof typeof Type];

View File

@ -2,7 +2,8 @@ import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
@Injectable() @Injectable()
export class PrismaService extends PrismaClient export class PrismaService
extends PrismaClient
implements OnModuleInit, OnModuleDestroy { implements OnModuleInit, OnModuleDestroy {
async onModuleInit() { async onModuleInit() {
await this.$connect(); await this.$connect();

View File

@ -81,8 +81,7 @@ const routes: Routes = [
{ {
preloadingStrategy: ModulePreloadService, preloadingStrategy: ModulePreloadService,
// enableTracing: true // <-- debugging purposes only // enableTracing: true // <-- debugging purposes only
relativeLinkResolution: 'legacy', relativeLinkResolution: 'legacy'
scrollPositionRestoration: 'enabled'
} }
) )
], ],

View File

@ -6,6 +6,8 @@ import {
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router'; import { NavigationEnd, Router } from '@angular/router';
import { InfoItem } from '@ghostfolio/api/app/info/interfaces/info-item.interface';
import { User } from '@ghostfolio/api/app/user/interfaces/user.interface';
import { import {
hasPermission, hasPermission,
permissions, permissions,
@ -13,8 +15,6 @@ import {
secondaryColorHex secondaryColorHex
} from '@ghostfolio/helper'; } from '@ghostfolio/helper';
import { MaterialCssVarsService } from 'angular-material-css-vars'; import { MaterialCssVarsService } from 'angular-material-css-vars';
import { InfoItem } from 'apps/api/src/app/info/interfaces/info-item.interface';
import { User } from 'apps/api/src/app/user/interfaces/user.interface';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators'; import { filter, takeUntil } from 'rxjs/operators';

View File

@ -6,7 +6,7 @@ import {
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { Access } from 'apps/api/src/app/access/interfaces/access.interface'; import { Access } from '@ghostfolio/api/app/access/interfaces/access.interface';
@Component({ @Component({
selector: 'gf-access-table', selector: 'gf-access-table',

View File

@ -1,3 +1,9 @@
<gf-symbol-icon
*ngIf="symbolUrl"
class="mr-1"
size="large"
[url]="symbolUrl"
></gf-symbol-icon>
<span class="flex-grow-1 text-truncate">{{ title }}</span> <span class="flex-grow-1 text-truncate">{{ title }}</span>
<button <button
*ngIf="deviceType !== 'mobile'" *ngIf="deviceType !== 'mobile'"

View File

@ -1,3 +1,4 @@
:host { :host {
align-items: center;
display: flex; display: flex;
} }

View File

@ -16,6 +16,7 @@ import {
}) })
export class DialogHeaderComponent implements OnInit { export class DialogHeaderComponent implements OnInit {
@Input() deviceType: string; @Input() deviceType: string;
@Input() symbolUrl: string;
@Input() title: string; @Input() title: string;
@Output() closeButtonClicked = new EventEmitter<void>(); @Output() closeButtonClicked = new EventEmitter<void>();

View File

@ -1,13 +1,14 @@
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 { MatButtonModule } from '@angular/material/button';
import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module';
import { DialogHeaderComponent } from './dialog-header.component'; import { DialogHeaderComponent } from './dialog-header.component';
@NgModule({ @NgModule({
declarations: [DialogHeaderComponent], declarations: [DialogHeaderComponent],
exports: [DialogHeaderComponent], exports: [DialogHeaderComponent],
imports: [CommonModule, MatButtonModule], imports: [CommonModule, GfSymbolIconModule, MatButtonModule],
providers: [], providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })

View File

@ -37,7 +37,7 @@
>Transactions</a >Transactions</a
> >
<a <a
*ngIf="canAccessAdminAccessControl" *ngIf="hasPermissionToAccessAdminControl"
class="d-none d-sm-block mx-1" class="d-none d-sm-block mx-1"
[routerLink]="['/admin']" [routerLink]="['/admin']"
i18n i18n
@ -150,7 +150,7 @@
>Account</a >Account</a
> >
<a <a
*ngIf="canAccessAdminAccessControl" *ngIf="hasPermissionToAccessAdminControl"
class="d-block d-sm-none" class="d-block d-sm-none"
[routerLink]="['/admin']" [routerLink]="['/admin']"
i18n i18n

View File

@ -6,17 +6,16 @@ import {
} from '@angular/core'; } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { InfoItem } from '@ghostfolio/api/app/info/interfaces/info-item.interface';
import { User } from '@ghostfolio/api/app/user/interfaces/user.interface';
import { LoginWithAccessTokenDialog } from '@ghostfolio/client/pages/login/login-with-access-token-dialog/login-with-access-token-dialog.component';
import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { hasPermission, permissions } from '@ghostfolio/helper'; import { hasPermission, permissions } from '@ghostfolio/helper';
import { InfoItem } from 'apps/api/src/app/info/interfaces/info-item.interface';
import { User } from 'apps/api/src/app/user/interfaces/user.interface';
import { EMPTY, Subject } from 'rxjs'; import { EMPTY, Subject } from 'rxjs';
import { catchError, takeUntil } from 'rxjs/operators'; import { catchError, takeUntil } from 'rxjs/operators';
import { LoginWithAccessTokenDialog } from '../../pages/login/login-with-access-token-dialog/login-with-access-token-dialog.component';
import { DataService } from '../../services/data.service';
import { ImpersonationStorageService } from '../../services/impersonation-storage.service';
import { TokenStorageService } from '../../services/token-storage.service';
@Component({ @Component({
selector: 'gf-header', selector: 'gf-header',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@ -28,8 +27,8 @@ export class HeaderComponent implements OnChanges {
@Input() info: InfoItem; @Input() info: InfoItem;
@Input() user: User; @Input() user: User;
public canAccessAdminAccessControl: boolean; public hasPermissionToAccessAdminControl: boolean;
public hasPermissionToUseSocialLogin: boolean; public hasPermissionForSocialLogin: boolean;
public impersonationId: string; public impersonationId: string;
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
@ -50,15 +49,15 @@ export class HeaderComponent implements OnChanges {
public ngOnChanges() { public ngOnChanges() {
if (this.user) { if (this.user) {
this.canAccessAdminAccessControl = hasPermission( this.hasPermissionToAccessAdminControl = hasPermission(
this.user.permissions, this.user.permissions,
permissions.accessAdminControl permissions.accessAdminControl
); );
} }
this.hasPermissionToUseSocialLogin = hasPermission( this.hasPermissionForSocialLogin = hasPermission(
this.info?.globalPermissions, this.info?.globalPermissions,
permissions.useSocialLogin permissions.enableSocialLogin
); );
} }
@ -82,7 +81,7 @@ export class HeaderComponent implements OnChanges {
autoFocus: false, autoFocus: false,
data: { data: {
accessToken: '', accessToken: '',
hasPermissionToUseSocialLogin: this.hasPermissionToUseSocialLogin hasPermissionToUseSocialLogin: this.hasPermissionForSocialLogin
}, },
width: '30rem' width: '30rem'
}); });

View File

@ -4,8 +4,8 @@ import { MatButtonModule } from '@angular/material/button';
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu';
import { MatToolbarModule } from '@angular/material/toolbar'; import { MatToolbarModule } from '@angular/material/toolbar';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { LoginWithAccessTokenDialogModule } from '@ghostfolio/client/pages/login/login-with-access-token-dialog/login-with-access-token-dialog.module';
import { LoginWithAccessTokenDialogModule } from '../../pages/login/login-with-access-token-dialog/login-with-access-token-dialog.module';
import { GfLogoModule } from '../logo/logo.module'; import { GfLogoModule } from '../logo/logo.module';
import { HeaderComponent } from './header.component'; import { HeaderComponent } from './header.component';

View File

@ -9,8 +9,8 @@ import {
OnInit, OnInit,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { PortfolioItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-item.interface';
import { primaryColorRgb } from '@ghostfolio/helper'; import { primaryColorRgb } from '@ghostfolio/helper';
import { PortfolioItem } from 'apps/api/src/app/portfolio/interfaces/portfolio-item.interface';
import { import {
LineController, LineController,
LineElement, LineElement,

View File

@ -5,9 +5,9 @@ import {
Inject Inject
} from '@angular/core'; } from '@angular/core';
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 { isToday, parse } from 'date-fns'; import { isToday, parse } from 'date-fns';
import { DataService } from '../../services/data.service';
import { LineChartItem } from '../line-chart/interfaces/line-chart.interface'; import { LineChartItem } from '../line-chart/interfaces/line-chart.interface';
import { PositionDetailDialogParams } from './interfaces/interfaces'; import { PositionDetailDialogParams } from './interfaces/interfaces';

View File

@ -2,9 +2,9 @@ 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 { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { GfLineChartModule } from '../../components/line-chart/line-chart.module';
import { GfDialogFooterModule } from '../dialog-footer/dialog-footer.module'; import { GfDialogFooterModule } from '../dialog-footer/dialog-footer.module';
import { GfDialogHeaderModule } from '../dialog-header/dialog-header.module'; import { GfDialogHeaderModule } from '../dialog-header/dialog-header.module';
import { GfFearAndGreedIndexModule } from '../fear-and-greed-index/fear-and-greed-index.module'; import { GfFearAndGreedIndexModule } from '../fear-and-greed-index/fear-and-greed-index.module';

View File

@ -5,8 +5,8 @@ import {
OnChanges, OnChanges,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { PortfolioOverview } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-overview.interface';
import { Currency } from '@prisma/client'; import { Currency } from '@prisma/client';
import { PortfolioOverview } from 'apps/api/src/app/portfolio/interfaces/portfolio-overview.interface';
@Component({ @Component({
selector: 'gf-portfolio-overview', selector: 'gf-portfolio-overview',

View File

@ -34,16 +34,16 @@
<div *ngIf="showDetails" class="row"> <div *ngIf="showDetails" class="row">
<div class="d-flex col justify-content-end"> <div class="d-flex col justify-content-end">
<gf-value <gf-value
colorizeSign="true" [colorizeSign]="true"
isCurrency="true" [isCurrency]="true"
[locale]="locale" [locale]="locale"
[value]="isLoading ? undefined : performance?.currentNetPerformance" [value]="isLoading ? undefined : performance?.currentNetPerformance"
></gf-value> ></gf-value>
</div> </div>
<div class="col"> <div class="col">
<gf-value <gf-value
colorizeSign="true" [colorizeSign]="true"
isPercent="true" [isPercent]="true"
[locale]="locale" [locale]="locale"
[value]=" [value]="
isLoading ? undefined : performance?.currentNetPerformancePercent isLoading ? undefined : performance?.currentNetPerformancePercent

View File

@ -7,8 +7,8 @@ import {
OnInit, OnInit,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { PortfolioPerformance } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-performance.interface';
import { Currency } from '@prisma/client'; import { Currency } from '@prisma/client';
import { PortfolioPerformance } from 'apps/api/src/app/portfolio/interfaces/portfolio-performance.interface';
import { CountUp } from 'countup.js'; import { CountUp } from 'countup.js';
import { isNumber } from 'lodash'; import { isNumber } from 'lodash';

View File

@ -16,17 +16,17 @@
<div class="d-flex flex-column flex-wrap justify-content-end"> <div class="d-flex flex-column flex-wrap justify-content-end">
<gf-value <gf-value
class="justify-content-end mb-2" class="justify-content-end mb-2"
colorizeSign="true"
position="end" position="end"
[colorizeSign]="true"
[currency]="baseCurrency" [currency]="baseCurrency"
[locale]="locale" [locale]="locale"
[value]="isLoading ? undefined : performance?.currentGrossPerformance" [value]="isLoading ? undefined : performance?.currentGrossPerformance"
></gf-value> ></gf-value>
<gf-value <gf-value
class="justify-content-end" class="justify-content-end"
colorizeSign="true"
isPercent="true"
position="end" position="end"
[colorizeSign]="true"
[isPercent]="true"
[locale]="locale" [locale]="locale"
[value]=" [value]="
isLoading ? undefined : performance?.currentGrossPerformancePercent isLoading ? undefined : performance?.currentGrossPerformancePercent
@ -39,17 +39,17 @@
<div class="d-flex flex-column flex-wrap justify-content-end"> <div class="d-flex flex-column flex-wrap justify-content-end">
<gf-value <gf-value
class="justify-content-end mb-2" class="justify-content-end mb-2"
colorizeSign="true"
position="end" position="end"
[colorizeSign]="true"
[currency]="baseCurrency" [currency]="baseCurrency"
[locale]="locale" [locale]="locale"
[value]="isLoading ? undefined : performance?.currentNetPerformance" [value]="isLoading ? undefined : performance?.currentNetPerformance"
></gf-value> ></gf-value>
<gf-value <gf-value
class="justify-content-end" class="justify-content-end"
colorizeSign="true"
isPercent="true"
position="end" position="end"
[colorizeSign]="true"
[isPercent]="true"
[locale]="locale" [locale]="locale"
[value]=" [value]="
isLoading ? undefined : performance?.currentNetPerformancePercent isLoading ? undefined : performance?.currentNetPerformancePercent

View File

@ -4,8 +4,8 @@ import {
Input, Input,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { PortfolioPerformance } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-performance.interface';
import { Currency } from '@prisma/client'; import { Currency } from '@prisma/client';
import { PortfolioPerformance } from 'apps/api/src/app/portfolio/interfaces/portfolio-performance.interface';
@Component({ @Component({
selector: 'gf-portfolio-performance', selector: 'gf-portfolio-performance',

View File

@ -7,7 +7,7 @@ import {
OnChanges, OnChanges,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { PortfolioItem } from 'apps/api/src/app/portfolio/interfaces/portfolio-item.interface'; import { PortfolioItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-item.interface';
import { endOfDay, parseISO, startOfDay } from 'date-fns'; import { endOfDay, parseISO, startOfDay } from 'date-fns';
@Component({ @Component({

View File

@ -7,8 +7,8 @@ import {
OnInit, OnInit,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
import { Currency } from '@prisma/client'; import { Currency } from '@prisma/client';
import { PortfolioPosition } from 'apps/api/src/app/portfolio/interfaces/portfolio-position.interface';
import { Tooltip } from 'chart.js'; import { Tooltip } from 'chart.js';
import { LinearScale } from 'chart.js'; import { LinearScale } from 'chart.js';
import { ArcElement } from 'chart.js'; import { ArcElement } from 'chart.js';

View File

@ -3,5 +3,6 @@ export interface PositionDetailDialogParams {
deviceType: string; deviceType: string;
locale: string; locale: string;
symbol: string; symbol: string;
symbolUrl: string;
title: string; title: string;
} }

View File

@ -5,9 +5,9 @@ import {
Inject Inject
} from '@angular/core'; } from '@angular/core';
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 { format, isSameMonth, isToday, parseISO } from 'date-fns'; import { format, isSameMonth, isToday, parseISO } from 'date-fns';
import { DataService } from '../../../services/data.service';
import { LineChartItem } from '../../line-chart/interfaces/line-chart.interface'; import { LineChartItem } from '../../line-chart/interfaces/line-chart.interface';
import { PositionDetailDialogParams } from './interfaces/interfaces'; import { PositionDetailDialogParams } from './interfaces/interfaces';
@ -23,6 +23,7 @@ export class PositionDetailDialog {
public benchmarkDataItems: LineChartItem[]; public benchmarkDataItems: LineChartItem[];
public currency: string; public currency: string;
public firstBuyDate: string; public firstBuyDate: string;
public grossPerformance: number;
public grossPerformancePercent: number; public grossPerformancePercent: number;
public historicalDataItems: LineChartItem[]; public historicalDataItems: LineChartItem[];
public investment: number; public investment: number;
@ -30,6 +31,7 @@ export class PositionDetailDialog {
public maxPrice: number; public maxPrice: number;
public minPrice: number; public minPrice: number;
public quantity: number; public quantity: number;
public transactionCount: number;
public constructor( public constructor(
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
@ -44,18 +46,21 @@ export class PositionDetailDialog {
averagePrice, averagePrice,
currency, currency,
firstBuyDate, firstBuyDate,
grossPerformance,
grossPerformancePercent, grossPerformancePercent,
historicalData, historicalData,
investment, investment,
marketPrice, marketPrice,
maxPrice, maxPrice,
minPrice, minPrice,
quantity quantity,
transactionCount
}) => { }) => {
this.averagePrice = averagePrice; this.averagePrice = averagePrice;
this.benchmarkDataItems = []; this.benchmarkDataItems = [];
this.currency = currency; this.currency = currency;
this.firstBuyDate = firstBuyDate; this.firstBuyDate = firstBuyDate;
this.grossPerformance = quantity * grossPerformance;
this.grossPerformancePercent = grossPerformancePercent; this.grossPerformancePercent = grossPerformancePercent;
this.historicalDataItems = historicalData.map( this.historicalDataItems = historicalData.map(
(historicalDataItem) => { (historicalDataItem) => {
@ -75,6 +80,7 @@ export class PositionDetailDialog {
this.maxPrice = maxPrice; this.maxPrice = maxPrice;
this.minPrice = minPrice; this.minPrice = minPrice;
this.quantity = quantity; this.quantity = quantity;
this.transactionCount = transactionCount;
if (isToday(parseISO(this.firstBuyDate))) { if (isToday(parseISO(this.firstBuyDate))) {
// Add average price // Add average price

View File

@ -1,6 +1,7 @@
<gf-dialog-header <gf-dialog-header
mat-dialog-title mat-dialog-title
[deviceType]="data.deviceType" [deviceType]="data.deviceType"
[symbolUrl]="data.symbolUrl"
[title]="data.title" [title]="data.title"
(closeButtonClicked)="onClose()" (closeButtonClicked)="onClose()"
></gf-dialog-header> ></gf-dialog-header>
@ -20,19 +21,22 @@
<div class="row"> <div class="row">
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value <gf-value
colorizeSign="true" label="Change"
isPercent="true"
label="Performance"
size="medium" size="medium"
[colorizeSign]="true"
[currency]="data.baseCurrency"
[locale]="data.locale" [locale]="data.locale"
[value]="grossPerformancePercent" [value]="grossPerformance"
></gf-value> ></gf-value>
</div> </div>
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value <gf-value
label="First Buy Date" label="Performance"
size="medium" size="medium"
[value]="firstBuyDate" [colorizeSign]="true"
[isPercent]="true"
[locale]="data.locale"
[value]="grossPerformancePercent"
></gf-value> ></gf-value>
</div> </div>
<div class="col-6 mb-3"> <div class="col-6 mb-3">
@ -75,9 +79,9 @@
</div> </div>
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value <gf-value
isCurrency="true"
label="Quantity" label="Quantity"
size="medium" size="medium"
[isCurrency]="true"
[value]="quantity" [value]="quantity"
></gf-value> ></gf-value>
</div> </div>
@ -90,6 +94,22 @@
[value]="investment" [value]="investment"
></gf-value> ></gf-value>
</div> </div>
<div class="col-6 mb-3">
<gf-value
label="First Buy Date"
size="medium"
[value]="firstBuyDate"
></gf-value>
</div>
<div class="col-6 mb-3">
<gf-value
label="Transactions"
size="medium"
[isCurrency]="true"
[locale]="data.locale"
[value]="transactionCount"
></gf-value>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -2,9 +2,9 @@ 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 { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { GfLineChartModule } from '../../../components/line-chart/line-chart.module';
import { GfDialogFooterModule } from '../../dialog-footer/dialog-footer.module'; import { GfDialogFooterModule } from '../../dialog-footer/dialog-footer.module';
import { GfDialogHeaderModule } from '../../dialog-header/dialog-header.module'; import { GfDialogHeaderModule } from '../../dialog-header/dialog-header.module';
import { GfValueModule } from '../../value/value.module'; import { GfValueModule } from '../../value/value.module';

View File

@ -9,7 +9,7 @@
<gf-trend-indicator <gf-trend-indicator
class="d-flex" class="d-flex"
[isLoading]="isLoading" [isLoading]="isLoading"
[isPaused]="!position?.isMarketOpen" [marketState]="position?.marketState"
[range]="range" [range]="range"
[value]="position?.grossPerformancePercent" [value]="position?.grossPerformancePercent"
></gf-trend-indicator> ></gf-trend-indicator>
@ -34,25 +34,22 @@
<div *ngIf="!isLoading" class="flex-grow-1 text-truncate"> <div *ngIf="!isLoading" class="flex-grow-1 text-truncate">
<div class="h6 m-0 text-truncate">{{ position?.name }}</div> <div class="h6 m-0 text-truncate">{{ position?.name }}</div>
<div class="d-flex"> <div class="d-flex">
<span>{{ position?.symbol }}</span> <span>{{ position?.symbol | gfSymbol }}</span>
<gf-symbol-icon <span *ngIf="position?.exchange" class="ml-2 text-muted"
*ngIf="position?.url" >({{ position.exchange }})</span
class="ml-1" >
[url]="position?.url"
></gf-symbol-icon>
<span class="ml-2 text-muted">({{ position?.exchange }})</span>
</div> </div>
<div class="d-flex mt-1"> <div class="d-flex mt-1">
<gf-value <gf-value
class="mr-3" class="mr-3"
colorizeSign="true" [colorizeSign]="true"
[currency]="position?.currency" [currency]="position?.currency"
[locale]="locale" [locale]="locale"
[value]="position?.grossPerformance" [value]="position?.grossPerformance"
></gf-value> ></gf-value>
<gf-value <gf-value
colorizeSign="true" [colorizeSign]="true"
isPercent="true" [isPercent]="true"
[locale]="locale" [locale]="locale"
[value]="position?.grossPerformancePercent" [value]="position?.grossPerformancePercent"
></gf-value> ></gf-value>

View File

@ -7,7 +7,7 @@ import {
} from '@angular/core'; } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { PortfolioPosition } from 'apps/api/src/app/portfolio/interfaces/portfolio-position.interface'; import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
import { Subject, Subscription } from 'rxjs'; import { Subject, Subscription } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
@ -51,6 +51,11 @@ export class PositionComponent implements OnDestroy, OnInit {
public ngOnInit() {} public ngOnInit() {}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private openDialog(): void { private openDialog(): void {
const dialogRef = this.dialog.open(PositionDetailDialog, { const dialogRef = this.dialog.open(PositionDetailDialog, {
autoFocus: false, autoFocus: false,
@ -59,6 +64,7 @@ export class PositionComponent implements OnDestroy, OnInit {
deviceType: this.deviceType, deviceType: this.deviceType,
locale: this.locale, locale: this.locale,
symbol: this.position?.symbol, symbol: this.position?.symbol,
symbolUrl: this.position?.url,
title: this.position?.name title: this.position?.name
}, },
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
@ -69,9 +75,4 @@ export class PositionComponent implements OnDestroy, OnInit {
this.router.navigate(['.'], { relativeTo: this.route }); this.router.navigate(['.'], { relativeTo: this.route });
}); });
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
} }

View File

@ -2,9 +2,9 @@ import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { GfSymbolIconModule } from '../symbol-icon/symbol-icon.module';
import { GfTrendIndicatorModule } from '../trend-indicator/trend-indicator.module'; import { GfTrendIndicatorModule } from '../trend-indicator/trend-indicator.module';
import { GfValueModule } from '../value/value.module'; import { GfValueModule } from '../value/value.module';
import { GfPositionDetailDialogModule } from './position-detail-dialog/position-detail-dialog.module'; import { GfPositionDetailDialogModule } from './position-detail-dialog/position-detail-dialog.module';
@ -16,7 +16,7 @@ import { PositionComponent } from './position.component';
imports: [ imports: [
CommonModule, CommonModule,
GfPositionDetailDialogModule, GfPositionDetailDialogModule,
GfSymbolIconModule, GfSymbolModule,
GfTrendIndicatorModule, GfTrendIndicatorModule,
GfValueModule, GfValueModule,
MatDialogModule, MatDialogModule,

View File

@ -13,7 +13,7 @@
> >
<ng-container matColumnDef="symbol"> <ng-container matColumnDef="symbol">
<th mat-header-cell *matHeaderCellDef mat-sort-header i18n>Symbol</th> <th mat-header-cell *matHeaderCellDef mat-sort-header i18n>Symbol</th>
<td mat-cell *matCellDef="let element">{{ element.symbol }}</td> <td mat-cell *matCellDef="let element">{{ element.symbol | gfSymbol }}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="performance"> <ng-container matColumnDef="performance">
@ -28,8 +28,8 @@
<td class="d-none d-lg-table-cell" mat-cell *matCellDef="let element"> <td class="d-none d-lg-table-cell" mat-cell *matCellDef="let element">
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<gf-value <gf-value
colorizeSign="true" [colorizeSign]="true"
isPercent="true" [isPercent]="true"
[locale]="locale" [locale]="locale"
[value]="isLoading ? undefined : element.grossPerformancePercent" [value]="isLoading ? undefined : element.grossPerformancePercent"
></gf-value> ></gf-value>
@ -50,7 +50,7 @@
<td mat-cell *matCellDef="let element"> <td mat-cell *matCellDef="let element">
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<gf-value <gf-value
isPercent="true" [isPercent]="true"
[locale]="locale" [locale]="locale"
[value]="isLoading ? undefined : element.shareInvestment" [value]="isLoading ? undefined : element.shareInvestment"
></gf-value> ></gf-value>
@ -71,7 +71,7 @@
<td mat-cell *matCellDef="let element"> <td mat-cell *matCellDef="let element">
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<gf-value <gf-value
isPercent="true" [isPercent]="true"
[locale]="locale" [locale]="locale"
[value]="isLoading ? undefined : element.shareCurrent" [value]="isLoading ? undefined : element.shareCurrent"
></gf-value> ></gf-value>

View File

@ -13,8 +13,8 @@ import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort'; import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
import { Order as OrderModel } from '@prisma/client'; import { Order as OrderModel } from '@prisma/client';
import { PortfolioPosition } from 'apps/api/src/app/portfolio/interfaces/portfolio-position.interface';
import { Subject, Subscription } from 'rxjs'; import { Subject, Subscription } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';

View File

@ -7,6 +7,7 @@ import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort'; import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { GfNoTransactionsInfoModule } from '../no-transactions-info/no-transactions-info.module'; import { GfNoTransactionsInfoModule } from '../no-transactions-info/no-transactions-info.module';
@ -23,6 +24,7 @@ import { PositionsTableComponent } from './positions-table.component';
GfNoTransactionsInfoModule, GfNoTransactionsInfoModule,
GfPositionDetailDialogModule, GfPositionDetailDialogModule,
GfSymbolIconModule, GfSymbolIconModule,
GfSymbolModule,
GfValueModule, GfValueModule,
MatButtonModule, MatButtonModule,
MatDialogModule, MatDialogModule,

View File

@ -5,7 +5,8 @@ import {
OnChanges, OnChanges,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { PortfolioPosition } from 'apps/api/src/app/portfolio/interfaces/portfolio-position.interface'; import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces';
@Component({ @Component({
selector: 'gf-positions', selector: 'gf-positions',
@ -40,7 +41,10 @@ export class PositionsComponent implements OnChanges, OnInit {
this.positionsWithPriority = []; this.positionsWithPriority = [];
for (const [, portfolioPosition] of Object.entries(this.positions)) { for (const [, portfolioPosition] of Object.entries(this.positions)) {
if (portfolioPosition.isMarketOpen || this.range !== '1d') { if (
portfolioPosition.marketState === MarketState.open ||
this.range !== '1d'
) {
// Only show positions where the market is open in today's view // Only show positions where the market is open in today's view
this.positionsWithPriority.push(portfolioPosition); this.positionsWithPriority.push(portfolioPosition);
} else { } else {

View File

@ -1,18 +1,18 @@
<div class="py-3"> <div class="py-3">
<div class="flex-nowrap no-gutters row"> <div class="align-items-center flex-nowrap no-gutters row">
<div *ngIf="isLoading"> <div *ngIf="isLoading">
<ngx-skeleton-loader <ngx-skeleton-loader
animation="pulse" animation="pulse"
class="mr-3" class="mr-2"
[theme]="{ [theme]="{
height: '3rem', height: '2rem',
width: '3rem' width: '2rem'
}" }"
></ngx-skeleton-loader> ></ngx-skeleton-loader>
</div> </div>
<div <div
*ngIf="!isLoading" *ngIf="!isLoading"
class="align-items-center d-flex icon-container mr-3 px-3" class="align-items-center d-flex icon-container mr-2 px-2"
[ngClass]="{ okay: rule?.value === true, warn: rule?.value === false }" [ngClass]="{ okay: rule?.value === true, warn: rule?.value === false }"
> >
<ion-icon <ion-icon

View File

@ -2,15 +2,16 @@
display: block; display: block;
.icon-container { .icon-container {
background-color: rgba(var(--dark-primary-text), 0.05);
border-radius: 0.25rem; border-radius: 0.25rem;
height: 3rem; height: 2rem;
&.okay { &.okay {
background-color: var(--success); color: var(--success);
} }
&.warn { &.warn {
background-color: var(--warning); color: var(--danger);
} }
} }
@ -21,6 +22,6 @@
:host-context(.is-dark-theme) { :host-context(.is-dark-theme) {
.icon-container { .icon-container {
color: rgba(var(--dark-primary-text)); background-color: rgba(var(--light-primary-text), 0.05);
} }
} }

View File

@ -4,7 +4,7 @@ import {
Input, Input,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { PortfolioReportRule } from 'apps/api/src/app/portfolio/interfaces/portfolio-report.interface'; import { PortfolioReportRule } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-report.interface';
@Component({ @Component({
selector: 'gf-rule', selector: 'gf-rule',

View File

@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { PortfolioReportRule } from 'apps/api/src/app/portfolio/interfaces/portfolio-report.interface'; import { PortfolioReportRule } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-report.interface';
@Component({ @Component({
selector: 'gf-rules', selector: 'gf-rules',

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