Compare commits
32 Commits
Author | SHA1 | Date | |
---|---|---|---|
060846023f | |||
f06a0fbbee | |||
4ab6a1a071 | |||
93dcbeb6c7 | |||
b9f0a57522 | |||
174c1d1a62 | |||
f308ae7a13 | |||
a7a6b0608b | |||
15a61b7a20 | |||
d1eedf9726 | |||
30a592b524 | |||
de94494aa0 | |||
d3c6788ad5 | |||
3ec4a73b35 | |||
1050bfa098 | |||
595ec1d7b4 | |||
c8389599b6 | |||
8769fe4c90 | |||
4219e1121e | |||
f558eb8de8 | |||
fe2bd6eea8 | |||
035052be99 | |||
bcdd2780b3 | |||
22d1ed7920 | |||
39d9828f9f | |||
6333aa972d | |||
554f2f861f | |||
dcee651098 | |||
508a48f4c3 | |||
8466e3d73f | |||
9ae9904389 | |||
af022ae316 |
81
CHANGELOG.md
81
CHANGELOG.md
@ -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
|
||||
|
@ -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?
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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(),
|
||||
|
16
apps/api/src/app/cache/cache.module.ts
vendored
16
apps/api/src/app/cache/cache.module.ts
vendored
@ -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 {}
|
||||
|
@ -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;
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { Currency } from '@prisma/client';
|
||||
|
||||
export interface Data {
|
||||
currency: Currency;
|
||||
currency: string;
|
||||
value: number;
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
|
||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||
import { 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 {}
|
||||
|
@ -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[];
|
||||
}
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {}
|
||||
|
@ -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(),
|
||||
|
@ -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()
|
||||
|
@ -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;
|
||||
|
@ -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([
|
||||
{
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,6 @@
|
||||
import { Currency } from '@prisma/client';
|
||||
|
||||
export interface GetValueParams {
|
||||
currency: Currency;
|
||||
currency: string;
|
||||
date: Date;
|
||||
symbol: string;
|
||||
userCurrency: Currency;
|
||||
userCurrency: string;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -1,8 +1,6 @@
|
||||
import { Currency } from '@prisma/client';
|
||||
|
||||
export interface PortfolioPositionDetail {
|
||||
averagePrice: number;
|
||||
currency: Currency;
|
||||
currency: string;
|
||||
firstBuyDate: string;
|
||||
grossPerformance: number;
|
||||
grossPerformancePercent: number;
|
||||
|
@ -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;
|
||||
|
@ -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),
|
||||
|
@ -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) {
|
||||
|
@ -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))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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) => {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { Currency } from '@prisma/client';
|
||||
|
||||
export interface UserSettings {
|
||||
baseCurrency: Currency;
|
||||
baseCurrency: string;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Account, Currency, SymbolProfile } from '@prisma/client';
|
||||
import { Account, SymbolProfile } from '@prisma/client';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { 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;
|
||||
|
@ -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]) => ({
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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]
|
||||
})
|
||||
|
@ -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),
|
||||
|
@ -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()
|
||||
|
@ -0,0 +1,73 @@
|
||||
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
|
||||
import { IDataProviderResponse } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import bent from 'bent';
|
||||
|
||||
const getJSON = bent('json');
|
||||
|
||||
export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
||||
private static baseUrl = 'https://data.trackinsight.com/holdings';
|
||||
private static countries = require('countries-list/dist/countries.json');
|
||||
private static sectorsMapping = {
|
||||
'Consumer Discretionary': 'Consumer Cyclical',
|
||||
'Consumer Defensive': 'Consumer Staples',
|
||||
'Health Care': 'Healthcare',
|
||||
'Information Technology': 'Technology'
|
||||
};
|
||||
|
||||
public async enhance({
|
||||
response,
|
||||
symbol
|
||||
}: {
|
||||
response: IDataProviderResponse;
|
||||
symbol: string;
|
||||
}): Promise<IDataProviderResponse> {
|
||||
if (
|
||||
!(response.assetClass === 'EQUITY' && response.assetSubClass === 'ETF')
|
||||
) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const holdings = await getJSON(
|
||||
`${TrackinsightDataEnhancerService.baseUrl}/${symbol}.json`
|
||||
).catch(() => {
|
||||
return getJSON(
|
||||
`${TrackinsightDataEnhancerService.baseUrl}/${
|
||||
symbol.split('.')[0]
|
||||
}.json`
|
||||
);
|
||||
});
|
||||
|
||||
if (!response.countries || response.countries.length === 0) {
|
||||
response.countries = [];
|
||||
for (const [name, value] of Object.entries<any>(holdings.countries)) {
|
||||
let countryCode: string;
|
||||
|
||||
for (const [key, country] of Object.entries<any>(
|
||||
TrackinsightDataEnhancerService.countries
|
||||
)) {
|
||||
if (country.name === name) {
|
||||
countryCode = key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
response.countries.push({
|
||||
code: countryCode,
|
||||
weight: value.weight
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.sectors || response.sectors.length === 0) {
|
||||
response.sectors = [];
|
||||
for (const [name, value] of Object.entries<any>(holdings.sectors)) {
|
||||
response.sectors.push({
|
||||
name: TrackinsightDataEnhancerService.sectorsMapping[name] ?? name,
|
||||
weight: value.weight
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve(response);
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||
import { 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]
|
||||
})
|
||||
|
@ -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);
|
||||
|
@ -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()
|
||||
|
@ -1,7 +1,5 @@
|
||||
import { Currency } from '@prisma/client';
|
||||
|
||||
export interface ScraperConfig {
|
||||
currency: Currency;
|
||||
currency: string;
|
||||
selector: string;
|
||||
symbol: string;
|
||||
url: string;
|
||||
|
@ -0,0 +1,11 @@
|
||||
import { IDataProviderResponse } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
|
||||
export interface DataEnhancerInterface {
|
||||
enhance({
|
||||
response,
|
||||
symbol
|
||||
}: {
|
||||
response: IDataProviderResponse;
|
||||
symbol: string;
|
||||
}): Promise<IDataProviderResponse>;
|
||||
}
|
@ -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;
|
@ -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 {
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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]
|
||||
})
|
||||
|
@ -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}`
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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({
|
||||
|
@ -13,6 +13,7 @@
|
||||
[benchmarkDataItems]="benchmarkDataItems"
|
||||
[benchmarkLabel]="benchmarkLabel"
|
||||
[historicalDataItems]="historicalDataItems"
|
||||
[showGradient]="true"
|
||||
[showLegend]="true"
|
||||
[showXAxis]="true"
|
||||
[showYAxis]="false"
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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({
|
||||
|
@ -12,6 +12,7 @@
|
||||
benchmarkLabel="Buy Price"
|
||||
[benchmarkDataItems]="benchmarkDataItems"
|
||||
[historicalDataItems]="historicalDataItems"
|
||||
[showGradient]="true"
|
||||
[showXAxis]="true"
|
||||
[showYAxis]="true"
|
||||
[symbol]="data.symbol"
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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[];
|
||||
|
@ -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
|
||||
|
@ -1,4 +1,6 @@
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
.fab-container {
|
||||
position: fixed;
|
||||
right: 2rem;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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">
|
||||
|
@ -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
|
||||
|
@ -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 {}
|
||||
|
@ -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"
|
||||
|
@ -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 {}
|
||||
|
@ -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"
|
||||
|
@ -0,0 +1,3 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
@ -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() {
|
||||
|
@ -33,6 +33,7 @@
|
||||
class="mr-3"
|
||||
symbol="Performance"
|
||||
[historicalDataItems]="historicalDataItems"
|
||||
[showGradient]="true"
|
||||
[showLoader]="false"
|
||||
[showXAxis]="false"
|
||||
[showYAxis]="false"
|
||||
|
@ -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';
|
||||
|
@ -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');
|
||||
|
@ -50,6 +50,7 @@
|
||||
class="position-absolute"
|
||||
symbol="Performance"
|
||||
[historicalDataItems]="historicalDataItems"
|
||||
[showGradient]="true"
|
||||
[showLoader]="false"
|
||||
[showXAxis]="false"
|
||||
[showYAxis]="false"
|
||||
|
@ -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';
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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';
|
||||
|
@ -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
Reference in New Issue
Block a user