Compare commits

...

32 Commits

Author SHA1 Message Date
060846023f Release 1.60.0 (#418) 2021-10-13 12:10:12 +02:00
f06a0fbbee Feature/validate duplicate orders for import (#416)
* Validate duplicate orders

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

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

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

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

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

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

* Update column

* Fix impersonation mode

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

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

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

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

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

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

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

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

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

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

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

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

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

* Use new component

* Update changelog
2021-09-20 20:45:58 +02:00
152 changed files with 1558 additions and 694 deletions

View File

@ -5,6 +5,87 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 1.60.0 - 13.10.2021
### Added
- Extended the validation of the import functionality for transactions
- Valid data types
- Maximum number of orders
- No duplicate orders
- Data provider service returns data for the `dataSource` / `symbol` pair
### Changed
- Harmonized the page layouts
### Fixed
- Fixed the broken line charts showing value labels
## 1.59.0 - 11.10.2021
### Added
- Added a data enhancer for symbol profile data (countries and sectors) via _Trackinsight_
### Changed
- Changed the values of the global heat map to fixed-point notation
### Fixed
- Fixed the links of cryptocurrency assets in the positions table
- Fixed various values in the impersonation mode which have not been nullified
## 1.58.1 - 03.10.2021
### Fixed
- Fixed an issue in the symbol conversion for _Yahoo Finance_ (for a cryptocurrency with the same code as a currency)
## 1.58.0 - 02.10.2021
### Changed
- Improved the symbol conversion for _Yahoo Finance_: Support for _Solana USD_ (`SOL1-USD`)
- Improved the tooltips of the allocations page
- Upgraded `envalid` from version `7.1.0` to `7.2.1`
## 1.57.0 - 29.09.2021
### Added
- Added a protection for endpoints (subscriptions)
### Changed
- Reformatted the exchange rates table in the admin control panel
## 1.56.0 - 25.09.2021
### Added
- Added a story for the line chart component
- Added a story for the portfolio proportion chart component
### Changed
- Changed the navigation to always show the portfolio page
- Migrated the data type of currencies from `enum` to `string` in the database
- Supported unlimited currencies (instead of `CHF`, `EUR`, `GBP` and `USD`)
- Respected the accounts' currencies in the exchange rate service
### Fixed
- Hid the actions from the accounts table in the _Presenter View_
- Hid the actions from the transactions table in the _Presenter View_
- Fixed the data gathering of the initial project setup (database seeding)
### Todo
- Apply data migration (`yarn prisma migrate deploy`)
## 1.55.0 - 20.09.2021
### Changed

View File

@ -34,7 +34,7 @@
Our official **[Ghostfolio Premium](https://ghostfol.io/pricing)** cloud offering is the easiest way to get started. Due to the time it saves, this will be the best option for most people. The revenue is used for covering the hosting costs.
If you prefer to run Ghostfolio on your own infrastructure, please find the source code and further instructions here on _GitHub_.
If you prefer to run Ghostfolio on your own infrastructure, please find the source code and further instructions here on _GitHub_ or use the [setup](https://github.com/psychowood/ghostfolio-docker) by [psychowood](https://github.com/psychowood).
## Why Ghostfolio?

View File

@ -1,7 +1,7 @@
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { Injectable } from '@nestjs/common';
import { Account, Currency, Order, Platform, Prisma } from '@prisma/client';
import { Account, Order, Platform, Prisma } from '@prisma/client';
import { CashDetails } from './interfaces/cash-details.interface';
@ -95,7 +95,7 @@ export class AccountService {
public async getCashDetails(
aUserId: string,
aCurrency: Currency
aCurrency: string
): Promise<CashDetails> {
let totalCashBalance = 0;

View File

@ -1,4 +1,4 @@
import { AccountType, Currency } from '@prisma/client';
import { AccountType } from '@prisma/client';
import { IsNumber, IsString, ValidateIf } from 'class-validator';
export class CreateAccountDto {
@ -9,7 +9,7 @@ export class CreateAccountDto {
balance: number;
@IsString()
currency: Currency;
currency: string;
@IsString()
name: string;

View File

@ -1,4 +1,4 @@
import { AccountType, Currency } from '@prisma/client';
import { AccountType } from '@prisma/client';
import { IsNumber, IsString, ValidateIf } from 'class-validator';
export class UpdateAccountDto {
@ -9,7 +9,7 @@ export class UpdateAccountDto {
balance: number;
@IsString()
currency: Currency;
currency: string;
@IsString()
id: string;

View File

@ -3,9 +3,9 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration.ser
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { baseCurrency } from '@ghostfolio/common/config';
import { AdminData } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common';
import { Currency } from '@prisma/client';
import { differenceInDays } from 'date-fns';
@Injectable()
@ -20,53 +20,22 @@ export class AdminService {
public async get(): Promise<AdminData> {
return {
exchangeRates: [
{
label1: Currency.EUR,
label2: Currency.CHF,
value: await this.exchangeRateDataService.toCurrency(
1,
Currency.EUR,
Currency.CHF
)
},
{
label1: Currency.GBP,
label2: Currency.CHF,
value: await this.exchangeRateDataService.toCurrency(
1,
Currency.GBP,
Currency.CHF
)
},
{
label1: Currency.USD,
label2: Currency.CHF,
value: await this.exchangeRateDataService.toCurrency(
1,
Currency.USD,
Currency.CHF
)
},
{
label1: Currency.USD,
label2: Currency.EUR,
value: await this.exchangeRateDataService.toCurrency(
1,
Currency.USD,
Currency.EUR
)
},
{
label1: Currency.USD,
label2: Currency.GBP,
value: await this.exchangeRateDataService.toCurrency(
1,
Currency.USD,
Currency.GBP
)
}
],
exchangeRates: this.exchangeRateDataService
.getCurrencies()
.filter((currency) => {
return currency !== baseCurrency;
})
.map((currency) => {
return {
label1: baseCurrency,
label2: currency,
value: this.exchangeRateDataService.toCurrency(
1,
baseCurrency,
currency
)
};
}),
lastDataGathering: await this.getLastDataGathering(),
transactionCount: await this.prismaService.order.count(),
userCount: await this.prismaService.user.count(),

View File

@ -2,29 +2,21 @@ import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.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 { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { Module } from '@nestjs/common';
import { CacheController } from './cache.controller';
@Module({
imports: [RedisCacheModule],
imports: [DataProviderModule, ExchangeRateDataModule, RedisCacheModule],
controllers: [CacheController],
providers: [
AlphaVantageService,
CacheService,
ConfigurationService,
DataGatheringService,
DataProviderService,
GhostfolioScraperApiService,
PrismaService,
RakutenRapidApiService,
YahooFinanceService
PrismaService
]
})
export class CacheModule {}

View File

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

View File

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

View File

@ -1,4 +1,3 @@
import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
@ -18,6 +17,6 @@ import { ExportService } from './export.service';
RedisCacheModule
],
controllers: [ExportController],
providers: [CacheService, ExportService]
providers: [ExportService]
})
export class ExportModule {}

View File

@ -1,7 +1,11 @@
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { Order } from '@prisma/client';
import { IsArray } from 'class-validator';
import { Type } from 'class-transformer';
import { IsArray, ValidateNested } from 'class-validator';
export class ImportDataDto {
@IsArray()
orders: Partial<Order>[];
@Type(() => CreateOrderDto)
@ValidateNested({ each: true })
orders: Order[];
}

View File

@ -42,7 +42,10 @@ export class ImportController {
console.error(error);
throw new HttpException(
getReasonPhrase(StatusCodes.BAD_REQUEST),
{
error: getReasonPhrase(StatusCodes.BAD_REQUEST),
message: [error.message]
},
StatusCodes.BAD_REQUEST
);
}

View File

@ -1,11 +1,17 @@
import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { Injectable } from '@nestjs/common';
import { Order } from '@prisma/client';
import { parseISO } from 'date-fns';
import { isSameDay, parseISO } from 'date-fns';
@Injectable()
export class ImportService {
public constructor(private readonly orderService: OrderService) {}
private static MAX_ORDERS_TO_IMPORT = 20;
public constructor(
private readonly dataProviderService: DataProviderService,
private readonly orderService: OrderService
) {}
public async import({
orders,
@ -14,7 +20,10 @@ export class ImportService {
orders: Partial<Order>[];
userId: string;
}): Promise<void> {
await this.validateOrders({ orders, userId });
for (const {
accountId,
currency,
dataSource,
date,
@ -25,6 +34,11 @@ export class ImportService {
unitPrice
} of orders) {
await this.orderService.createOrder({
Account: {
connect: {
id_userId: { userId, id: accountId }
}
},
currency,
dataSource,
fee,
@ -37,4 +51,51 @@ export class ImportService {
});
}
}
private async validateOrders({
orders,
userId
}: {
orders: Partial<Order>[];
userId: string;
}) {
if (orders?.length > ImportService.MAX_ORDERS_TO_IMPORT) {
throw new Error('Too many transactions');
}
const existingOrders = await this.orderService.orders({
orderBy: { date: 'desc' },
where: { userId }
});
for (const [
index,
{ currency, dataSource, date, fee, quantity, symbol, type, unitPrice }
] of orders.entries()) {
const duplicateOrder = existingOrders.find((order) => {
return (
order.currency === currency &&
order.dataSource === dataSource &&
isSameDay(order.date, parseISO(<string>(<unknown>date))) &&
order.fee === fee &&
order.quantity === quantity &&
order.symbol === symbol &&
order.type === type &&
order.unitPrice === unitPrice
);
});
if (duplicateOrder) {
throw new Error(`orders.${index} is a duplicate transaction`);
}
const result = await this.dataProviderService.get([
{ dataSource, symbol }
]);
if (result[symbol] === undefined) {
throw new Error(`${symbol} is not a valid symbol for ${dataSource}`);
}
}
}
}

View File

@ -1,10 +1,7 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.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 { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
@ -14,6 +11,8 @@ import { InfoService } from './info.service';
@Module({
imports: [
DataProviderModule,
ExchangeRateDataModule,
JwtModule.register({
secret: process.env.JWT_SECRET_KEY,
signOptions: { expiresIn: '30 days' }
@ -21,15 +20,10 @@ import { InfoService } from './info.service';
],
controllers: [InfoController],
providers: [
AlphaVantageService,
ConfigurationService,
DataGatheringService,
DataProviderService,
GhostfolioScraperApiService,
InfoService,
PrismaService,
RakutenRapidApiService,
YahooFinanceService
PrismaService
]
})
export class InfoModule {}

View File

@ -1,12 +1,12 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { InfoItem } from '@ghostfolio/common/interfaces';
import { Subscription } from '@ghostfolio/common/interfaces/subscription.interface';
import { permissions } from '@ghostfolio/common/permissions';
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Currency } from '@prisma/client';
import * as bent from 'bent';
import { subDays } from 'date-fns';
@ -16,6 +16,7 @@ export class InfoService {
public constructor(
private readonly configurationService: ConfigurationService,
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly dataGatheringService: DataGatheringService,
private readonly jwtService: JwtService,
private readonly prismaService: PrismaService
@ -56,7 +57,7 @@ export class InfoService {
...info,
globalPermissions,
platforms,
currencies: Object.values(Currency),
currencies: this.exchangeRateDataService.getCurrencies(),
demoAuthToken: this.getDemoAuthToken(),
lastDataGathering: await this.getLastDataGathering(),
statistics: await this.getStatistics(),

View File

@ -1,14 +1,14 @@
import { Currency, DataSource, Type } from '@prisma/client';
import { IsISO8601, IsNumber, IsString } from 'class-validator';
import { DataSource, Type } from '@prisma/client';
import { IsEnum, IsISO8601, IsNumber, IsString } from 'class-validator';
export class CreateOrderDto {
@IsString()
accountId: string;
@IsString()
currency: Currency;
currency: string;
@IsString()
@IsEnum(DataSource, { each: true })
dataSource: DataSource;
@IsISO8601()
@ -23,7 +23,7 @@ export class CreateOrderDto {
@IsString()
symbol: string;
@IsString()
@IsEnum(Type, { each: true })
type: Type;
@IsNumber()

View File

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

View File

@ -1,6 +1,6 @@
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { Currency, DataSource, MarketData } from '@prisma/client';
import { DataSource, MarketData } from '@prisma/client';
import { CurrentRateService } from './current-rate.service';
import { MarketDataService } from './market-data.service';
@ -75,12 +75,13 @@ describe('CurrentRateService', () => {
dataProviderService = new DataProviderService(
null,
null,
[],
null,
null,
null,
null
);
exchangeRateDataService = new ExchangeRateDataService(null);
exchangeRateDataService = new ExchangeRateDataService(null, null);
marketDataService = new MarketDataService(null);
await exchangeRateDataService.initialize();
@ -95,10 +96,10 @@ describe('CurrentRateService', () => {
it('getValue', async () => {
expect(
await currentRateService.getValue({
currency: Currency.USD,
currency: 'USD',
date: new Date(Date.UTC(2020, 0, 1, 0, 0, 0)),
symbol: 'AMZN',
userCurrency: Currency.CHF
userCurrency: 'CHF'
})
).toMatchObject({
marketPrice: 1847.839966
@ -108,13 +109,13 @@ describe('CurrentRateService', () => {
it('getValues', async () => {
expect(
await currentRateService.getValues({
currencies: { AMZN: Currency.USD },
currencies: { AMZN: 'USD' },
dataGatheringItems: [{ dataSource: DataSource.YAHOO, symbol: 'AMZN' }],
dateQuery: {
lt: new Date(Date.UTC(2020, 0, 2, 0, 0, 0)),
gte: new Date(Date.UTC(2020, 0, 1, 0, 0, 0))
},
userCurrency: Currency.CHF
userCurrency: 'CHF'
})
).toMatchObject([
{

View File

@ -30,9 +30,9 @@ export class CurrentRateService {
{ symbol, dataSource: DataSource.YAHOO }
]);
return {
symbol,
date: resetHours(date),
marketPrice: dataProviderResult?.[symbol]?.marketPrice ?? 0,
symbol: symbol
marketPrice: dataProviderResult?.[symbol]?.marketPrice ?? 0
};
}

View File

@ -1,8 +1,6 @@
import { Currency } from '@prisma/client';
export interface GetValueParams {
currency: Currency;
currency: string;
date: Date;
symbol: string;
userCurrency: Currency;
userCurrency: string;
}

View File

@ -1,11 +1,10 @@
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { Currency } from '@prisma/client';
import { DateQuery } from './date-query.interface';
export interface GetValuesParams {
currencies: { [symbol: string]: Currency };
currencies: { [symbol: string]: string };
dataGatheringItems: IDataGatheringItem[];
dateQuery: DateQuery;
userCurrency: Currency;
userCurrency: string;
}

View File

@ -1,9 +1,9 @@
import { OrderType } from '@ghostfolio/api/models/order-type';
import { Currency, DataSource } from '@prisma/client';
import { DataSource } from '@prisma/client';
import Big from 'big.js';
export interface PortfolioOrder {
currency: Currency;
currency: string;
date: string;
dataSource: DataSource;
fee: Big;

View File

@ -1,8 +1,6 @@
import { Currency } from '@prisma/client';
export interface PortfolioPositionDetail {
averagePrice: number;
currency: Currency;
currency: string;
firstBuyDate: string;
grossPerformance: number;
grossPerformancePercent: number;

View File

@ -1,8 +1,8 @@
import { Currency, DataSource } from '@prisma/client';
import { DataSource } from '@prisma/client';
import Big from 'big.js';
export interface TransactionPointSymbol {
currency: Currency;
currency: string;
dataSource: DataSource;
fee: Big;
firstBuyDate: string;

View File

@ -1,7 +1,6 @@
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
import { OrderType } from '@ghostfolio/api/models/order-type';
import { parseDate, resetHours } from '@ghostfolio/common/helper';
import { Currency, DataSource } from '@prisma/client';
import { DataSource } from '@prisma/client';
import Big from 'big.js';
import {
addDays,
@ -134,7 +133,7 @@ describe('PortfolioCalculator', () => {
it('with orders of only one symbol', () => {
const portfolioCalculator = new PortfolioCalculator(
currentRateService,
Currency.USD
'USD'
);
portfolioCalculator.computeTransactionPoints(ordersVTI);
const portfolioItemsAtTransactionPoints =
@ -148,7 +147,7 @@ describe('PortfolioCalculator', () => {
it('with orders of only one symbol and a fee', () => {
const portfolioCalculator = new PortfolioCalculator(
currentRateService,
Currency.USD
'USD'
);
const orders: PortfolioOrder[] = [
{
@ -158,7 +157,7 @@ describe('PortfolioCalculator', () => {
symbol: 'VTI',
type: OrderType.Buy,
unitPrice: new Big('144.38'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
fee: new Big('5')
},
@ -169,7 +168,7 @@ describe('PortfolioCalculator', () => {
symbol: 'VTI',
type: OrderType.Buy,
unitPrice: new Big('147.99'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
fee: new Big('10')
},
@ -180,7 +179,7 @@ describe('PortfolioCalculator', () => {
symbol: 'VTI',
type: OrderType.Sell,
unitPrice: new Big('151.41'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
fee: new Big('5')
}
@ -198,7 +197,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('10'),
symbol: 'VTI',
investment: new Big('1443.8'),
currency: Currency.USD,
currency: 'USD',
firstBuyDate: '2019-02-01',
transactionCount: 1,
fee: new Big('5')
@ -213,7 +212,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('20'),
symbol: 'VTI',
investment: new Big('2923.7'),
currency: Currency.USD,
currency: 'USD',
firstBuyDate: '2019-02-01',
transactionCount: 2,
fee: new Big('15')
@ -228,7 +227,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('5'),
symbol: 'VTI',
investment: new Big('652.55'),
currency: Currency.USD,
currency: 'USD',
firstBuyDate: '2019-02-01',
transactionCount: 3,
fee: new Big('20')
@ -241,7 +240,7 @@ describe('PortfolioCalculator', () => {
it('with orders of two different symbols and a fee', () => {
const portfolioCalculator = new PortfolioCalculator(
currentRateService,
Currency.USD
'USD'
);
const orders: PortfolioOrder[] = [
{
@ -251,7 +250,7 @@ describe('PortfolioCalculator', () => {
symbol: 'VTI',
type: OrderType.Buy,
unitPrice: new Big('144.38'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
fee: new Big('5')
},
@ -262,7 +261,7 @@ describe('PortfolioCalculator', () => {
symbol: 'VTX',
type: OrderType.Buy,
unitPrice: new Big('147.99'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
fee: new Big('10')
},
@ -273,7 +272,7 @@ describe('PortfolioCalculator', () => {
symbol: 'VTI',
type: OrderType.Sell,
unitPrice: new Big('151.41'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
fee: new Big('5')
}
@ -291,7 +290,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('10'),
symbol: 'VTI',
investment: new Big('1443.8'),
currency: Currency.USD,
currency: 'USD',
firstBuyDate: '2019-02-01',
transactionCount: 1,
fee: new Big('5')
@ -306,7 +305,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('10'),
symbol: 'VTI',
investment: new Big('1443.8'),
currency: Currency.USD,
currency: 'USD',
firstBuyDate: '2019-02-01',
transactionCount: 1,
fee: new Big('5')
@ -316,7 +315,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('10'),
symbol: 'VTX',
investment: new Big('1479.9'),
currency: Currency.USD,
currency: 'USD',
firstBuyDate: '2019-08-03',
transactionCount: 1,
fee: new Big('10')
@ -331,7 +330,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('5'),
symbol: 'VTI',
investment: new Big('686.75'),
currency: Currency.USD,
currency: 'USD',
firstBuyDate: '2019-02-01',
transactionCount: 2,
fee: new Big('10')
@ -341,7 +340,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('10'),
symbol: 'VTX',
investment: new Big('1479.9'),
currency: Currency.USD,
currency: 'USD',
firstBuyDate: '2019-08-03',
transactionCount: 1,
fee: new Big('10')
@ -355,7 +354,7 @@ describe('PortfolioCalculator', () => {
const orders: PortfolioOrder[] = [
...ordersVTI,
{
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
date: '2021-02-01',
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
@ -368,7 +367,7 @@ describe('PortfolioCalculator', () => {
];
const portfolioCalculator = new PortfolioCalculator(
currentRateService,
Currency.USD
'USD'
);
portfolioCalculator.computeTransactionPoints(orders);
const portfolioItemsAtTransactionPoints =
@ -379,7 +378,7 @@ describe('PortfolioCalculator', () => {
date: '2019-02-01',
items: [
{
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01',
investment: new Big('1443.8'),
@ -394,7 +393,7 @@ describe('PortfolioCalculator', () => {
date: '2019-08-03',
items: [
{
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01',
investment: new Big('2923.7'),
@ -409,7 +408,7 @@ describe('PortfolioCalculator', () => {
date: '2020-02-02',
items: [
{
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01',
investment: new Big('652.55'),
@ -424,7 +423,7 @@ describe('PortfolioCalculator', () => {
date: '2021-02-01',
items: [
{
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01',
investment: new Big('6627.05'),
@ -439,7 +438,7 @@ describe('PortfolioCalculator', () => {
date: '2021-08-01',
items: [
{
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01',
investment: new Big('8403.95'),
@ -457,7 +456,7 @@ describe('PortfolioCalculator', () => {
const orders: PortfolioOrder[] = [
...ordersVTI,
{
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
date: '2019-09-01',
name: 'Amazon.com, Inc.',
@ -470,7 +469,7 @@ describe('PortfolioCalculator', () => {
];
const portfolioCalculator = new PortfolioCalculator(
currentRateService,
Currency.USD
'USD'
);
portfolioCalculator.computeTransactionPoints(orders);
const portfolioItemsAtTransactionPoints =
@ -485,7 +484,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('10'),
symbol: 'VTI',
investment: new Big('1443.8'),
currency: Currency.USD,
currency: 'USD',
firstBuyDate: '2019-02-01',
fee: new Big(0),
transactionCount: 1
@ -500,7 +499,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('20'),
symbol: 'VTI',
investment: new Big('2923.7'),
currency: Currency.USD,
currency: 'USD',
firstBuyDate: '2019-02-01',
fee: new Big(0),
transactionCount: 2
@ -515,7 +514,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('5'),
symbol: 'AMZN',
investment: new Big('10109.95'),
currency: Currency.USD,
currency: 'USD',
firstBuyDate: '2019-09-01',
fee: new Big(0),
transactionCount: 1
@ -525,7 +524,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('20'),
symbol: 'VTI',
investment: new Big('2923.7'),
currency: Currency.USD,
currency: 'USD',
firstBuyDate: '2019-02-01',
fee: new Big(0),
transactionCount: 2
@ -540,7 +539,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('5'),
symbol: 'AMZN',
investment: new Big('10109.95'),
currency: Currency.USD,
currency: 'USD',
firstBuyDate: '2019-09-01',
fee: new Big(0),
transactionCount: 1
@ -550,7 +549,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('5'),
symbol: 'VTI',
investment: new Big('652.55'),
currency: Currency.USD,
currency: 'USD',
firstBuyDate: '2019-02-01',
fee: new Big(0),
transactionCount: 3
@ -565,7 +564,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('5'),
symbol: 'AMZN',
investment: new Big('10109.95'),
currency: Currency.USD,
currency: 'USD',
firstBuyDate: '2019-09-01',
fee: new Big(0),
transactionCount: 1
@ -575,7 +574,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('15'),
symbol: 'VTI',
investment: new Big('2684.05'),
currency: Currency.USD,
currency: 'USD',
firstBuyDate: '2019-02-01',
fee: new Big(0),
transactionCount: 4
@ -590,7 +589,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('5'),
symbol: 'AMZN',
investment: new Big('10109.95'),
currency: Currency.USD,
currency: 'USD',
firstBuyDate: '2019-09-01',
fee: new Big(0),
transactionCount: 1
@ -600,7 +599,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('25'),
symbol: 'VTI',
investment: new Big('4460.95'),
currency: Currency.USD,
currency: 'USD',
firstBuyDate: '2019-02-01',
fee: new Big(0),
transactionCount: 5
@ -620,7 +619,7 @@ describe('PortfolioCalculator', () => {
symbol: 'AMZN',
type: OrderType.Buy,
unitPrice: new Big('2021.99'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
fee: new Big(0)
},
@ -631,14 +630,14 @@ describe('PortfolioCalculator', () => {
symbol: 'AMZN',
type: OrderType.Sell,
unitPrice: new Big('2412.23'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
fee: new Big(0)
}
];
const portfolioCalculator = new PortfolioCalculator(
currentRateService,
Currency.USD
'USD'
);
portfolioCalculator.computeTransactionPoints(orders);
const portfolioItemsAtTransactionPoints =
@ -652,7 +651,7 @@ describe('PortfolioCalculator', () => {
it('with mixed symbols', () => {
const portfolioCalculator = new PortfolioCalculator(
currentRateService,
Currency.USD
'USD'
);
portfolioCalculator.computeTransactionPoints(ordersMixedSymbols);
const portfolioItemsAtTransactionPoints =
@ -667,7 +666,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('50'),
symbol: 'TSLA',
investment: new Big('2148.5'),
currency: Currency.USD,
currency: 'USD',
firstBuyDate: '2017-01-03',
fee: new Big(0),
transactionCount: 1
@ -682,7 +681,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('0.5614682'),
symbol: 'BTCUSD',
investment: new Big('1999.9999999999998659756'),
currency: Currency.USD,
currency: 'USD',
firstBuyDate: '2017-07-01',
fee: new Big(0),
transactionCount: 1
@ -692,7 +691,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('50'),
symbol: 'TSLA',
investment: new Big('2148.5'),
currency: Currency.USD,
currency: 'USD',
firstBuyDate: '2017-01-03',
fee: new Big(0),
transactionCount: 1
@ -707,7 +706,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('5'),
symbol: 'AMZN',
investment: new Big('10109.95'),
currency: Currency.USD,
currency: 'USD',
firstBuyDate: '2018-09-01',
fee: new Big(0),
transactionCount: 1
@ -717,7 +716,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('0.5614682'),
symbol: 'BTCUSD',
investment: new Big('1999.9999999999998659756'),
currency: Currency.USD,
currency: 'USD',
firstBuyDate: '2017-07-01',
fee: new Big(0),
transactionCount: 1
@ -727,7 +726,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('50'),
symbol: 'TSLA',
investment: new Big('2148.5'),
currency: Currency.USD,
currency: 'USD',
firstBuyDate: '2017-01-03',
fee: new Big(0),
transactionCount: 1
@ -742,7 +741,7 @@ describe('PortfolioCalculator', () => {
it('with single TSLA and early start', async () => {
const portfolioCalculator = new PortfolioCalculator(
currentRateService,
Currency.USD
'USD'
);
portfolioCalculator.setTransactionPoints(orderTslaTransactionPoint);
@ -782,7 +781,7 @@ describe('PortfolioCalculator', () => {
it('with single TSLA and buy day start', async () => {
const portfolioCalculator = new PortfolioCalculator(
currentRateService,
Currency.USD
'USD'
);
portfolioCalculator.setTransactionPoints(orderTslaTransactionPoint);
@ -822,7 +821,7 @@ describe('PortfolioCalculator', () => {
it('with single TSLA and late start', async () => {
const portfolioCalculator = new PortfolioCalculator(
currentRateService,
Currency.USD
'USD'
);
portfolioCalculator.setTransactionPoints(orderTslaTransactionPoint);
@ -862,7 +861,7 @@ describe('PortfolioCalculator', () => {
it('with VTI only', async () => {
const portfolioCalculator = new PortfolioCalculator(
currentRateService,
Currency.USD
'USD'
);
portfolioCalculator.setTransactionPoints(ordersVTITransactionPoints);
@ -905,7 +904,7 @@ describe('PortfolioCalculator', () => {
it('with buy and sell', async () => {
const portfolioCalculator = new PortfolioCalculator(
currentRateService,
Currency.USD
'USD'
);
portfolioCalculator.setTransactionPoints(transactionPointsBuyAndSell);
@ -959,7 +958,7 @@ describe('PortfolioCalculator', () => {
it('with buy, sell, buy', async () => {
const portfolioCalculator = new PortfolioCalculator(
currentRateService,
Currency.USD
'USD'
);
portfolioCalculator.setTransactionPoints([
{
@ -969,7 +968,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('5'),
symbol: 'VTI',
investment: new Big('805.9'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-09-01',
fee: new Big(0),
@ -984,7 +983,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('0'),
symbol: 'VTI',
investment: new Big('0'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-09-01',
fee: new Big(0),
@ -999,7 +998,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('5'),
symbol: 'VTI',
investment: new Big('1013.9'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-09-01',
fee: new Big(0),
@ -1047,7 +1046,7 @@ describe('PortfolioCalculator', () => {
it('with performance since Jan 1st, 2020', async () => {
const portfolioCalculator = new PortfolioCalculator(
currentRateService,
Currency.USD
'USD'
);
const transactionPoints: TransactionPoint[] = [
{
@ -1057,7 +1056,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('10'),
symbol: 'VTI',
investment: new Big('1443.8'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01',
fee: new Big(0),
@ -1072,7 +1071,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('20'),
symbol: 'VTI',
investment: new Big('2923.7'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01',
fee: new Big(0),
@ -1130,7 +1129,7 @@ describe('PortfolioCalculator', () => {
it('with net performance since Jan 1st, 2020 - include fees', async () => {
const portfolioCalculator = new PortfolioCalculator(
currentRateService,
Currency.USD
'USD'
);
const transactionPoints: TransactionPoint[] = [
{
@ -1140,7 +1139,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('10'),
symbol: 'VTI',
investment: new Big('1443.8'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01',
fee: new Big(50),
@ -1155,7 +1154,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('20'),
symbol: 'VTI',
investment: new Big('2923.7'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01',
fee: new Big(100),
@ -1223,7 +1222,7 @@ describe('PortfolioCalculator', () => {
it('with net performance since Feb 1st, 2019 - include fees', async () => {
const portfolioCalculator = new PortfolioCalculator(
currentRateService,
Currency.USD
'USD'
);
const transactionPoints: TransactionPoint[] = [
{
@ -1233,7 +1232,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('10'),
symbol: 'VTI',
investment: new Big('1443.8'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01',
fee: new Big(50),
@ -1248,7 +1247,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('20'),
symbol: 'VTI',
investment: new Big('2923.7'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01',
fee: new Big(100),
@ -1311,7 +1310,7 @@ describe('PortfolioCalculator', () => {
it('with TWR example from Investopedia: Scenario 1', async () => {
const portfolioCalculator = new PortfolioCalculator(
currentRateService,
Currency.USD
'USD'
);
portfolioCalculator.setTransactionPoints([
{
@ -1321,7 +1320,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('1000000'), // 1 million
symbol: 'MFA', // Mutual Fund A
investment: new Big('1000000'), // 1 million
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2010-12-31',
fee: new Big(0),
@ -1336,7 +1335,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('1086022.689344541'), // 1,000,000 + 100,000 / 1.162484
symbol: 'MFA', // Mutual Fund A
investment: new Big('1100000'), // 1,000,000 + 100,000
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2010-12-31',
fee: new Big(0),
@ -1388,7 +1387,7 @@ describe('PortfolioCalculator', () => {
it('with example from chsoft.ch: Performance of a Combination of Investments', async () => {
const portfolioCalculator = new PortfolioCalculator(
currentRateService,
Currency.CHF
'CHF'
);
portfolioCalculator.setTransactionPoints([
{
@ -1398,7 +1397,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('200'),
symbol: 'SPA', // Sub Portfolio A
investment: new Big('200'),
currency: Currency.CHF,
currency: 'CHF',
dataSource: DataSource.YAHOO,
firstBuyDate: '2012-12-31',
fee: new Big(0),
@ -1408,7 +1407,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('300'),
symbol: 'SPB', // Sub Portfolio B
investment: new Big('300'),
currency: Currency.CHF,
currency: 'CHF',
dataSource: DataSource.YAHOO,
firstBuyDate: '2012-12-31',
fee: new Big(0),
@ -1423,7 +1422,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('200'),
symbol: 'SPA', // Sub Portfolio A
investment: new Big('200'),
currency: Currency.CHF,
currency: 'CHF',
dataSource: DataSource.YAHOO,
firstBuyDate: '2012-12-31',
fee: new Big(0),
@ -1433,7 +1432,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('300'),
symbol: 'SPB', // Sub Portfolio B
investment: new Big('300'),
currency: Currency.CHF,
currency: 'CHF',
dataSource: DataSource.YAHOO,
firstBuyDate: '2012-12-31',
fee: new Big(0),
@ -1494,7 +1493,7 @@ describe('PortfolioCalculator', () => {
it('with yearly', async () => {
const portfolioCalculator = new PortfolioCalculator(
currentRateService,
Currency.USD
'USD'
);
portfolioCalculator.setTransactionPoints(ordersVTITransactionPoints);
const timelineSpecification: TimelineSpecification[] = [
@ -1537,7 +1536,7 @@ describe('PortfolioCalculator', () => {
it('with yearly and fees', async () => {
const portfolioCalculator = new PortfolioCalculator(
currentRateService,
Currency.USD
'USD'
);
const transactionPoints: TransactionPoint[] = [
{
@ -1547,7 +1546,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('10'),
symbol: 'VTI',
investment: new Big('1443.8'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01',
fee: new Big(50),
@ -1562,7 +1561,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('20'),
symbol: 'VTI',
investment: new Big('2923.7'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01',
fee: new Big(100),
@ -1577,7 +1576,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('5'),
symbol: 'VTI',
investment: new Big('652.55'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01',
fee: new Big(150),
@ -1592,7 +1591,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('15'),
symbol: 'VTI',
investment: new Big('2684.05'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01',
fee: new Big(200),
@ -1607,7 +1606,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('25'),
symbol: 'VTI',
investment: new Big('4460.95'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01',
fee: new Big(250),
@ -1657,7 +1656,7 @@ describe('PortfolioCalculator', () => {
it('with monthly', async () => {
const portfolioCalculator = new PortfolioCalculator(
currentRateService,
Currency.USD
'USD'
);
portfolioCalculator.setTransactionPoints(ordersVTITransactionPoints);
const timelineSpecification: TimelineSpecification[] = [
@ -1889,7 +1888,7 @@ describe('PortfolioCalculator', () => {
it('with yearly and monthly mixed', async () => {
const portfolioCalculator = new PortfolioCalculator(
currentRateService,
Currency.USD
'USD'
);
portfolioCalculator.setTransactionPoints(ordersVTITransactionPoints);
const timelineSpecification: TimelineSpecification[] = [
@ -1971,7 +1970,7 @@ describe('PortfolioCalculator', () => {
it('with all mixed', async () => {
const portfolioCalculator = new PortfolioCalculator(
currentRateService,
Currency.USD
'USD'
);
portfolioCalculator.setTransactionPoints(ordersVTITransactionPoints);
const timelineSpecification: TimelineSpecification[] = [
@ -2262,7 +2261,7 @@ describe('PortfolioCalculator', () => {
it('with mixed portfolio', async () => {
const portfolioCalculator = new PortfolioCalculator(
currentRateService,
Currency.USD
'USD'
);
portfolioCalculator.setTransactionPoints([
{
@ -2272,7 +2271,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('5'),
symbol: 'AMZN',
investment: new Big('10109.95'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01',
fee: new Big(0),
@ -2282,7 +2281,7 @@ describe('PortfolioCalculator', () => {
quantity: new Big('10'),
symbol: 'VTI',
investment: new Big('1443.8'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01',
fee: new Big(0),
@ -2325,7 +2324,7 @@ describe('PortfolioCalculator', () => {
describe('annualized performance percentage', () => {
const portfolioCalculator = new PortfolioCalculator(
currentRateService,
Currency.USD
'USD'
);
it('Get annualized performance', async () => {
@ -2391,7 +2390,7 @@ const ordersMixedSymbols: PortfolioOrder[] = [
symbol: 'TSLA',
type: OrderType.Buy,
unitPrice: new Big('42.97'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
fee: new Big(0)
},
@ -2402,7 +2401,7 @@ const ordersMixedSymbols: PortfolioOrder[] = [
symbol: 'BTCUSD',
type: OrderType.Buy,
unitPrice: new Big('3562.089535970158'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
fee: new Big(0)
},
@ -2413,7 +2412,7 @@ const ordersMixedSymbols: PortfolioOrder[] = [
symbol: 'AMZN',
type: OrderType.Buy,
unitPrice: new Big('2021.99'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
fee: new Big(0)
}
@ -2427,7 +2426,7 @@ const ordersVTI: PortfolioOrder[] = [
symbol: 'VTI',
type: OrderType.Buy,
unitPrice: new Big('144.38'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
fee: new Big(0)
},
@ -2438,7 +2437,7 @@ const ordersVTI: PortfolioOrder[] = [
symbol: 'VTI',
type: OrderType.Buy,
unitPrice: new Big('147.99'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
fee: new Big(0)
},
@ -2449,7 +2448,7 @@ const ordersVTI: PortfolioOrder[] = [
symbol: 'VTI',
type: OrderType.Sell,
unitPrice: new Big('151.41'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
fee: new Big(0)
},
@ -2460,7 +2459,7 @@ const ordersVTI: PortfolioOrder[] = [
symbol: 'VTI',
type: OrderType.Buy,
unitPrice: new Big('177.69'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
fee: new Big(0)
},
@ -2471,7 +2470,7 @@ const ordersVTI: PortfolioOrder[] = [
symbol: 'VTI',
type: OrderType.Buy,
unitPrice: new Big('203.15'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
fee: new Big(0)
}
@ -2485,7 +2484,7 @@ const orderTslaTransactionPoint: TransactionPoint[] = [
quantity: new Big('1'),
symbol: 'TSLA',
investment: new Big('719.46'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2021-01-01',
fee: new Big(0),
@ -2503,7 +2502,7 @@ const ordersVTITransactionPoints: TransactionPoint[] = [
quantity: new Big('10'),
symbol: 'VTI',
investment: new Big('1443.8'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01',
fee: new Big(0),
@ -2518,7 +2517,7 @@ const ordersVTITransactionPoints: TransactionPoint[] = [
quantity: new Big('20'),
symbol: 'VTI',
investment: new Big('2923.7'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01',
fee: new Big(0),
@ -2533,7 +2532,7 @@ const ordersVTITransactionPoints: TransactionPoint[] = [
quantity: new Big('5'),
symbol: 'VTI',
investment: new Big('652.55'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01',
fee: new Big(0),
@ -2548,7 +2547,7 @@ const ordersVTITransactionPoints: TransactionPoint[] = [
quantity: new Big('15'),
symbol: 'VTI',
investment: new Big('2684.05'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01',
fee: new Big(0),
@ -2563,7 +2562,7 @@ const ordersVTITransactionPoints: TransactionPoint[] = [
quantity: new Big('25'),
symbol: 'VTI',
investment: new Big('4460.95'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01',
fee: new Big(0),
@ -2581,7 +2580,7 @@ const transactionPointsBuyAndSell: TransactionPoint[] = [
quantity: new Big('10'),
symbol: 'VTI',
investment: new Big('1443.8'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01',
fee: new Big(0),
@ -2596,7 +2595,7 @@ const transactionPointsBuyAndSell: TransactionPoint[] = [
quantity: new Big('20'),
symbol: 'VTI',
investment: new Big('2923.7'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01',
fee: new Big(0),
@ -2611,7 +2610,7 @@ const transactionPointsBuyAndSell: TransactionPoint[] = [
quantity: new Big('5'),
symbol: 'AMZN',
investment: new Big('10109.95'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-09-01',
fee: new Big(0),
@ -2621,7 +2620,7 @@ const transactionPointsBuyAndSell: TransactionPoint[] = [
quantity: new Big('20'),
symbol: 'VTI',
investment: new Big('2923.7'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01',
fee: new Big(0),
@ -2636,7 +2635,7 @@ const transactionPointsBuyAndSell: TransactionPoint[] = [
quantity: new Big('5'),
symbol: 'AMZN',
investment: new Big('10109.95'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-09-01',
fee: new Big(0),
@ -2646,7 +2645,7 @@ const transactionPointsBuyAndSell: TransactionPoint[] = [
quantity: new Big('5'),
symbol: 'VTI',
investment: new Big('652.55'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01',
fee: new Big(0),
@ -2661,7 +2660,7 @@ const transactionPointsBuyAndSell: TransactionPoint[] = [
quantity: new Big('0'),
symbol: 'AMZN',
investment: new Big('0'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-09-01',
fee: new Big(0),
@ -2671,7 +2670,7 @@ const transactionPointsBuyAndSell: TransactionPoint[] = [
quantity: new Big('5'),
symbol: 'VTI',
investment: new Big('652.55'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01',
fee: new Big(0),
@ -2686,7 +2685,7 @@ const transactionPointsBuyAndSell: TransactionPoint[] = [
quantity: new Big('0'),
symbol: 'AMZN',
investment: new Big('0'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-09-01',
fee: new Big(0),
@ -2696,7 +2695,7 @@ const transactionPointsBuyAndSell: TransactionPoint[] = [
quantity: new Big('15'),
symbol: 'VTI',
investment: new Big('2684.05'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01',
fee: new Big(0),
@ -2711,7 +2710,7 @@ const transactionPointsBuyAndSell: TransactionPoint[] = [
quantity: new Big('0'),
symbol: 'AMZN',
investment: new Big('0'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-09-01',
fee: new Big(0),
@ -2721,7 +2720,7 @@ const transactionPointsBuyAndSell: TransactionPoint[] = [
quantity: new Big('25'),
symbol: 'VTI',
investment: new Big('4460.95'),
currency: Currency.USD,
currency: 'USD',
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01',
fee: new Big(0),

View File

@ -2,7 +2,6 @@ import { OrderType } from '@ghostfolio/api/models/order-type';
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
import { TimelinePosition } from '@ghostfolio/common/interfaces';
import { Currency, DataSource } from '@prisma/client';
import Big from 'big.js';
import {
addDays,
@ -35,7 +34,7 @@ export class PortfolioCalculator {
public constructor(
private currentRateService: CurrentRateService,
private currency: Currency
private currency: string
) {}
public computeTransactionPoints(orders: PortfolioOrder[]) {
@ -157,7 +156,7 @@ export class PortfolioCalculator {
let firstIndex = this.transactionPoints.length;
const dates = [];
const dataGatheringItems: IDataGatheringItem[] = [];
const currencies: { [symbol: string]: Currency } = {};
const currencies: { [symbol: string]: string } = {};
dates.push(resetHours(start));
for (const item of this.transactionPoints[firstIndex - 1].items) {
@ -521,7 +520,7 @@ export class PortfolioCalculator {
[date: string]: { [symbol: string]: Big };
} = {};
if (j >= 0) {
const currencies: { [name: string]: Currency } = {};
const currencies: { [name: string]: string } = {};
const dataGatheringItems: IDataGatheringItem[] = [];
for (const item of this.transactionPoints[j].items) {

View File

@ -3,6 +3,7 @@ import {
hasNotDefinedValuesInObject,
nullifyValuesInObject
} from '@ghostfolio/api/helper/object.helper';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import {
PortfolioDetails,
@ -38,6 +39,7 @@ import { PortfolioService } from './portfolio.service';
@Controller('portfolio')
export class PortfolioController {
public constructor(
private readonly configurationService: ConfigurationService,
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly portfolioService: PortfolioService,
@Inject(REQUEST) private readonly request: RequestWithUser,
@ -47,8 +49,17 @@ export class PortfolioController {
@Get('investments')
@UseGuards(AuthGuard('jwt'))
public async findAll(
@Headers('impersonation-id') impersonationId
@Headers('impersonation-id') impersonationId,
@Res() res: Response
): Promise<InvestmentItem[]> {
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === 'Basic'
) {
res.status(StatusCodes.FORBIDDEN);
return <any>res.json([]);
}
let investments = await this.portfolioService.getInvestments(
impersonationId
);
@ -68,7 +79,7 @@ export class PortfolioController {
}));
}
return investments;
return <any>res.json(investments);
}
@Get('chart')
@ -125,6 +136,14 @@ export class PortfolioController {
@Query('range') range,
@Res() res: Response
): Promise<PortfolioDetails> {
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === 'Basic'
) {
res.status(StatusCodes.FORBIDDEN);
return <any>res.json({ accounts: {}, holdings: {} });
}
const { accounts, holdings, hasErrors } =
await this.portfolioService.getDetails(impersonationId, range);
@ -156,8 +175,9 @@ export class PortfolioController {
portfolioPosition.grossPerformance = null;
portfolioPosition.investment =
portfolioPosition.investment / totalInvestment;
portfolioPosition.netPerformance = null;
portfolioPosition.quantity = null;
portfolioPosition.value = portfolioPosition.value / totalValue;
}
for (const [name, { current, original }] of Object.entries(accounts)) {
@ -295,8 +315,19 @@ export class PortfolioController {
@Get('report')
@UseGuards(AuthGuard('jwt'))
public async getReport(
@Headers('impersonation-id') impersonationId
@Headers('impersonation-id') impersonationId,
@Res() res: Response
): Promise<PortfolioReport> {
return await this.portfolioService.getReport(impersonationId);
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === 'Basic'
) {
res.status(StatusCodes.FORBIDDEN);
return <any>res.json({ rules: [] });
}
return <any>(
res.json(await this.portfolioService.getReport(impersonationId))
);
}
}

View File

@ -39,15 +39,9 @@ import type {
} from '@ghostfolio/common/types';
import { Inject, Injectable } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import {
AssetClass,
Currency,
DataSource,
Type as TypeOfOrder
} from '@prisma/client';
import { AssetClass, DataSource, Type as TypeOfOrder } from '@prisma/client';
import Big from 'big.js';
import {
differenceInDays,
endOfToday,
format,
isAfter,
@ -59,7 +53,7 @@ import {
subDays,
subYears
} from 'date-fns';
import { isEmpty, isNumber } from 'lodash';
import { isEmpty } from 'lodash';
import {
HistoricalDataItem,
@ -775,7 +769,7 @@ export class PortfolioService {
assetClass: AssetClass.CASH,
assetSubClass: AssetClass.CASH,
countries: [],
currency: Currency.CHF,
currency: 'CHF',
grossPerformance: 0,
grossPerformancePercent: 0,
investment: cashValue.toNumber(),
@ -865,7 +859,7 @@ export class PortfolioService {
private async getAccounts(
orders: OrderWithAccount[],
portfolioItemsNow: { [p: string]: TimelinePosition },
userCurrency: Currency,
userCurrency: string,
userId: string
) {
const accounts: PortfolioDetails['accounts'] = {};
@ -938,7 +932,7 @@ export class PortfolioService {
private getTotalByType(
orders: OrderWithAccount[],
currency: Currency,
currency: string,
type: TypeOfOrder
) {
return orders

View File

@ -1,7 +1,6 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { Rule } from '@ghostfolio/api/models/rule';
import { Injectable } from '@nestjs/common';
import { Currency } from '@prisma/client';
@Injectable()
export class RulesService {
@ -9,7 +8,7 @@ export class RulesService {
public async evaluate<T extends RuleSettings>(
aRules: Rule<T>[],
aUserSettings: { baseCurrency: Currency }
aUserSettings: { baseCurrency: string }
) {
return aRules
.filter((rule) => {

View File

@ -1,7 +1,7 @@
import { Currency, DataSource } from '@prisma/client';
import { DataSource } from '@prisma/client';
export interface LookupItem {
currency: Currency;
currency: string;
dataSource: DataSource;
name: string;
symbol: string;

View File

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

View File

@ -2,7 +2,7 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { Injectable } from '@nestjs/common';
import { Currency, DataSource } from '@prisma/client';
import { DataSource } from '@prisma/client';
import { LookupItem } from './interfaces/lookup-item.interface';
import { SymbolItem } from './interfaces/symbol-item.interface';
@ -20,8 +20,8 @@ export class SymbolService {
if (dataGatheringItem.dataSource && marketPrice) {
return {
currency,
marketPrice,
currency: <Currency>(<unknown>currency),
dataSource: dataGatheringItem.dataSource
};
}

View File

@ -1,7 +1,7 @@
import { Currency, ViewMode } from '@prisma/client';
import { ViewMode } from '@prisma/client';
export interface UserSettingsParams {
currency?: Currency;
currency?: string;
userId: string;
viewMode?: ViewMode;
}

View File

@ -1,9 +1,9 @@
import { Currency, ViewMode } from '@prisma/client';
import { ViewMode } from '@prisma/client';
import { IsString } from 'class-validator';
export class UpdateUserSettingsDto {
@IsString()
baseCurrency: Currency;
baseCurrency: string;
@IsString()
viewMode: ViewMode;

View File

@ -1,12 +1,12 @@
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { locale } from '@ghostfolio/common/config';
import { baseCurrency, locale } from '@ghostfolio/common/config';
import { User as IUser, UserWithSettings } from '@ghostfolio/common/interfaces';
import { getPermissions, permissions } from '@ghostfolio/common/permissions';
import { SubscriptionType } from '@ghostfolio/common/types/subscription.type';
import { Injectable } from '@nestjs/common';
import { Currency, Prisma, Provider, User, ViewMode } from '@prisma/client';
import { Prisma, Provider, User, ViewMode } from '@prisma/client';
import { UserSettingsParams } from './interfaces/user-settings-params.interface';
import { UserSettings } from './interfaces/user-settings.interface';
@ -15,7 +15,7 @@ const crypto = require('crypto');
@Injectable()
export class UserService {
public static DEFAULT_CURRENCY = Currency.USD;
public static DEFAULT_CURRENCY = 'USD';
public constructor(
private readonly configurationService: ConfigurationService,
@ -144,9 +144,15 @@ export class UserService {
...data,
Account: {
create: {
currency: baseCurrency,
isDefault: true,
name: 'Default Account'
}
},
Settings: {
create: {
currency: baseCurrency
}
}
}
});

View File

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

View File

@ -1,4 +1,4 @@
import { Account, Currency, SymbolProfile } from '@prisma/client';
import { Account, SymbolProfile } from '@prisma/client';
import { v4 as uuidv4 } from 'uuid';
import { IOrder } from '../services/interfaces/interfaces';
@ -6,7 +6,7 @@ import { OrderType } from './order-type';
export class Order {
private account: Account;
private currency: Currency;
private currency: string;
private fee: number;
private date: string;
private id: string;

View File

@ -3,7 +3,6 @@ import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.in
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { groupBy } from '@ghostfolio/common/helper';
import { TimelinePosition } from '@ghostfolio/common/interfaces';
import { Currency } from '@prisma/client';
import { EvaluationResult } from './interfaces/evaluation-result.interface';
import { RuleInterface } from './interfaces/rule.interface';
@ -29,7 +28,7 @@ export abstract class Rule<T extends RuleSettings> implements RuleInterface<T> {
public groupCurrentPositionsByAttribute(
positions: TimelinePosition[],
attribute: keyof TimelinePosition,
baseCurrency: Currency
baseCurrency: string
) {
return Array.from(groupBy(attribute, positions).entries()).map(
([attributeValue, objs]) => ({

View File

@ -2,8 +2,6 @@ import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/curre
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
import { Currency } from '@prisma/client';
import { Rule } from '../../rule';
@ -69,5 +67,5 @@ export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule<Setti
}
interface Settings extends RuleSettings {
baseCurrency: Currency;
baseCurrency: string;
}

View File

@ -1,7 +1,6 @@
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
import { Currency } from '@prisma/client';
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
import { Rule } from '../../rule';
@ -69,5 +68,5 @@ export class CurrencyClusterRiskBaseCurrencyInitialInvestment extends Rule<Setti
}
interface Settings extends RuleSettings {
baseCurrency: Currency;
baseCurrency: string;
}

View File

@ -1,7 +1,6 @@
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
import { Currency } from '@prisma/client';
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
import { Rule } from '../../rule';
@ -69,6 +68,6 @@ export class CurrencyClusterRiskCurrentInvestment extends Rule<Settings> {
}
interface Settings extends RuleSettings {
baseCurrency: Currency;
baseCurrency: string;
threshold: number;
}

View File

@ -1,7 +1,6 @@
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
import { Currency } from '@prisma/client';
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
import { Rule } from '../../rule';
@ -69,6 +68,6 @@ export class CurrencyClusterRiskInitialInvestment extends Rule<Settings> {
}
interface Settings extends RuleSettings {
baseCurrency: Currency;
baseCurrency: string;
threshold: number;
}

View File

@ -1,6 +1,5 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
import { Currency } from '@prisma/client';
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
import { Rule } from '../../rule';
@ -46,6 +45,6 @@ export class FeeRatioInitialInvestment extends Rule<Settings> {
}
interface Settings extends RuleSettings {
baseCurrency: Currency;
baseCurrency: string;
threshold: number;
}

View File

@ -4,8 +4,15 @@ import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
import { Module } from '@nestjs/common';
import { ExchangeRateDataModule } from './exchange-rate-data.module';
@Module({
imports: [ConfigurationModule, DataProviderModule, PrismaModule],
imports: [
ConfigurationModule,
DataProviderModule,
ExchangeRateDataModule,
PrismaModule
],
providers: [DataGatheringService],
exports: [DataGatheringService]
})

View File

@ -1,9 +1,8 @@
import {
benchmarks,
currencyPairs,
ghostfolioFearAndGreedIndexSymbol
} from '@ghostfolio/common/config';
import { DATE_FORMAT, getUtc, resetHours } from '@ghostfolio/common/helper';
import { DATE_FORMAT, resetHours } from '@ghostfolio/common/helper';
import { Injectable } from '@nestjs/common';
import { DataSource } from '@prisma/client';
import {
@ -19,6 +18,7 @@ import {
import { ConfigurationService } from './configuration.service';
import { DataProviderService } from './data-provider/data-provider.service';
import { GhostfolioScraperApiService } from './data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
import { ExchangeRateDataService } from './exchange-rate-data.service';
import { IDataGatheringItem } from './interfaces/interfaces';
import { PrismaService } from './prisma.service';
@ -27,6 +27,7 @@ export class DataGatheringService {
public constructor(
private readonly configurationService: ConfigurationService,
private readonly dataProviderService: DataProviderService,
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly ghostfolioScraperApi: GhostfolioScraperApiService,
private readonly prismaService: PrismaService
) {}
@ -131,7 +132,15 @@ export class DataGatheringService {
for (const [
symbol,
{ assetClass, assetSubClass, countries, currency, dataSource, name }
{
assetClass,
assetSubClass,
countries,
currency,
dataSource,
name,
sectors
}
] of Object.entries(currentData)) {
try {
await this.prismaService.symbolProfile.upsert({
@ -142,6 +151,7 @@ export class DataGatheringService {
currency,
dataSource,
name,
sectors,
symbol
},
update: {
@ -149,7 +159,8 @@ export class DataGatheringService {
assetSubClass,
countries,
currency,
name
name,
sectors
},
where: {
dataSource_symbol: {
@ -230,6 +241,8 @@ export class DataGatheringService {
}
}
await this.exchangeRateDataService.initialize();
if (hasError) {
throw '';
}
@ -316,15 +329,15 @@ export class DataGatheringService {
};
});
const currencyPairsToGather = currencyPairs.map(
({ dataSource, symbol }) => {
const currencyPairsToGather = this.exchangeRateDataService
.getCurrencyPairs()
.map(({ dataSource, symbol }) => {
return {
dataSource,
symbol,
date: startDate
};
}
);
});
const customSymbolsToGather =
await this.ghostfolioScraperApi.getCustomSymbolsToGather(startDate);
@ -338,29 +351,46 @@ export class DataGatheringService {
}
private async getSymbolsMax(): Promise<IDataGatheringItem[]> {
const startDate = new Date(getUtc('2015-01-01'));
const startDate =
(
await this.prismaService.order.findFirst({
orderBy: [{ date: 'asc' }]
})
)?.date ?? new Date();
const customSymbolsToGather =
await this.ghostfolioScraperApi.getCustomSymbolsToGather(startDate);
const currencyPairsToGather = currencyPairs.map(
({ dataSource, symbol }) => {
const currencyPairsToGather = this.exchangeRateDataService
.getCurrencyPairs()
.map(({ dataSource, symbol }) => {
return {
dataSource,
symbol,
date: startDate
};
}
);
});
const symbolProfilesToGather =
const symbolProfilesToGather = (
await this.prismaService.symbolProfile.findMany({
orderBy: [{ symbol: 'asc' }],
select: {
dataSource: true,
Order: {
orderBy: [{ date: 'asc' }],
select: { date: true },
take: 1
},
symbol: true
}
});
})
).map((item) => {
return {
dataSource: item.dataSource,
date: item.Order?.[0]?.date ?? startDate,
symbol: item.symbol
};
});
return [
...this.getBenchmarksToGather(startDate),

View File

@ -6,11 +6,11 @@ import { Injectable } from '@nestjs/common';
import { DataSource } from '@prisma/client';
import { isAfter, isBefore, parse } from 'date-fns';
import { DataProviderInterface } from '../../interfaces/data-provider.interface';
import {
IDataProviderHistoricalResponse,
IDataProviderResponse
} from '../../interfaces/interfaces';
import { DataProviderInterface } from '../interfaces/data-provider.interface';
import { IAlphaVantageHistoricalResponse } from './interfaces/interfaces';
@Injectable()

View File

@ -0,0 +1,73 @@
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
import { IDataProviderResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import bent from 'bent';
const getJSON = bent('json');
export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
private static baseUrl = 'https://data.trackinsight.com/holdings';
private static countries = require('countries-list/dist/countries.json');
private static sectorsMapping = {
'Consumer Discretionary': 'Consumer Cyclical',
'Consumer Defensive': 'Consumer Staples',
'Health Care': 'Healthcare',
'Information Technology': 'Technology'
};
public async enhance({
response,
symbol
}: {
response: IDataProviderResponse;
symbol: string;
}): Promise<IDataProviderResponse> {
if (
!(response.assetClass === 'EQUITY' && response.assetSubClass === 'ETF')
) {
return response;
}
const holdings = await getJSON(
`${TrackinsightDataEnhancerService.baseUrl}/${symbol}.json`
).catch(() => {
return getJSON(
`${TrackinsightDataEnhancerService.baseUrl}/${
symbol.split('.')[0]
}.json`
);
});
if (!response.countries || response.countries.length === 0) {
response.countries = [];
for (const [name, value] of Object.entries<any>(holdings.countries)) {
let countryCode: string;
for (const [key, country] of Object.entries<any>(
TrackinsightDataEnhancerService.countries
)) {
if (country.name === name) {
countryCode = key;
break;
}
}
response.countries.push({
code: countryCode,
weight: value.weight
});
}
}
if (!response.sectors || response.sectors.length === 0) {
response.sectors = [];
for (const [name, value] of Object.entries<any>(holdings.sectors)) {
response.sectors.push({
name: TrackinsightDataEnhancerService.sectorsMapping[name] ?? name,
weight: value.weight
});
}
}
return Promise.resolve(response);
}
}

View File

@ -1,4 +1,5 @@
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
import { TrackinsightDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/trackinsight/trackinsight.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';
@ -15,7 +16,13 @@ import { DataProviderService } from './data-provider.service';
DataProviderService,
GhostfolioScraperApiService,
RakutenRapidApiService,
YahooFinanceService
TrackinsightDataEnhancerService,
YahooFinanceService,
{
inject: [TrackinsightDataEnhancerService],
provide: 'DataEnhancers',
useFactory: (trackinsight) => [trackinsight]
}
],
exports: [DataProviderService, GhostfolioScraperApiService]
})

View File

@ -1,5 +1,6 @@
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
import {
IDataGatheringItem,
IDataProviderHistoricalResponse,
@ -8,23 +9,23 @@ import {
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { Granularity } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common';
import { Inject, Injectable } from '@nestjs/common';
import { DataSource, MarketData } from '@prisma/client';
import { format } from 'date-fns';
import { isEmpty } from 'lodash';
import { AlphaVantageService } from './alpha-vantage/alpha-vantage.service';
import { GhostfolioScraperApiService } from './ghostfolio-scraper-api/ghostfolio-scraper-api.service';
import { RakutenRapidApiService } from './rakuten-rapid-api/rakuten-rapid-api.service';
import {
YahooFinanceService,
convertToYahooFinanceSymbol
} from './yahoo-finance/yahoo-finance.service';
import { YahooFinanceService } from './yahoo-finance/yahoo-finance.service';
@Injectable()
export class DataProviderService {
public constructor(
private readonly alphaVantageService: AlphaVantageService,
private readonly configurationService: ConfigurationService,
@Inject('DataEnhancers')
private readonly dataEnhancers: DataEnhancerInterface[],
private readonly ghostfolioScraperApiService: GhostfolioScraperApiService,
private readonly prismaService: PrismaService,
private readonly rakutenRapidApiService: RakutenRapidApiService,
@ -41,27 +42,35 @@ export class DataProviderService {
} = {};
for (const item of items) {
if (item.dataSource === DataSource.ALPHA_VANTAGE) {
response[item.symbol] = (
await this.alphaVantageService.get([item.symbol])
)[item.symbol];
} else if (item.dataSource === DataSource.GHOSTFOLIO) {
response[item.symbol] = (
await this.ghostfolioScraperApiService.get([item.symbol])
)[item.symbol];
} else if (item.dataSource === DataSource.RAKUTEN) {
response[item.symbol] = (
await this.rakutenRapidApiService.get([item.symbol])
)[item.symbol];
} else if (item.dataSource === DataSource.YAHOO) {
response[item.symbol] = (
await this.yahooFinanceService.get([
convertToYahooFinanceSymbol(item.symbol)
])
)[item.symbol];
}
const dataProvider = this.getDataProvider(item.dataSource);
response[item.symbol] = (await dataProvider.get([item.symbol]))[
item.symbol
];
}
const promises = [];
for (const symbol of Object.keys(response)) {
let promise = Promise.resolve(response[symbol]);
for (const dataEnhancer of this.dataEnhancers) {
promise = promise.then((currentResponse) =>
dataEnhancer
.enhance({ symbol, response: currentResponse })
.catch((error) => {
console.error(
`Failed to enhance data for symbol ${symbol}`,
error
);
return currentResponse;
})
);
}
promises.push(
promise.then((currentResponse) => (response[symbol] = currentResponse))
);
}
await Promise.all(promises);
return response;
}
@ -77,6 +86,10 @@ export class DataProviderService {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
} = {};
if (isEmpty(aItems)) {
return response;
}
const granularityQuery =
aGranularity === 'month'
? `AND (date_part('day', date) = 1 OR date >= TIMESTAMP 'yesterday')`
@ -98,11 +111,13 @@ export class DataProviderService {
});
try {
const queryRaw = `SELECT * FROM "MarketData" WHERE "dataSource" IN ('${dataSources.join(
`','`
)}') AND "symbol" IN ('${symbols.join(
`','`
)}') ${granularityQuery} ${rangeQuery} ORDER BY date;`;
const queryRaw = `SELECT *
FROM "MarketData"
WHERE "dataSource" IN ('${dataSources.join(`','`)}')
AND "symbol" IN ('${symbols.join(
`','`
)}') ${granularityQuery} ${rangeQuery}
ORDER BY date;`;
const marketDataByGranularity: MarketData[] =
await this.prismaService.$queryRaw(queryRaw);

View File

@ -12,13 +12,13 @@ import * as bent from 'bent';
import * as cheerio from 'cheerio';
import { format } from 'date-fns';
import { DataProviderInterface } from '../../interfaces/data-provider.interface';
import {
IDataGatheringItem,
IDataProviderHistoricalResponse,
IDataProviderResponse,
MarketState
} from '../../interfaces/interfaces';
import { DataProviderInterface } from '../interfaces/data-provider.interface';
import { ScraperConfig } from './interfaces/scraper-config.interface';
@Injectable()

View File

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

View File

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

View File

@ -4,7 +4,7 @@ import { Granularity } from '@ghostfolio/common/types';
import {
IDataProviderHistoricalResponse,
IDataProviderResponse
} from './interfaces';
} from '../../interfaces/interfaces';
export interface DataProviderInterface {
canHandle(symbol: string): boolean;

View File

@ -14,12 +14,12 @@ import { DataSource } from '@prisma/client';
import * as bent from 'bent';
import { format, subMonths, subWeeks, subYears } from 'date-fns';
import { DataProviderInterface } from '../../interfaces/data-provider.interface';
import {
IDataProviderHistoricalResponse,
IDataProviderResponse,
MarketState
} from '../../interfaces/interfaces';
import { DataProviderInterface } from '../interfaces/data-provider.interface';
@Injectable()
export class RakutenRapidApiService implements DataProviderInterface {

View File

@ -1,31 +1,21 @@
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
import {
DATE_FORMAT,
isCrypto,
isCurrency,
parseCurrency
} from '@ghostfolio/common/helper';
import { DATE_FORMAT, isCrypto, isCurrency } from '@ghostfolio/common/helper';
import { Granularity } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common';
import {
AssetClass,
AssetSubClass,
Currency,
DataSource
} from '@prisma/client';
import { AssetClass, AssetSubClass, DataSource } from '@prisma/client';
import * as bent from 'bent';
import Big from 'big.js';
import { countries } from 'countries-list';
import { format } from 'date-fns';
import * as yahooFinance from 'yahoo-finance';
import { DataProviderInterface } from '../../interfaces/data-provider.interface';
import {
IDataProviderHistoricalResponse,
IDataProviderResponse,
MarketState
} from '../../interfaces/interfaces';
import { DataProviderInterface } from '../interfaces/data-provider.interface';
import {
IYahooFinanceHistoricalResponse,
IYahooFinancePrice,
@ -43,11 +33,14 @@ export class YahooFinanceService implements DataProviderInterface {
}
public async get(
aYahooFinanceSymbols: string[]
aSymbols: string[]
): Promise<{ [symbol: string]: IDataProviderResponse }> {
if (aYahooFinanceSymbols.length <= 0) {
if (aSymbols.length <= 0) {
return {};
}
const yahooFinanceSymbols = aSymbols.map((symbol) =>
this.convertToYahooFinanceSymbol(symbol)
);
try {
const response: { [symbol: string]: IDataProviderResponse } = {};
@ -56,19 +49,19 @@ export class YahooFinanceService implements DataProviderInterface {
[symbol: string]: IYahooFinanceQuoteResponse;
} = await yahooFinance.quote({
modules: ['price', 'summaryProfile'],
symbols: aYahooFinanceSymbols
symbols: yahooFinanceSymbols
});
for (const [yahooFinanceSymbol, value] of Object.entries(data)) {
// Convert symbols back
const symbol = convertFromYahooFinanceSymbol(yahooFinanceSymbol);
const symbol = this.convertFromYahooFinanceSymbol(yahooFinanceSymbol);
const { assetClass, assetSubClass } = this.parseAssetClass(value.price);
response[symbol] = {
assetClass,
assetSubClass,
currency: parseCurrency(value.price?.currency),
currency: value.price?.currency,
dataSource: DataSource.YAHOO,
exchange: this.parseExchange(value.price?.exchangeName),
marketState:
@ -81,7 +74,7 @@ export class YahooFinanceService implements DataProviderInterface {
if (value.price?.currency === 'GBp') {
// Convert GBp (pence) to GBP
response[symbol].currency = Currency.GBP;
response[symbol].currency = 'GBP';
response[symbol].marketPrice = new Big(
value.price?.regularMarketPrice ?? 0
)
@ -103,6 +96,12 @@ export class YahooFinanceService implements DataProviderInterface {
response[symbol].countries = [{ code, weight: 1 }];
}
} catch {}
if (value.summaryProfile?.sector) {
response[symbol].sectors = [
{ name: value.summaryProfile?.sector, weight: 1 }
];
}
}
// Add url if available
@ -133,7 +132,7 @@ export class YahooFinanceService implements DataProviderInterface {
}
const yahooFinanceSymbols = aSymbols.map((symbol) => {
return convertToYahooFinanceSymbol(symbol);
return this.convertToYahooFinanceSymbol(symbol);
});
try {
@ -153,7 +152,7 @@ export class YahooFinanceService implements DataProviderInterface {
historicalData
)) {
// Convert symbols back
const symbol = convertFromYahooFinanceSymbol(yahooFinanceSymbol);
const symbol = this.convertFromYahooFinanceSymbol(yahooFinanceSymbol);
response[symbol] = {};
timeSeries.forEach((timeSerie) => {
@ -200,7 +199,7 @@ export class YahooFinanceService implements DataProviderInterface {
.filter(({ quoteType, symbol }) => {
if (quoteType === 'CRYPTOCURRENCY') {
// Only allow cryptocurrencies in USD
return symbol.includes(Currency.USD);
return symbol.includes('USD');
}
return true;
@ -224,6 +223,40 @@ export class YahooFinanceService implements DataProviderInterface {
return { items };
}
private convertFromYahooFinanceSymbol(aYahooFinanceSymbol: string) {
const symbol = aYahooFinanceSymbol.replace('-', '');
return symbol.replace('=X', '');
}
/**
* Converts a symbol to a Yahoo Finance symbol
*
* Currency: USDCHF -> USDCHF=X
* Cryptocurrency: BTCUSD -> BTC-USD
* DOGEUSD -> DOGE-USD
* SOL1USD -> SOL1-USD
*/
private convertToYahooFinanceSymbol(aSymbol: string) {
if (
(aSymbol.includes('CHF') ||
aSymbol.includes('EUR') ||
aSymbol.includes('USD')) &&
aSymbol.length >= 6
) {
if (isCurrency(aSymbol.substring(0, aSymbol.length - 3))) {
return `${aSymbol}=X`;
} else if (isCrypto(aSymbol) || isCrypto(aSymbol.replace('1', ''))) {
// Add a dash before the last three characters
// BTCUSD -> BTC-USD
// DOGEUSD -> DOGE-USD
// SOL1USD -> SOL1-USD
return aSymbol.replace('USD', '-USD');
}
}
return aSymbol;
}
private parseAssetClass(aPrice: IYahooFinancePrice): {
assetClass: AssetClass;
assetSubClass: AssetSubClass;
@ -257,31 +290,3 @@ export class YahooFinanceService implements DataProviderInterface {
return aString;
}
}
export const convertFromYahooFinanceSymbol = (aYahooFinanceSymbol: string) => {
const symbol = aYahooFinanceSymbol.replace('-', '');
return symbol.replace('=X', '');
};
/**
* Converts a symbol to a Yahoo Finance symbol
*
* Currency: USDCHF=X
* Cryptocurrency: BTC-USD
*/
export const convertToYahooFinanceSymbol = (aSymbol: string) => {
if (isCurrency(aSymbol)) {
if (isCrypto(aSymbol)) {
// Add a dash before the last three characters
// BTCUSD -> BTC-USD
// DOGEUSD -> DOGE-USD
return `${aSymbol.substring(0, aSymbol.length - 3)}-${aSymbol.substring(
aSymbol.length - 3
)}`;
}
return `${aSymbol}=X`;
}
return aSymbol;
};

View File

@ -2,8 +2,10 @@ import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { Module } from '@nestjs/common';
import { PrismaModule } from './prisma.module';
@Module({
imports: [DataProviderModule],
imports: [DataProviderModule, PrismaModule],
providers: [ExchangeRateDataService],
exports: [ExchangeRateDataService]
})

View File

@ -1,27 +1,45 @@
import { currencyPairs } from '@ghostfolio/common/config';
import { baseCurrency } from '@ghostfolio/common/config';
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
import { Injectable } from '@nestjs/common';
import { Currency, DataSource } from '@prisma/client';
import { DataSource } from '@prisma/client';
import { format } from 'date-fns';
import { isEmpty, isNumber } from 'lodash';
import { isEmpty, isNumber, uniq } from 'lodash';
import { DataProviderService } from './data-provider/data-provider.service';
import { IDataGatheringItem } from './interfaces/interfaces';
import { PrismaService } from './prisma.service';
@Injectable()
export class ExchangeRateDataService {
private currencies: string[] = [];
private currencyPairs: IDataGatheringItem[] = [];
private exchangeRates: { [currencyPair: string]: number } = {};
public constructor(private dataProviderService: DataProviderService) {
public constructor(
private readonly dataProviderService: DataProviderService,
private readonly prismaService: PrismaService
) {
this.initialize();
}
public getCurrencies() {
return this.currencies?.length > 0 ? this.currencies : [baseCurrency];
}
public getCurrencyPairs() {
return this.currencyPairs;
}
public async initialize() {
this.currencies = await this.prepareCurrencies();
this.currencyPairs = [];
this.exchangeRates = {};
for (const { currency1, currency2, dataSource } of currencyPairs) {
for (const {
currency1,
currency2,
dataSource
} of this.prepareCurrencyPairs(this.currencies)) {
this.addCurrencyPairs({ currency1, currency2, dataSource });
}
@ -77,8 +95,8 @@ export class ExchangeRateDataService {
if (!this.exchangeRates[symbol]) {
// Not found, calculate indirectly via USD
this.exchangeRates[symbol] =
resultExtended[`${currency1}${Currency.USD}`]?.[date]?.marketPrice *
resultExtended[`${Currency.USD}${currency2}`]?.[date]?.marketPrice;
resultExtended[`${currency1}${'USD'}`]?.[date]?.marketPrice *
resultExtended[`${'USD'}${currency2}`]?.[date]?.marketPrice;
// Calculate the opposite direction
this.exchangeRates[`${currency2}${currency1}`] =
@ -89,10 +107,14 @@ export class ExchangeRateDataService {
public toCurrency(
aValue: number,
aFromCurrency: Currency,
aToCurrency: Currency
aFromCurrency: string,
aToCurrency: string
) {
if (isNaN(this.exchangeRates[`${Currency.USD}${Currency.CHF}`])) {
const hasNaN = Object.values(this.exchangeRates).some((exchangeRate) => {
return isNaN(exchangeRate);
});
if (hasNaN) {
// Reinitialize if data is not loaded correctly
this.initialize();
}
@ -104,8 +126,8 @@ export class ExchangeRateDataService {
factor = this.exchangeRates[`${aFromCurrency}${aToCurrency}`];
} else {
// Calculate indirectly via USD
const factor1 = this.exchangeRates[`${aFromCurrency}${Currency.USD}`];
const factor2 = this.exchangeRates[`${Currency.USD}${aToCurrency}`];
const factor1 = this.exchangeRates[`${aFromCurrency}${'USD'}`];
const factor2 = this.exchangeRates[`${'USD'}${aToCurrency}`];
factor = factor1 * factor2;
@ -113,7 +135,7 @@ export class ExchangeRateDataService {
}
}
if (isNumber(factor)) {
if (isNumber(factor) && !isNaN(factor)) {
return factor * aValue;
}
@ -129,8 +151,8 @@ export class ExchangeRateDataService {
currency2,
dataSource
}: {
currency1: Currency;
currency2: Currency;
currency1: string;
currency2: string;
dataSource: DataSource;
}) {
this.currencyPairs.push({
@ -142,4 +164,55 @@ export class ExchangeRateDataService {
symbol: `${currency2}${currency1}`
});
}
private async prepareCurrencies(): Promise<string[]> {
const currencies: string[] = [];
(
await this.prismaService.account.findMany({
distinct: ['currency'],
orderBy: [{ currency: 'asc' }],
select: { currency: true }
})
).forEach((account) => {
currencies.push(account.currency);
});
(
await this.prismaService.settings.findMany({
distinct: ['currency'],
orderBy: [{ currency: 'asc' }],
select: { currency: true }
})
).forEach((userSettings) => {
currencies.push(userSettings.currency);
});
(
await this.prismaService.symbolProfile.findMany({
distinct: ['currency'],
orderBy: [{ currency: 'asc' }],
select: { currency: true }
})
).forEach((symbolProfile) => {
currencies.push(symbolProfile.currency);
});
return uniq(currencies).sort();
}
private prepareCurrencyPairs(aCurrencies: string[]) {
return aCurrencies
.filter((currency) => {
return currency !== baseCurrency;
})
.map((currency) => {
return {
currency1: baseCurrency,
currency2: currency,
dataSource: DataSource.YAHOO,
symbol: `${baseCurrency}${currency}`
};
});
}
}

View File

@ -2,7 +2,6 @@ import {
Account,
AssetClass,
AssetSubClass,
Currency,
DataSource,
SymbolProfile
} from '@prisma/client';
@ -17,7 +16,7 @@ export const MarketState = {
export interface IOrder {
account: Account;
currency: Currency;
currency: string;
date: string;
fee: number;
id?: string;
@ -38,7 +37,7 @@ export interface IDataProviderResponse {
assetClass?: AssetClass;
assetSubClass?: AssetSubClass;
countries?: { code: string; weight: number }[];
currency: Currency;
currency: string;
dataSource: DataSource;
exchange?: string;
marketChange?: number;
@ -46,6 +45,7 @@ export interface IDataProviderResponse {
marketPrice: number;
marketState: MarketState;
name?: string;
sectors?: { name: string; weight: number }[];
url?: string;
}

View File

@ -1,17 +1,12 @@
import { Country } from '@ghostfolio/common/interfaces/country.interface';
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
import {
AssetClass,
AssetSubClass,
Currency,
DataSource
} from '@prisma/client';
import { AssetClass, AssetSubClass, DataSource } from '@prisma/client';
export interface EnhancedSymbolProfile {
assetClass: AssetClass;
assetSubClass: AssetSubClass;
createdAt: Date;
currency: Currency | null;
currency: string | null;
dataSource: DataSource;
id: string;
name: string | null;

View File

@ -28,7 +28,10 @@
<router-outlet></router-outlet>
</main>
<footer *ngIf="!user" class="footer d-flex justify-content-center w-100">
<footer
*ngIf="currentRoute === 'start'"
class="footer d-flex justify-content-center w-100"
>
<div class="container text-center">
<div>
© {{ currentYear }} <a href="https://ghostfol.io">Ghostfolio</a>

View File

@ -51,7 +51,7 @@
<ng-container matColumnDef="balance">
<th *matHeaderCellDef class="px-1 text-right" i18n mat-header-cell>
Balance
Cash Balance
</th>
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
<gf-value

View File

@ -17,7 +17,6 @@
>Overview</a
>
<a
*ngIf="user?.settings?.viewMode === 'DEFAULT'"
class="d-none d-sm-block mx-1"
i18n
mat-flat-button
@ -28,18 +27,6 @@
[routerLink]="['/portfolio']"
>Portfolio</a
>
<a
*ngIf="user?.settings?.viewMode !== 'DEFAULT'"
class="d-none d-sm-block mx-1"
i18n
mat-flat-button
[ngClass]="{
'font-weight-bold': currentRoute === 'portfolio',
'text-decoration-underline': currentRoute === 'portfolio'
}"
[routerLink]="['/portfolio', 'transactions']"
>Transactions</a
>
<a
class="d-none d-sm-block mx-1"
i18n
@ -166,7 +153,6 @@
>Overview</a
>
<a
*ngIf="user?.settings?.viewMode === 'DEFAULT'"
class="d-block d-sm-none"
i18n
mat-menu-item
@ -176,17 +162,6 @@
[routerLink]="['/portfolio']"
>Portfolio</a
>
<a
*ngIf="user?.settings?.viewMode !== 'DEFAULT'"
class="d-block d-sm-none"
i18n
mat-menu-item
[ngClass]="{
'font-weight-bold': currentRoute === 'portfolio'
}"
[routerLink]="['/portfolio', 'transactions']"
>Transactions</a
>
<a
class="d-block d-sm-none"
i18n

View File

@ -1,4 +1,4 @@
import { LineChartItem } from '../../line-chart/interfaces/line-chart.interface';
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
export interface PositionDetailDialogParams {
deviceType: string;

View File

@ -7,11 +7,11 @@ import {
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { DataService } from '@ghostfolio/client/services/data.service';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
import { isToday, parse } from 'date-fns';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { LineChartItem } from '../line-chart/interfaces/line-chart.interface';
import { PositionDetailDialogParams } from './interfaces/interfaces';
@Component({

View File

@ -13,6 +13,7 @@
[benchmarkDataItems]="benchmarkDataItems"
[benchmarkLabel]="benchmarkLabel"
[historicalDataItems]="historicalDataItems"
[showGradient]="true"
[showLegend]="true"
[showXAxis]="true"
[showYAxis]="false"

View File

@ -2,7 +2,7 @@ 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 { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
import { GfValueModule } from '@ghostfolio/ui/value';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';

View File

@ -8,7 +8,6 @@ import {
ViewChild
} from '@angular/core';
import { PortfolioPerformance } from '@ghostfolio/common/interfaces';
import { Currency } from '@prisma/client';
import { CountUp } from 'countup.js';
import { isNumber } from 'lodash';
@ -19,7 +18,7 @@ import { isNumber } from 'lodash';
styleUrls: ['./portfolio-performance.component.scss']
})
export class PortfolioPerformanceComponent implements OnChanges, OnInit {
@Input() baseCurrency: Currency;
@Input() baseCurrency: string;
@Input() isLoading: boolean;
@Input() locale: string;
@Input() performance: PortfolioPerformance;

View File

@ -6,7 +6,6 @@ import {
OnInit
} from '@angular/core';
import { PortfolioSummary } from '@ghostfolio/common/interfaces';
import { Currency } from '@prisma/client';
import { formatDistanceToNow } from 'date-fns';
@Component({
@ -16,7 +15,7 @@ import { formatDistanceToNow } from 'date-fns';
styleUrls: ['./portfolio-summary.component.scss']
})
export class PortfolioSummaryComponent implements OnChanges, OnInit {
@Input() baseCurrency: Currency;
@Input() baseCurrency: string;
@Input() isLoading: boolean;
@Input() locale: string;
@Input() summary: PortfolioSummary;

View File

@ -8,11 +8,11 @@ import {
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { DataService } from '@ghostfolio/client/services/data.service';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
import { format, isSameMonth, isToday, parseISO } from 'date-fns';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { LineChartItem } from '../../line-chart/interfaces/line-chart.interface';
import { PositionDetailDialogParams } from './interfaces/interfaces';
@Component({

View File

@ -12,6 +12,7 @@
benchmarkLabel="Buy Price"
[benchmarkDataItems]="benchmarkDataItems"
[historicalDataItems]="historicalDataItems"
[showGradient]="true"
[showXAxis]="true"
[showYAxis]="true"
[symbol]="data.symbol"

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 { MatDialogModule } from '@angular/material/dialog';
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
import { GfValueModule } from '@ghostfolio/ui/value';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';

View File

@ -83,10 +83,10 @@
*matRowDef="let row; columns: displayedColumns"
mat-row
[ngClass]="{
'cursor-pointer': !this.ignoreAssetClasses.includes(row.assetClass)
'cursor-pointer': !ignoreAssetSubClasses.includes(row.assetSubClass)
}"
(click)="
!this.ignoreAssetClasses.includes(row.assetClass) &&
!ignoreAssetSubClasses.includes(row.assetSubClass) &&
onOpenPositionDialog({ symbol: row.symbol })
"
></tr>

View File

@ -42,7 +42,7 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
public dataSource: MatTableDataSource<PortfolioPosition> =
new MatTableDataSource();
public displayedColumns = [];
public ignoreAssetClasses = [AssetClass.CASH.toString()];
public ignoreAssetSubClasses = [AssetClass.CASH.toString()];
public isLoading = true;
public pageSize = 7;
public routeQueryParams: Subscription;

View File

@ -7,7 +7,6 @@ import {
OnDestroy,
OnInit
} from '@angular/core';
import { Currency } from '@prisma/client';
import svgMap from 'svgmap';
@Component({
@ -17,8 +16,9 @@ import svgMap from 'svgmap';
styleUrls: ['./world-map-chart.component.scss']
})
export class WorldMapChartComponent implements OnChanges, OnDestroy, OnInit {
@Input() baseCurrency: Currency;
@Input() baseCurrency: string;
@Input() countries: { [code: string]: { name: string; value: number } };
@Input() isInPercent = false;
public isLoading = true;
public svgMapElement;
@ -42,6 +42,27 @@ export class WorldMapChartComponent implements OnChanges, OnDestroy, OnInit {
}
private initialize() {
if (this.isInPercent) {
// Convert value of countries to percentage
let sum = 0;
Object.keys(this.countries).map((country) => {
sum += this.countries[country].value;
});
Object.keys(this.countries).map((country) => {
this.countries[country].value = Number(
((this.countries[country].value * 100) / sum).toFixed(2)
);
});
} else {
// Convert value to fixed-point notation
Object.keys(this.countries).map((country) => {
this.countries[country].value = Number(
this.countries[country].value.toFixed(2)
);
});
}
this.svgMapElement = new svgMap({
colorMax: '#22bdb9',
colorMin: '#c3f1f0',
@ -50,7 +71,7 @@ export class WorldMapChartComponent implements OnChanges, OnDestroy, OnInit {
applyData: 'value',
data: {
value: {
format: `{0} ${this.baseCurrency}`
format: this.isInPercent ? `{0}%` : `{0} ${this.baseCurrency}`
}
},
values: this.countries

View File

@ -61,7 +61,23 @@ export class HttpResponseInterceptor implements HttpInterceptor {
return event;
}),
catchError((error: HttpErrorResponse) => {
if (error.status === StatusCodes.INTERNAL_SERVER_ERROR) {
if (error.status === StatusCodes.FORBIDDEN) {
if (!this.snackBarRef) {
this.snackBarRef = this.snackBar.open(
'This feature requires a subscription.',
'Upgrade Plan',
{ duration: 6000 }
);
this.snackBarRef.afterDismissed().subscribe(() => {
this.snackBarRef = undefined;
});
this.snackBarRef.onAction().subscribe(() => {
this.router.navigate(['/pricing']);
});
}
} else if (error.status === StatusCodes.INTERNAL_SERVER_ERROR) {
if (!this.snackBarRef) {
this.snackBarRef = this.snackBar.open(
'Oops! Something went wrong. Please try again later.',
@ -85,7 +101,7 @@ export class HttpResponseInterceptor implements HttpInterceptor {
}
}
return throwError('');
return throwError(error);
})
);
}

View File

@ -11,9 +11,10 @@ import { takeUntil } from 'rxjs/operators';
import { environment } from '../../../environments/environment';
@Component({
host: { class: 'mb-5' },
selector: 'gf-about-page',
templateUrl: './about-page.html',
styleUrls: ['./about-page.scss']
styleUrls: ['./about-page.scss'],
templateUrl: './about-page.html'
})
export class AboutPageComponent implements OnDestroy, OnInit {
public baseCurrency = baseCurrency;

View File

@ -15,15 +15,15 @@ import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
import { DEFAULT_DATE_FORMAT, baseCurrency } from '@ghostfolio/common/config';
import { Access, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { Currency } from '@prisma/client';
import { StripeService } from 'ngx-stripe';
import { EMPTY, Subject } from 'rxjs';
import { catchError, switchMap, takeUntil } from 'rxjs/operators';
@Component({
host: { class: 'mb-5' },
selector: 'gf-account-page',
templateUrl: './account-page.html',
styleUrls: ['./account-page.scss']
styleUrls: ['./account-page.scss'],
templateUrl: './account-page.html'
})
export class AccountPageComponent implements OnDestroy, OnInit {
@ViewChild('toggleSignInWithFingerprintEnabledElement')
@ -33,7 +33,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
public baseCurrency = baseCurrency;
public coupon: number;
public couponId: string;
public currencies: Currency[] = [];
public currencies: string[] = [];
public defaultDateFormat = DEFAULT_DATE_FORMAT;
public hasPermissionForSubscription: boolean;
public hasPermissionToUpdateViewMode: boolean;

View File

@ -16,9 +16,10 @@ import { takeUntil } from 'rxjs/operators';
import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog/create-or-update-account-dialog.component';
@Component({
host: { class: 'mb-5' },
selector: 'gf-accounts-page',
templateUrl: './accounts-page.html',
styleUrls: ['./accounts-page.scss']
styleUrls: ['./accounts-page.scss'],
templateUrl: './accounts-page.html'
})
export class AccountsPageComponent implements OnDestroy, OnInit {
public accounts: AccountModel[];

View File

@ -7,7 +7,7 @@
[baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType"
[locale]="user?.settings?.locale"
[showActions]="!hasImpersonationId && hasPermissionToDeleteAccount"
[showActions]="!hasImpersonationId && hasPermissionToDeleteAccount && !user.settings.isRestrictedView"
(accountDeleted)="onDeleteAccount($event)"
(accountToUpdate)="onUpdateAccount($event)"
></gf-accounts-table>
@ -15,7 +15,7 @@
</div>
<div
*ngIf="!hasImpersonationId && hasPermissionToCreateAccount"
*ngIf="!hasImpersonationId && hasPermissionToCreateAccount && !user.settings.isRestrictedView"
class="fab-container"
>
<a

View File

@ -1,4 +1,6 @@
:host {
display: block;
.fab-container {
position: fixed;
right: 2rem;

View File

@ -1,12 +1,10 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
Inject,
OnDestroy
} from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Currency } from '@prisma/client';
import { Subject } from 'rxjs';
import { DataService } from '../../../services/data.service';
@ -20,13 +18,12 @@ import { CreateOrUpdateAccountDialogParams } from './interfaces/interfaces';
templateUrl: 'create-or-update-account-dialog.html'
})
export class CreateOrUpdateAccountDialog implements OnDestroy {
public currencies: Currency[] = [];
public currencies: string[] = [];
public platforms: { id: string; name: string }[];
private unsubscribeSubject = new Subject<void>();
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
public dialogRef: MatDialogRef<CreateOrUpdateAccountDialog>,
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateAccountDialogParams

View File

@ -15,9 +15,10 @@ import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
host: { class: 'mb-5' },
selector: 'gf-admin-page',
templateUrl: './admin-page.html',
styleUrls: ['./admin-page.scss']
styleUrls: ['./admin-page.scss'],
templateUrl: './admin-page.html'
})
export class AdminPageComponent implements OnDestroy, OnInit {
public dataGatheringInProgress: boolean;

View File

@ -6,13 +6,32 @@
</h3>
<mat-card class="mb-3">
<mat-card-content>
<div class="d-flex my-3">
<div
*ngIf="exchangeRates?.length > 0"
class="align-items-start d-flex my-3"
>
<div class="w-50" i18n>Exchange Rates</div>
<div class="w-50">
<div *ngFor="let exchangeRate of exchangeRates" class="mb-1">
1 {{ exchangeRate.label1 }} = {{ exchangeRate.value | number :
'1.5-5' }} {{ exchangeRate.label2 }}
</div>
<table>
<tr *ngFor="let exchangeRate of exchangeRates">
<td class="d-flex">
<gf-value
[locale]="user?.settings?.locale"
[value]="1"
></gf-value>
</td>
<td class="pl-1">{{ exchangeRate.label1 }}</td>
<td class="px-1">=</td>
<td class="d-flex justify-content-end">
<gf-value
[locale]="user?.settings?.locale"
[precision]="4"
[value]="exchangeRate.value"
></gf-value>
</td>
<td class="pl-1">{{ exchangeRate.label2 }}</td>
</tr>
</table>
</div>
</div>
<div class="d-flex my-3">

View File

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatMenuModule } from '@angular/material/menu';
import { CacheService } from '@ghostfolio/client/services/cache.service';
import { GfValueModule } from '@ghostfolio/ui/value';
import { AdminPageRoutingModule } from './admin-page-routing.module';
import { AdminPageComponent } from './admin-page.component';
@ -14,6 +15,7 @@ import { AdminPageComponent } from './admin-page.component';
imports: [
AdminPageRoutingModule,
CommonModule,
GfValueModule,
MatButtonModule,
MatCardModule,
MatMenuModule

View File

@ -1,7 +1,9 @@
import { Component } from '@angular/core';
@Component({
host: { class: 'mb-5' },
selector: 'gf-hallo-ghostfolio-page',
styleUrls: ['./hallo-ghostfolio-page.scss'],
templateUrl: './hallo-ghostfolio-page.html'
})
export class HalloGhostfolioPageComponent {}

View File

@ -139,7 +139,7 @@
Thomas von Ghostfolio
</p>
</section>
<section class="my-5">
<section class="mb-4">
<ul class="list-inline">
<li class="h5">
<span class="badge badge-light font-weight-normal mr-2"

View File

@ -1,7 +1,9 @@
import { Component } from '@angular/core';
@Component({
host: { class: 'mb-5' },
selector: 'gf-hello-ghostfolio-page',
styleUrls: ['./hello-ghostfolio-page.scss'],
templateUrl: './hello-ghostfolio-page.html'
})
export class HelloGhostfolioPageComponent {}

View File

@ -134,7 +134,7 @@
Thomas from Ghostfolio
</p>
</section>
<section class="my-5">
<section class="mb-4">
<ul class="list-inline">
<li class="h5">
<span class="badge badge-light font-weight-normal mr-2"

View File

@ -0,0 +1,3 @@
:host {
display: block;
}

View File

@ -10,7 +10,6 @@ import {
import { MatDialog } from '@angular/material/dialog';
import { MatTabChangeEvent } from '@angular/material/tabs';
import { ActivatedRoute, Router } from '@angular/router';
import { LineChartItem } from '@ghostfolio/client/components/line-chart/interfaces/line-chart.interface';
import { PerformanceChartDialog } from '@ghostfolio/client/components/performance-chart-dialog/performance-chart-dialog.component';
import { ToggleOption } from '@ghostfolio/client/components/toggle/interfaces/toggle-option.type';
import { DataService } from '@ghostfolio/client/services/data.service';
@ -29,6 +28,7 @@ import {
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { DateRange } from '@ghostfolio/common/types';
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
import { DataSource } from '@prisma/client';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject, Subscription } from 'rxjs';
@ -36,8 +36,8 @@ import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'gf-home-page',
templateUrl: './home-page.html',
styleUrls: ['./home-page.scss']
styleUrls: ['./home-page.scss'],
templateUrl: './home-page.html'
})
export class HomePageComponent implements OnDestroy, OnInit {
@HostBinding('class.with-create-account-container') get isDemo() {

View File

@ -33,6 +33,7 @@
class="mr-3"
symbol="Performance"
[historicalDataItems]="historicalDataItems"
[showGradient]="true"
[showLoader]="false"
[showXAxis]="false"
[showYAxis]="false"

View File

@ -4,12 +4,12 @@ import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatTabsModule } from '@angular/material/tabs';
import { RouterModule } from '@angular/router';
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
import { GfPerformanceChartDialogModule } from '@ghostfolio/client/components/performance-chart-dialog/performance-chart-dialog.module';
import { GfPortfolioPerformanceModule } from '@ghostfolio/client/components/portfolio-performance/portfolio-performance.module';
import { GfPortfolioSummaryModule } from '@ghostfolio/client/components/portfolio-summary/portfolio-summary.module';
import { GfPositionsModule } from '@ghostfolio/client/components/positions/positions.module';
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
import { HomePageRoutingModule } from './home-page-routing.module';

View File

@ -1,15 +1,16 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { LineChartItem } from '@ghostfolio/client/components/line-chart/interfaces/line-chart.interface';
import { DataService } from '@ghostfolio/client/services/data.service';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
import { format } from 'date-fns';
import { Subject } from 'rxjs';
@Component({
host: { class: 'mb-5' },
selector: 'gf-landing-page',
templateUrl: './landing-page.html',
styleUrls: ['./landing-page.scss']
styleUrls: ['./landing-page.scss'],
templateUrl: './landing-page.html'
})
export class LandingPageComponent implements OnDestroy, OnInit {
public currentYear = format(new Date(), 'yyyy');

View File

@ -50,6 +50,7 @@
class="position-absolute"
symbol="Performance"
[historicalDataItems]="historicalDataItems"
[showGradient]="true"
[showLoader]="false"
[showXAxis]="false"
[showYAxis]="false"

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 { RouterModule } from '@angular/router';
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
import { GfLogoModule } from '@ghostfolio/ui/logo';
import { LandingPageRoutingModule } from './landing-page-routing.module';

View File

@ -15,9 +15,10 @@ import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
host: { class: 'mb-5' },
selector: 'gf-allocations-page',
templateUrl: './allocations-page.html',
styleUrls: ['./allocations-page.scss']
styleUrls: ['./allocations-page.scss'],
templateUrl: './allocations-page.html'
})
export class AllocationsPageComponent implements OnDestroy, OnInit {
public accounts: {
@ -37,13 +38,23 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
{ label: 'Current', value: 'current' }
];
public portfolioDetails: PortfolioDetails;
public positions: { [symbol: string]: any };
public positions: {
[symbol: string]: Pick<
PortfolioPosition,
| 'assetClass'
| 'assetSubClass'
| 'currency'
| 'exchange'
| 'name'
| 'value'
>;
};
public positionsArray: PortfolioPosition[];
public sectors: {
[name: string]: { name: string; value: number };
};
public symbols: {
[name: string]: { name: string; value: number };
[name: string]: { name: string; symbol: string; value: number };
};
public user: User;
@ -121,6 +132,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
this.symbols = {
[UNKNOWN_KEY]: {
name: UNKNOWN_KEY,
symbol: UNKNOWN_KEY,
value: 0
}
};
@ -137,15 +149,29 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
for (const [symbol, position] of Object.entries(
this.portfolioDetails.holdings
)) {
let value = 0;
if (aPeriod === 'original') {
if (this.hasImpersonationId) {
value = position.allocationInvestment;
} else {
value = position.investment;
}
} else {
if (this.hasImpersonationId) {
value = position.allocationCurrent;
} else {
value = position.value;
}
}
this.positions[symbol] = {
value,
assetClass: position.assetClass,
assetSubClass: position.assetSubClass,
currency: position.currency,
exchange: position.exchange,
value:
aPeriod === 'original'
? position.allocationInvestment
: position.allocationCurrent
name: position.name
};
this.positionsArray.push(position);
@ -221,7 +247,8 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
if (position.assetClass === AssetClass.EQUITY) {
this.symbols[symbol] = {
name: symbol,
symbol,
name: position.name,
value: aPeriod === 'original' ? position.investment : position.value
};
}

View File

@ -19,7 +19,7 @@
<mat-card-content>
<gf-portfolio-proportion-chart
[baseCurrency]="user?.settings?.baseCurrency"
[isInPercent]="hasImpersonationId"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[keys]="['name']"
[locale]="user?.settings?.locale"
[positions]="accounts"
@ -43,7 +43,7 @@
<mat-card-content>
<gf-portfolio-proportion-chart
[baseCurrency]="user?.settings?.baseCurrency"
[isInPercent]="true"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[keys]="['assetClass', 'assetSubClass']"
[locale]="user?.settings?.locale"
[positions]="positions"
@ -67,7 +67,7 @@
<mat-card-content>
<gf-portfolio-proportion-chart
[baseCurrency]="user?.settings?.baseCurrency"
[isInPercent]="true"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[keys]="['currency']"
[locale]="user?.settings?.locale"
[positions]="positions"
@ -90,8 +90,8 @@
<gf-portfolio-proportion-chart
class="mx-auto"
[baseCurrency]="user?.settings?.baseCurrency"
[isInPercent]="false"
[keys]="['name']"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[keys]="['symbol']"
[locale]="user?.settings?.locale"
[positions]="symbols"
[showLabels]="deviceType !== 'mobile'"
@ -113,7 +113,7 @@
<mat-card-content>
<gf-portfolio-proportion-chart
[baseCurrency]="user?.settings?.baseCurrency"
[isInPercent]="false"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[keys]="['name']"
[locale]="user?.settings?.locale"
[maxItems]="10"
@ -138,7 +138,7 @@
<mat-card-content>
<gf-portfolio-proportion-chart
[baseCurrency]="user?.settings?.baseCurrency"
[isInPercent]="false"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[keys]="['name']"
[locale]="user?.settings?.locale"
[positions]="continents"
@ -161,7 +161,7 @@
<gf-portfolio-proportion-chart
[keys]="['name']"
[baseCurrency]="user?.settings?.baseCurrency"
[isInPercent]="false"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[locale]="user?.settings?.locale"
[maxItems]="10"
[positions]="countries"
@ -186,6 +186,7 @@
<gf-world-map-chart
[baseCurrency]="user?.settings?.baseCurrency"
[countries]="countries"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
></gf-world-map-chart>
</mat-card-content>
</mat-card>

View File

@ -1,10 +1,10 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { GfPortfolioProportionChartModule } from '@ghostfolio/client/components/portfolio-proportion-chart/portfolio-proportion-chart.module';
import { GfPositionsTableModule } from '@ghostfolio/client/components/positions-table/positions-table.module';
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module';
import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module';
import { AllocationsPageRoutingModule } from './allocations-page-routing.module';
import { AllocationsPageComponent } from './allocations-page.component';

View File

@ -1,4 +1,6 @@
:host {
display: block;
.allocations-by-symbol {
gf-portfolio-proportion-chart {
max-width: 80vh;

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