Compare commits

..

27 Commits

Author SHA1 Message Date
b23f3a8a81 Release 0.93.0 2021-04-26 21:55:51 +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
150 changed files with 1187 additions and 745 deletions

View File

@ -5,6 +5,68 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 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
### Added
@ -63,7 +125,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Fixed an issue in the portfolio update on deleting a transaction
- Fixed an issue in the _X-Ray_ section (missing redirection on logout)
- Fixed an issue in the _X-ray_ section (missing redirection on logout)
## 0.82.0 - 10.04.2021
@ -136,7 +198,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Grouped the _X-Ray_ section visually in _Currency Cluster Risk_ and _Platform Cluster Risk_
- Grouped the _X-ray_ section visually in _Currency Cluster Risk_ and _Platform Cluster Risk_
## 0.76.0 - 02.04.2021
@ -148,7 +210,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Fixed an issue in the _X-Ray_ section (empty portfolio)
- Fixed an issue in the _X-ray_ section (empty portfolio)
## 0.75.0 - 01.04.2021
@ -161,7 +223,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added a _Create Account_ message in the _Live Demo_
- Added skeleton loaders to the _X-Ray_ section
- Added skeleton loaders to the _X-ray_ section
### Changed
@ -177,7 +239,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Improved the intro text in the _X-Ray_ section
- Improved the intro text in the _X-ray_ section
### Fixed
@ -193,7 +255,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added an intro text to the _X-Ray_ section
- Added an intro text to the _X-ray_ section
### Changed
@ -212,7 +274,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Improved the styling in the _X-Ray_ section
- Improved the styling in the _X-ray_ section
## 0.70.0 - 27.03.2021
@ -220,7 +282,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added the current _Fear & Greed Index_ as text
- Extended the landing page text: _Ghostfolio_ empowers busy folks...
- Added the first static portfolio analysis rule in the brand new _X-Ray_ section
- Added the first static portfolio analysis rule in the brand new _X-ray_ section
### Changed

View File

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

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 { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { RequestWithUser } from 'apps/api/src/app/interfaces/request-with-user.type';
import { AccessService } from './access.service';
import { Access } from './interfaces/access.interface';

View File

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

View File

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

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 {
Controller,
@ -9,10 +11,8 @@ import {
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { RequestWithUser } from 'apps/api/src/app/interfaces/request-with-user.type';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { DataGatheringService } from '../../services/data-gathering.service';
import { AdminService } from './admin.service';
import { AdminData } from './interfaces/admin-data.interface';

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 { ConfigurationService } from '../../services/configuration.service';
import { DataGatheringService } from '../../services/data-gathering.service';
import { DataProviderService } from '../../services/data-provider.service';
import { AlphaVantageService } from '../../services/data-provider/alpha-vantage/alpha-vantage.service';
import { GhostfolioScraperApiService } from '../../services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
import { RakutenRapidApiService } from '../../services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
import { YahooFinanceService } from '../../services/data-provider/yahoo-finance/yahoo-finance.service';
import { ExchangeRateDataService } from '../../services/exchange-rate-data.service';
import { PrismaService } from '../../services/prisma.service';
import { AdminController } from './admin.controller';
import { AdminService } from './admin.service';

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

View File

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

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 { JwtModule } from '@nestjs/jwt';
import { ConfigurationService } from '../../services/configuration.service';
import { PrismaService } from '../../services/prisma.service';
import { UserService } from '../user/user.service';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';

View File

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

View File

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

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 { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigurationService } from '../../services/configuration.service';
import { PrismaService } from '../../services/prisma.service';
import { UserService } from '../user/user.service';
@Injectable()

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 { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { RequestWithUser } from 'apps/api/src/app/interfaces/request-with-user.type';
import { RedisCacheService } from '../redis-cache/redis-cache.service';
import { CacheService } from './cache.service';

View File

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

View File

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

View File

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

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 { ConfigurationService } from '../../services/configuration.service';
import { DataProviderService } from '../../services/data-provider.service';
import { AlphaVantageService } from '../../services/data-provider/alpha-vantage/alpha-vantage.service';
import { GhostfolioScraperApiService } from '../../services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
import { RakutenRapidApiService } from '../../services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
import { YahooFinanceService } from '../../services/data-provider/yahoo-finance/yahoo-finance.service';
import { ExchangeRateDataService } from '../../services/exchange-rate-data.service';
import { PrismaService } from '../../services/prisma.service';
import { RulesService } from '../../services/rules.service';
import { ExperimentalController } from './experimental.controller';
import { ExperimentalService } from './experimental.service';

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

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 { JwtModule } from '@nestjs/jwt';
import { ConfigurationService } from '../../services/configuration.service';
import { PrismaService } from '../../services/prisma.service';
import { InfoController } from './info.controller';
import { InfoService } from './info.service';

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

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

@ -2,6 +2,9 @@ import { Currency, Type } from '@prisma/client';
import { IsISO8601, IsNumber, IsString, ValidateIf } from 'class-validator';
export class CreateOrderDto {
@IsString()
accountId: string;
@IsString()
currency: Currency;

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 {
Body,
@ -15,12 +18,9 @@ import {
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { Order as OrderModel } from '@prisma/client';
import { RequestWithUser } from 'apps/api/src/app/interfaces/request-with-user.type';
import { parseISO } from 'date-fns';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { nullifyValuesInObjects } from '../../helper/object.helper';
import { ImpersonationService } from '../../services/impersonation.service';
import { CreateOrderDto } from './create-order.dto';
import { OrderService } from './order.service';
import { UpdateOrderDto } from './update-order.dto';
@ -118,6 +118,9 @@ export class OrderController {
const date = parseISO(data.date);
const accountId = data.accountId;
delete data.accountId;
if (data.platformId) {
const platformId = data.platformId;
delete data.platformId;
@ -126,6 +129,11 @@ export class OrderController {
{
...data,
date,
Account: {
connect: {
id_userId: { id: accountId, userId: this.request.user.id }
}
},
Platform: { connect: { id: platformId } },
User: { connect: { id: this.request.user.id } }
},
@ -138,6 +146,11 @@ export class OrderController {
{
...data,
date,
Account: {
connect: {
id_userId: { id: accountId, userId: this.request.user.id }
}
},
User: { connect: { id: this.request.user.id } }
},
this.request.user.id
@ -169,6 +182,9 @@ export class OrderController {
const date = parseISO(data.date);
const accountId = data.accountId;
delete data.accountId;
if (data.platformId) {
const platformId = data.platformId;
delete data.platformId;
@ -178,6 +194,11 @@ export class OrderController {
data: {
...data,
date,
Account: {
connect: {
id_userId: { id: accountId, userId: this.request.user.id }
}
},
Platform: { connect: { id: platformId } },
User: { connect: { id: this.request.user.id } }
},
@ -199,6 +220,11 @@ export class OrderController {
data: {
...data,
date,
Account: {
connect: {
id_userId: { id: accountId, userId: this.request.user.id }
}
},
Platform: originalOrder.platformId
? { disconnect: true }
: 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 { ConfigurationService } from '../../services/configuration.service';
import { DataGatheringService } from '../../services/data-gathering.service';
import { DataProviderService } from '../../services/data-provider.service';
import { AlphaVantageService } from '../../services/data-provider/alpha-vantage/alpha-vantage.service';
import { GhostfolioScraperApiService } from '../../services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
import { RakutenRapidApiService } from '../../services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
import { YahooFinanceService } from '../../services/data-provider/yahoo-finance/yahoo-finance.service';
import { ImpersonationService } from '../../services/impersonation.service';
import { PrismaService } from '../../services/prisma.service';
import { CacheService } from '../cache/cache.service';
import { RedisCacheModule } from '../redis-cache/redis-cache.module';
import { OrderController } from './order.controller';

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 { Order, Prisma } from '@prisma/client';
import { DataGatheringService } from '../../services/data-gathering.service';
import { PrismaService } from '../../services/prisma.service';
import { CacheService } from '../cache/cache.service';
import { RedisCacheService } from '../redis-cache/redis-cache.service';
import { OrderWithPlatform } from './interfaces/order-with-platform.type';

View File

@ -2,6 +2,9 @@ import { Currency, Type } from '@prisma/client';
import { IsISO8601, IsNumber, IsString, ValidateIf } from 'class-validator';
export class UpdateOrderDto {
@IsString()
accountId: string;
@IsString()
currency: Currency;

View File

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

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 {
Controller,
@ -15,12 +21,6 @@ import { AuthGuard } from '@nestjs/passport';
import { Response } from 'express';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import {
hasNotDefinedValuesInObject,
nullifyValuesInObject
} from '../../helper/object.helper';
import { ExchangeRateDataService } from '../../services/exchange-rate-data.service';
import { ImpersonationService } from '../../services/impersonation.service';
import { RequestWithUser } from '../interfaces/request-with-user.type';
import { PortfolioItem } from './interfaces/portfolio-item.interface';
import { PortfolioOverview } from './interfaces/portfolio-overview.interface';

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 { ConfigurationService } from '../../services/configuration.service';
import { DataGatheringService } from '../../services/data-gathering.service';
import { DataProviderService } from '../../services/data-provider.service';
import { AlphaVantageService } from '../../services/data-provider/alpha-vantage/alpha-vantage.service';
import { GhostfolioScraperApiService } from '../../services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
import { RakutenRapidApiService } from '../../services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
import { YahooFinanceService } from '../../services/data-provider/yahoo-finance/yahoo-finance.service';
import { ExchangeRateDataService } from '../../services/exchange-rate-data.service';
import { ImpersonationService } from '../../services/impersonation.service';
import { PrismaService } from '../../services/prisma.service';
import { RulesService } from '../../services/rules.service';
import { CacheService } from '../cache/cache.service';
import { OrderService } from '../order/order.service';
import { RedisCacheModule } from '../redis-cache/redis-cache.module';

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 { REQUEST } from '@nestjs/core';
import { RequestWithUser } from 'apps/api/src/app/interfaces/request-with-user.type';
import {
add,
format,
@ -9,7 +15,6 @@ import {
getYear,
isAfter,
isSameDay,
parse,
parseISO,
setDate,
setMonth,
@ -18,12 +23,6 @@ import {
import { isEmpty } from 'lodash';
import * as roundTo from 'round-to';
import { Portfolio } from '../../models/portfolio';
import { DataProviderService } from '../../services/data-provider.service';
import { ExchangeRateDataService } from '../../services/exchange-rate-data.service';
import { ImpersonationService } from '../../services/impersonation.service';
import { IOrder } from '../../services/interfaces/interfaces';
import { RulesService } from '../../services/rules.service';
import { OrderService } from '../order/order.service';
import { RedisCacheService } from '../redis-cache/redis-cache.service';
import { UserService } from '../user/user.service';
@ -48,53 +47,6 @@ export class PortfolioService {
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> {
let portfolio: Portfolio;
let stringifiedPortfolio = await this.redisCacheService.get(
@ -382,4 +334,51 @@ export class PortfolioService {
symbol: aSymbol
};
}
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 { ConfigModule, ConfigService } from '@nestjs/config';
import * as redisStore from 'cache-manager-redis-store';
import { ConfigurationService } from '../../services/configuration.service';
import { RedisCacheService } from './redis-cache.service';
@Module({

View File

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

View File

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

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 { ConfigurationService } from '../../services/configuration.service';
import { DataProviderService } from '../../services/data-provider.service';
import { AlphaVantageService } from '../../services/data-provider/alpha-vantage/alpha-vantage.service';
import { GhostfolioScraperApiService } from '../../services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
import { RakutenRapidApiService } from '../../services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
import { YahooFinanceService } from '../../services/data-provider/yahoo-finance/yahoo-finance.service';
import { PrismaService } from '../../services/prisma.service';
import { SymbolController } from './symbol.controller';
import { SymbolService } from './symbol.service';

View File

@ -1,9 +1,9 @@
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
import { convertFromYahooSymbol } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
import { Injectable } from '@nestjs/common';
import { Currency } from '@prisma/client';
import { convertFromYahooSymbol } from 'apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service';
import * as bent from 'bent';
import { DataProviderService } from '../../services/data-provider.service';
import { LookupItem } from './interfaces/lookup-item.interface';
import { SymbolItem } from './interfaces/symbol-item.interface';

View File

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

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 {
Body,
@ -14,7 +15,6 @@ import { REQUEST } from '@nestjs/core';
import { JwtService } from '@nestjs/jwt';
import { AuthGuard } from '@nestjs/passport';
import { Provider } from '@prisma/client';
import { RequestWithUser } from 'apps/api/src/app/interfaces/request-with-user.type';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
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 { JwtModule } from '@nestjs/jwt';
import { ConfigurationService } from '../../services/configuration.service';
import { PrismaService } from '../../services/prisma.service';
import { UserController } from './user.controller';
import { UserService } from './user.service';

View File

@ -1,3 +1,5 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import {
getPermissions,
locale,
@ -8,8 +10,6 @@ import { Injectable } from '@nestjs/common';
import { Currency, Prisma, Provider, User } from '@prisma/client';
import { add } from 'date-fns';
import { ConfigurationService } from '../../services/configuration.service';
import { PrismaService } from '../../services/prisma.service';
import { UserWithSettings } from '../interfaces/user-with-settings';
import { User as IUser } from './interfaces/user.interface';
@ -25,6 +25,7 @@ export class UserService {
) {}
public async getUser({
Account,
alias,
id,
role,
@ -44,10 +45,6 @@ export class UserService {
currentPermissions.push(permissions.accessFearAndGreedIndex);
}
if (this.configurationService.get('ENABLE_FEATURE_SOCIAL_LOGIN')) {
currentPermissions.push(permissions.useSocialLogin);
}
return {
alias,
id,
@ -57,6 +54,7 @@ export class UserService {
id: accessItem.id
};
}),
accounts: Account,
permissions: currentPermissions,
settings: {
baseCurrency: Settings?.currency || UserService.DEFAULT_CURRENCY,
@ -73,7 +71,7 @@ export class UserService {
userWhereUniqueInput: Prisma.UserWhereUniqueInput
): Promise<UserWithSettings | null> {
const user = await this.prisma.user.findUnique({
include: { Settings: true },
include: { Account: true, Settings: true },
where: userWhereUniqueInput
});
@ -120,7 +118,15 @@ export class UserService {
public async createUser(data?: Prisma.UserCreateInput): Promise<User> {
let user = await this.prisma.user.create({
data
data: {
...data,
Account: {
create: {
isDefault: true,
name: 'Default Account'
}
}
}
});
if (data.provider === Provider.ANONYMOUS) {
@ -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({
currency,
userId
@ -200,4 +194,16 @@ export class UserService {
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 {
PortfolioItem,
Position
} from 'apps/api/src/app/portfolio/interfaces/portfolio-item.interface';
} from '@ghostfolio/api/app/portfolio/interfaces/portfolio-item.interface';
import { Order } from '../order';

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';
export interface RuleInterface {

View File

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

View File

@ -1,8 +1,8 @@
import { getToday, getYesterday, resetHours } from '@ghostfolio/helper';
import {
PortfolioItem,
Position
} from 'apps/api/src/app/portfolio/interfaces/portfolio-item.interface';
} from '@ghostfolio/api/app/portfolio/interfaces/portfolio-item.interface';
import { getToday, getYesterday, resetHours } from '@ghostfolio/helper';
import {
add,
format,
@ -79,7 +79,9 @@ export class Portfolio implements PortfolioInterface {
investmentInOriginalCurrency:
portfolioItemsYesterday?.positions[symbol]
?.investmentInOriginalCurrency,
marketPrice: currentData[symbol]?.marketPrice,
marketPrice:
currentData[symbol]?.marketPrice ??
portfolioItemsYesterday.positions[symbol]?.marketPrice,
quantity: portfolioItemsYesterday?.positions[symbol]?.quantity
};
});
@ -158,53 +160,6 @@ export class Portfolio implements PortfolioInterface {
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[] {
if (aDate) {
const filteredPortfolio = this.portfolioItems.find((item) => {
@ -528,12 +483,6 @@ export class Portfolio implements PortfolioInterface {
return this.orders;
}
private getOrdersByType(aFilter: string[]) {
return this.orders.filter((order) => {
return aFilter.includes(order.getType());
});
}
public getValue(aDate = getToday()) {
const positions = this.getPositions(aDate);
let value = 0;
@ -692,6 +641,53 @@ export class Portfolio implements PortfolioInterface {
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() {
// console.time('update-portfolio-items');

View File

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

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 { 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 { 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 { 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 { Rule } from '../../rule';

View File

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

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 { 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 { Rule } from '../../rule';

View File

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

View File

@ -2,6 +2,7 @@ import {
benchmarks,
currencyPairs,
getUtc,
isGhostfolioScraperApiSymbol,
resetHours
} from '@ghostfolio/helper';
import { Injectable } from '@nestjs/common';
@ -235,12 +236,16 @@ export class DataGatheringService {
select: { symbol: true }
});
const distinctOrdersWithDate = distinctOrders.map((distinctOrder) => {
return {
...distinctOrder,
date: startDate
};
});
const distinctOrdersWithDate = distinctOrders
.filter((distinctOrder) => {
return !isGhostfolioScraperApiSymbol(distinctOrder.symbol);
})
.map((distinctOrder) => {
return {
...distinctOrder,
date: startDate
};
});
const currencyPairsToGather = currencyPairs.map((symbol) => {
return {
@ -262,7 +267,7 @@ export class DataGatheringService {
}
private async getSymbolsMax() {
const startDate = new Date(getUtc('2000-01-01'));
const startDate = new Date(getUtc('2015-01-01'));
const customSymbolsToGather = await this.getCustomSymbolsToGather(
startDate

View File

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

View File

@ -8,13 +8,15 @@ import { DataProviderInterface } from '../../interfaces/data-provider.interface'
import { Granularity } from '../../interfaces/granularity.type';
import {
IDataProviderHistoricalResponse,
IDataProviderResponse
IDataProviderResponse,
MarketState
} from '../../interfaces/interfaces';
import { PrismaService } from '../../prisma.service';
import { Currency } from '.prisma/client';
@Injectable()
export class GhostfolioScraperApiService implements DataProviderInterface {
private static NUMERIC_REGEXP = /[-]{0,1}[\d]*[.,]{0,1}[\d]+/g;
public constructor(private prisma: PrismaService) {}
public async get(
@ -26,6 +28,9 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
try {
const symbol = aSymbols[0];
const scraperConfig = await this.getScraperConfig(symbol);
const { marketPrice } = await this.prisma.marketData.findFirst({
orderBy: {
date: 'desc'
@ -38,9 +43,9 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
return {
[symbol]: {
marketPrice,
currency: Currency.CHF,
isMarketOpen: true,
name: symbol
currency: scraperConfig?.currency,
marketState: MarketState.delayed,
name: scraperConfig?.name
}
};
} catch (error) {
@ -65,27 +70,16 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
try {
const symbol = aSymbols[0];
const {
value: scraperConfigString
} = await this.prisma.property.findFirst({
select: {
value: true
},
where: { key: 'SCRAPER_CONFIG' }
});
const scraperConfig = await this.getScraperConfig(symbol);
const scraperConfig = JSON.parse(scraperConfigString).find((item) => {
return item.symbol === symbol;
});
const get = bent(scraperConfig.url, 'GET', 'string', 200, {});
const get = bent(scraperConfig?.url, 'GET', 'string', 200, {});
const html = await get();
const $ = cheerio.load(html);
const string = $(scraperConfig.selector).text().replace('CHF', '').trim();
const value = parseFloat(string);
const value = this.extractNumberFromString(
$(scraperConfig?.selector).text()
);
return {
[symbol]: {
@ -100,4 +94,34 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
return {};
}
private extractNumberFromString(aString: string): number {
try {
const [numberString] = aString.match(
GhostfolioScraperApiService.NUMERIC_REGEXP
);
return parseFloat(numberString.trim());
} catch {
return undefined;
}
}
private async getScraperConfig(aSymbol: string) {
try {
const {
value: scraperConfigString
} = await this.prisma.property.findFirst({
select: {
value: true
},
where: { key: 'SCRAPER_CONFIG' }
});
return JSON.parse(scraperConfigString).find((item) => {
return item.symbol === aSymbol;
});
} catch {}
return {};
}
}

View File

@ -8,7 +8,8 @@ import { DataProviderInterface } from '../../interfaces/data-provider.interface'
import { Granularity } from '../../interfaces/granularity.type';
import {
IDataProviderHistoricalResponse,
IDataProviderResponse
IDataProviderResponse,
MarketState
} from '../../interfaces/interfaces';
import { PrismaService } from '../../prisma.service';
@ -38,8 +39,8 @@ export class RakutenRapidApiService implements DataProviderInterface {
return {
'GF.FEAR_AND_GREED_INDEX': {
currency: undefined,
isMarketOpen: true,
marketPrice: fgi.now.value,
marketState: MarketState.open,
name: RakutenRapidApiService.FEAR_AND_GREED_INDEX_NAME
}
};

View File

@ -9,6 +9,7 @@ import {
IDataProviderHistoricalResponse,
IDataProviderResponse,
Industry,
MarketState,
Sector,
Type
} from '../../interfaces/interfaces';
@ -49,8 +50,10 @@ export class YahooFinanceService implements DataProviderInterface {
response[symbol] = {
currency: parseCurrency(value.price?.currency),
exchange: this.parseExchange(value.price?.exchangeName),
isMarketOpen:
value.price?.marketState === 'REGULAR' || isCrypto(symbol),
marketState:
value.price?.marketState === 'REGULAR' || isCrypto(symbol)
? MarketState.open
: MarketState.closed,
marketPrice: value.price?.regularMarketPrice || 0,
name: value.price?.longName || value.price?.shortName || symbol,
type: this.parseType(this.getType(symbol, value))

View File

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

View File

@ -12,6 +12,12 @@ export const Industry = {
Software: 'Software'
};
export const MarketState = {
closed: 'closed',
delayed: 'delayed',
open: 'open'
};
export const Sector = {
Consumer: 'Consumer',
Healthcare: 'Healthcare',
@ -47,10 +53,10 @@ export interface IDataProviderResponse {
currency: Currency;
exchange?: string;
industry?: Industry;
isMarketOpen: boolean;
marketChange?: number;
marketChangePercent?: number;
marketPrice: number;
marketState: MarketState;
name: string;
sector?: Sector;
type?: Type;
@ -59,6 +65,8 @@ export interface IDataProviderResponse {
export type Industry = typeof Industry[keyof typeof Industry];
export type MarketState = typeof MarketState[keyof typeof MarketState];
export type Sector = typeof Sector[keyof typeof Sector];
export type Type = typeof Type[keyof typeof Type];

View File

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

View File

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

View File

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

View File

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

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>
<button
*ngIf="deviceType !== 'mobile'"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,9 +5,9 @@ import {
Inject
} from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { DataService } from '@ghostfolio/client/services/data.service';
import { isToday, parse } from 'date-fns';
import { DataService } from '../../services/data.service';
import { LineChartItem } from '../line-chart/interfaces/line-chart.interface';
import { PositionDetailDialogParams } from './interfaces/interfaces';

View File

@ -2,9 +2,9 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog';
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { GfLineChartModule } from '../../components/line-chart/line-chart.module';
import { GfDialogFooterModule } from '../dialog-footer/dialog-footer.module';
import { GfDialogHeaderModule } from '../dialog-header/dialog-header.module';
import { GfFearAndGreedIndexModule } from '../fear-and-greed-index/fear-and-greed-index.module';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,9 +5,9 @@ import {
Inject
} from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { DataService } from '@ghostfolio/client/services/data.service';
import { format, isSameMonth, isToday, parseISO } from 'date-fns';
import { DataService } from '../../../services/data.service';
import { LineChartItem } from '../../line-chart/interfaces/line-chart.interface';
import { PositionDetailDialogParams } from './interfaces/interfaces';

View File

@ -1,6 +1,7 @@
<gf-dialog-header
mat-dialog-title
[deviceType]="data.deviceType"
[symbolUrl]="data.symbolUrl"
[title]="data.title"
(closeButtonClicked)="onClose()"
></gf-dialog-header>
@ -20,10 +21,10 @@
<div class="row">
<div class="col-6 mb-3">
<gf-value
colorizeSign="true"
isPercent="true"
label="Performance"
size="medium"
[colorizeSign]="true"
[isPercent]="true"
[locale]="data.locale"
[value]="grossPerformancePercent"
></gf-value>
@ -75,9 +76,9 @@
</div>
<div class="col-6 mb-3">
<gf-value
isCurrency="true"
label="Quantity"
size="medium"
[isCurrency]="true"
[value]="quantity"
></gf-value>
</div>

View File

@ -2,9 +2,9 @@ import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog';
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { GfLineChartModule } from '../../../components/line-chart/line-chart.module';
import { GfDialogFooterModule } from '../../dialog-footer/dialog-footer.module';
import { GfDialogHeaderModule } from '../../dialog-header/dialog-header.module';
import { GfValueModule } from '../../value/value.module';

View File

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

View File

@ -7,7 +7,7 @@ import {
} from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
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 { takeUntil } from 'rxjs/operators';
@ -51,6 +51,11 @@ export class PositionComponent implements OnDestroy, OnInit {
public ngOnInit() {}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private openDialog(): void {
const dialogRef = this.dialog.open(PositionDetailDialog, {
autoFocus: false,
@ -59,6 +64,7 @@ export class PositionComponent implements OnDestroy, OnInit {
deviceType: this.deviceType,
locale: this.locale,
symbol: this.position?.symbol,
symbolUrl: this.position?.url,
title: this.position?.name
},
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
@ -69,9 +75,4 @@ export class PositionComponent implements OnDestroy, OnInit {
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 { MatDialogModule } from '@angular/material/dialog';
import { RouterModule } from '@angular/router';
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { GfSymbolIconModule } from '../symbol-icon/symbol-icon.module';
import { GfTrendIndicatorModule } from '../trend-indicator/trend-indicator.module';
import { GfValueModule } from '../value/value.module';
import { GfPositionDetailDialogModule } from './position-detail-dialog/position-detail-dialog.module';
@ -16,7 +16,7 @@ import { PositionComponent } from './position.component';
imports: [
CommonModule,
GfPositionDetailDialogModule,
GfSymbolIconModule,
GfSymbolModule,
GfTrendIndicatorModule,
GfValueModule,
MatDialogModule,

View File

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

View File

@ -13,8 +13,8 @@ import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
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 { PortfolioPosition } from 'apps/api/src/app/portfolio/interfaces/portfolio-position.interface';
import { Subject, Subscription } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

View File

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

View File

@ -5,7 +5,8 @@ import {
OnChanges,
OnInit
} 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({
selector: 'gf-positions',
@ -40,7 +41,10 @@ export class PositionsComponent implements OnChanges, OnInit {
this.positionsWithPriority = [];
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
this.positionsWithPriority.push(portfolioPosition);
} else {

View File

@ -1,18 +1,18 @@
<div class="py-3">
<div class="flex-nowrap no-gutters row">
<div class="align-items-center flex-nowrap no-gutters row">
<div *ngIf="isLoading">
<ngx-skeleton-loader
animation="pulse"
class="mr-3"
class="mr-2"
[theme]="{
height: '3rem',
width: '3rem'
height: '2rem',
width: '2rem'
}"
></ngx-skeleton-loader>
</div>
<div
*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 }"
>
<ion-icon

View File

@ -2,15 +2,16 @@
display: block;
.icon-container {
background-color: rgba(var(--dark-primary-text), 0.05);
border-radius: 0.25rem;
height: 3rem;
height: 2rem;
&.okay {
background-color: var(--success);
color: var(--success);
}
&.warn {
background-color: var(--warning);
color: var(--danger);
}
}
@ -21,6 +22,6 @@
:host-context(.is-dark-theme) {
.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,
OnInit
} 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({
selector: 'gf-rule',

View File

@ -1,5 +1,5 @@
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({
selector: 'gf-rules',

View File

@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { GfRuleModule } from 'apps/client/src/app/components/rule/rule.module';
import { GfRuleModule } from '@ghostfolio/client/components/rule/rule.module';
import { GfNoTransactionsInfoModule } from '../no-transactions-info/no-transactions-info.module';
import { GfPositionModule } from '../position/position.module';

View File

@ -1,5 +1,6 @@
<img
*ngIf="url"
src="https://www.google.com/s2/favicons?domain={{ url }}&sz=64"
[ngClass]="{ large: size === 'large' }"
[title]="tooltip ? tooltip : ''"
/>

View File

@ -5,5 +5,10 @@
border-radius: 0.2rem;
height: 0.8rem;
width: 0.8rem;
&.large {
height: 1.4rem;
width: 1.4rem;
}
}
}

View File

@ -12,6 +12,7 @@ import {
styleUrls: ['./symbol-icon.component.scss']
})
export class SymbolIconComponent implements OnInit {
@Input() size: 'large';
@Input() tooltip: string;
@Input() url: string;

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