Compare commits

...

8 Commits

Author SHA1 Message Date
f46533107d Release 1.123.0 (#739) 2022-03-05 11:09:07 +01:00
c216ab1d76 Eliminate data source from order model (#730)
* Eliminate currency, data source and symbol from order model

* Remove prefix for symbols with data source GHOSTFOLIO

* Update changelog
2022-03-05 11:07:27 +01:00
86acbf06f4 Feature/add data provider errors to api response (#738)
* Add data provider error details

* Update changelog
2022-03-05 11:00:02 +01:00
3de7d3f60e Bugfix/improve account calculations (#737)
* Improve account calculations

* Update changelog
2022-03-04 21:31:31 +01:00
63ed227f3f Release 1.122.0 (#732) 2022-03-01 21:36:09 +01:00
5bb20f6d5f Bugfix/fix undefined currencies after creating an activity (#731)
* Fix issue with undefined currencies after creating an activity

* Update changelog
2022-03-01 21:32:19 +01:00
b3e58d182a Feature/add support for click in portfolio proportion chart (#729)
* Add support for click

* Update changelog
2022-02-28 21:35:52 +01:00
93d6746739 Refactoring (#727) 2022-02-28 20:42:14 +01:00
58 changed files with 462 additions and 370 deletions

View File

@ -5,6 +5,35 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 1.123.0 - 05.03.2022
### Added
- Included data provider errors in the API response
### Changed
- Removed the redundant attributes (`currency`, `dataSource`, `symbol`) of the activity model
- Removed the prefix for symbols with the data source `GHOSTFOLIO`
### Fixed
- Improved the account calculations
### Todo
- Apply data migration (`yarn database:migrate`)
## 1.122.0 - 01.03.2022
### Added
- Added support for click in the portfolio proportion chart component
### Fixed
- Fixed an issue with undefined currencies after creating an activity
## 1.121.0 - 27.02.2022 ## 1.121.0 - 27.02.2022
### Added ### Added

View File

@ -101,16 +101,18 @@ export class AccountController {
) { ) {
accountsWithAggregations = { accountsWithAggregations = {
...nullifyValuesInObject(accountsWithAggregations, [ ...nullifyValuesInObject(accountsWithAggregations, [
'totalBalance', 'totalBalanceInBaseCurrency',
'totalValue' 'totalValueInBaseCurrency'
]), ]),
accounts: nullifyValuesInObjects(accountsWithAggregations.accounts, [ accounts: nullifyValuesInObjects(accountsWithAggregations.accounts, [
'balance', 'balance',
'balanceInBaseCurrency',
'convertedBalance', 'convertedBalance',
'fee', 'fee',
'quantity', 'quantity',
'unitPrice', 'unitPrice',
'value' 'value',
'valueInBaseCurrency'
]) ])
}; };
} }

View File

@ -2,6 +2,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Account, Order, Platform, Prisma } from '@prisma/client'; import { Account, Order, Platform, Prisma } from '@prisma/client';
import Big from 'big.js';
import { CashDetails } from './interfaces/cash-details.interface'; import { CashDetails } from './interfaces/cash-details.interface';
@ -105,21 +106,26 @@ export class AccountService {
aUserId: string, aUserId: string,
aCurrency: string aCurrency: string
): Promise<CashDetails> { ): Promise<CashDetails> {
let totalCashBalance = 0; let totalCashBalanceInBaseCurrency = new Big(0);
const accounts = await this.accounts({ const accounts = await this.accounts({
where: { userId: aUserId } where: { userId: aUserId }
}); });
accounts.forEach((account) => { for (const account of accounts) {
totalCashBalance += this.exchangeRateDataService.toCurrency( totalCashBalanceInBaseCurrency = totalCashBalanceInBaseCurrency.plus(
account.balance, this.exchangeRateDataService.toCurrency(
account.currency, account.balance,
aCurrency account.currency,
aCurrency
)
); );
}); }
return { accounts, balance: totalCashBalance }; return {
accounts,
balanceInBaseCurrency: totalCashBalanceInBaseCurrency.toNumber()
};
} }
public async updateAccount( public async updateAccount(

View File

@ -2,5 +2,5 @@ import { Account } from '@prisma/client';
export interface CashDetails { export interface CashDetails {
accounts: Account[]; accounts: Account[];
balance: number; balanceInBaseCurrency: number;
} }

View File

@ -11,7 +11,8 @@ import {
AdminData, AdminData,
AdminMarketData, AdminMarketData,
AdminMarketDataDetails, AdminMarketDataDetails,
AdminMarketDataItem AdminMarketDataItem,
UniqueAsset
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { DataSource, Property } from '@prisma/client'; import { DataSource, Property } from '@prisma/client';
@ -30,13 +31,7 @@ export class AdminService {
private readonly symbolProfileService: SymbolProfileService private readonly symbolProfileService: SymbolProfileService
) {} ) {}
public async deleteProfileData({ public async deleteProfileData({ dataSource, symbol }: UniqueAsset) {
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
await this.marketDataService.deleteMany({ dataSource, symbol }); await this.marketDataService.deleteMany({ dataSource, symbol });
await this.symbolProfileService.delete({ dataSource, symbol }); await this.symbolProfileService.delete({ dataSource, symbol });
} }
@ -137,10 +132,7 @@ export class AdminService {
public async getMarketDataBySymbol({ public async getMarketDataBySymbol({
dataSource, dataSource,
symbol symbol
}: { }: UniqueAsset): Promise<AdminMarketDataDetails> {
dataSource: DataSource;
symbol: string;
}): Promise<AdminMarketDataDetails> {
return { return {
marketData: await this.marketDataService.marketDataItems({ marketData: await this.marketDataService.marketDataItems({
orderBy: { orderBy: {

View File

@ -18,8 +18,6 @@ export class ExportService {
orderBy: { date: 'desc' }, orderBy: { date: 'desc' },
select: { select: {
accountId: true, accountId: true,
currency: true,
dataSource: true,
date: true, date: true,
fee: true, fee: true,
id: true, id: true,
@ -42,7 +40,6 @@ export class ExportService {
orders: orders.map( orders: orders.map(
({ ({
accountId, accountId,
currency,
date, date,
fee, fee,
quantity, quantity,
@ -52,12 +49,12 @@ export class ExportService {
}) => { }) => {
return { return {
accountId, accountId,
currency,
date, date,
fee, fee,
quantity, quantity,
type, type,
unitPrice, unitPrice,
currency: SymbolProfile.currency,
dataSource: SymbolProfile.dataSource, dataSource: SymbolProfile.dataSource,
symbol: type === 'ITEM' ? SymbolProfile.name : SymbolProfile.symbol symbol: type === 'ITEM' ? SymbolProfile.name : SymbolProfile.symbol
}; };

View File

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

View File

@ -3,8 +3,8 @@ import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Order } from '@prisma/client';
import { isSameDay, parseISO } from 'date-fns'; import { isSameDay, parseISO } from 'date-fns';
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
@Injectable() @Injectable()
export class ImportService { export class ImportService {
@ -19,7 +19,7 @@ export class ImportService {
orders, orders,
userId userId
}: { }: {
orders: Partial<Order>[]; orders: Partial<CreateOrderDto>[];
userId: string; userId: string;
}): Promise<void> { }): Promise<void> {
for (const order of orders) { for (const order of orders) {
@ -52,11 +52,8 @@ export class ImportService {
unitPrice unitPrice
} of orders) { } of orders) {
await this.orderService.createOrder({ await this.orderService.createOrder({
currency,
dataSource,
fee, fee,
quantity, quantity,
symbol,
type, type,
unitPrice, unitPrice,
userId, userId,
@ -65,6 +62,7 @@ export class ImportService {
SymbolProfile: { SymbolProfile: {
connectOrCreate: { connectOrCreate: {
create: { create: {
currency,
dataSource, dataSource,
symbol symbol
}, },
@ -85,7 +83,7 @@ export class ImportService {
orders, orders,
userId userId
}: { }: {
orders: Partial<Order>[]; orders: Partial<CreateOrderDto>[];
userId: string; userId: string;
}) { }) {
if ( if (
@ -99,6 +97,7 @@ export class ImportService {
} }
const existingOrders = await this.orderService.orders({ const existingOrders = await this.orderService.orders({
include: { SymbolProfile: true },
orderBy: { date: 'desc' }, orderBy: { date: 'desc' },
where: { userId } where: { userId }
}); });
@ -109,12 +108,12 @@ export class ImportService {
] of orders.entries()) { ] of orders.entries()) {
const duplicateOrder = existingOrders.find((order) => { const duplicateOrder = existingOrders.find((order) => {
return ( return (
order.currency === currency && order.SymbolProfile.currency === currency &&
order.dataSource === dataSource && order.SymbolProfile.dataSource === dataSource &&
isSameDay(order.date, parseISO(<string>(<unknown>date))) && isSameDay(order.date, parseISO(<string>(<unknown>date))) &&
order.fee === fee && order.fee === fee &&
order.quantity === quantity && order.quantity === quantity &&
order.symbol === symbol && order.SymbolProfile.symbol === symbol &&
order.type === type && order.type === type &&
order.unitPrice === unitPrice order.unitPrice === unitPrice
); );

View File

@ -114,6 +114,7 @@ export class OrderController {
SymbolProfile: { SymbolProfile: {
connectOrCreate: { connectOrCreate: {
create: { create: {
currency: data.currency,
dataSource: data.dataSource, dataSource: data.dataSource,
symbol: data.symbol symbol: data.symbol
}, },
@ -171,6 +172,14 @@ export class OrderController {
id_userId: { id: accountId, userId: this.request.user.id } id_userId: { id: accountId, userId: this.request.user.id }
} }
}, },
SymbolProfile: {
connect: {
dataSource_symbol: {
dataSource: data.dataSource,
symbol: data.symbol
}
}
},
User: { connect: { id: this.request.user.id } } User: { connect: { id: this.request.user.id } }
}, },
where: { where: {

View File

@ -53,7 +53,13 @@ export class OrderService {
} }
public async createOrder( public async createOrder(
data: Prisma.OrderCreateInput & { accountId?: string; userId: string } data: Prisma.OrderCreateInput & {
accountId?: string;
currency?: string;
dataSource?: DataSource;
symbol?: string;
userId: string;
}
): Promise<Order> { ): Promise<Order> {
const defaultAccount = ( const defaultAccount = (
await this.accountService.getAccounts(data.userId) await this.accountService.getAccounts(data.userId)
@ -71,15 +77,13 @@ export class OrderService {
}; };
if (data.type === 'ITEM') { if (data.type === 'ITEM') {
const currency = data.currency; const currency = data.SymbolProfile.connectOrCreate.create.currency;
const dataSource: DataSource = 'MANUAL'; const dataSource: DataSource = 'MANUAL';
const id = uuidv4(); const id = uuidv4();
const name = data.SymbolProfile.connectOrCreate.create.symbol; const name = data.SymbolProfile.connectOrCreate.create.symbol;
Account = undefined; Account = undefined;
data.dataSource = dataSource;
data.id = id; data.id = id;
data.symbol = null;
data.SymbolProfile.connectOrCreate.create.currency = currency; data.SymbolProfile.connectOrCreate.create.currency = currency;
data.SymbolProfile.connectOrCreate.create.dataSource = dataSource; data.SymbolProfile.connectOrCreate.create.dataSource = dataSource;
data.SymbolProfile.connectOrCreate.create.name = name; data.SymbolProfile.connectOrCreate.create.name = name;
@ -93,29 +97,32 @@ export class OrderService {
data.SymbolProfile.connectOrCreate.create.symbol.toUpperCase(); data.SymbolProfile.connectOrCreate.create.symbol.toUpperCase();
} }
await this.dataGatheringService.gatherProfileData([
{
dataSource: data.SymbolProfile.connectOrCreate.create.dataSource,
symbol: data.SymbolProfile.connectOrCreate.create.symbol
}
]);
const isDraft = isAfter(data.date as Date, endOfToday()); const isDraft = isAfter(data.date as Date, endOfToday());
if (!isDraft) { if (!isDraft) {
// Gather symbol data of order in the background, if not draft // Gather symbol data of order in the background, if not draft
this.dataGatheringService.gatherSymbols([ this.dataGatheringService.gatherSymbols([
{ {
dataSource: data.dataSource, dataSource: data.SymbolProfile.connectOrCreate.create.dataSource,
date: <Date>data.date, date: <Date>data.date,
symbol: data.SymbolProfile.connectOrCreate.create.symbol symbol: data.SymbolProfile.connectOrCreate.create.symbol
} }
]); ]);
} }
this.dataGatheringService.gatherProfileData([
{
dataSource: data.dataSource,
symbol: data.SymbolProfile.connectOrCreate.create.symbol
}
]);
await this.cacheService.flush(); await this.cacheService.flush();
delete data.accountId; delete data.accountId;
delete data.currency;
delete data.dataSource;
delete data.symbol;
delete data.userId; delete data.userId;
const orderData: Prisma.OrderCreateInput = data; const orderData: Prisma.OrderCreateInput = data;
@ -193,50 +200,60 @@ export class OrderService {
value, value,
feeInBaseCurrency: this.exchangeRateDataService.toCurrency( feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
order.fee, order.fee,
order.currency, order.SymbolProfile.currency,
userCurrency userCurrency
), ),
valueInBaseCurrency: this.exchangeRateDataService.toCurrency( valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
value, value,
order.currency, order.SymbolProfile.currency,
userCurrency userCurrency
) )
}; };
}); });
} }
public async updateOrder(params: { public async updateOrder({
data,
where
}: {
data: Prisma.OrderUpdateInput & {
currency?: string;
dataSource?: DataSource;
symbol?: string;
};
where: Prisma.OrderWhereUniqueInput; where: Prisma.OrderWhereUniqueInput;
data: Prisma.OrderUpdateInput;
}): Promise<Order> { }): Promise<Order> {
const { data, where } = params;
if (data.Account.connect.id_userId.id === null) { if (data.Account.connect.id_userId.id === null) {
delete data.Account; delete data.Account;
} }
let isDraft = false;
if (data.type === 'ITEM') { if (data.type === 'ITEM') {
const name = data.symbol; const name = data.SymbolProfile.connect.dataSource_symbol.symbol;
data.symbol = null;
data.SymbolProfile = { update: { name } }; data.SymbolProfile = { update: { name } };
} } else {
isDraft = isAfter(data.date as Date, endOfToday());
const isDraft = isAfter(data.date as Date, endOfToday()); if (!isDraft) {
// Gather symbol data of order in the background, if not draft
if (!isDraft) { this.dataGatheringService.gatherSymbols([
// Gather symbol data of order in the background, if not draft {
this.dataGatheringService.gatherSymbols([ dataSource: data.SymbolProfile.connect.dataSource_symbol.dataSource,
{ date: <Date>data.date,
dataSource: <DataSource>data.dataSource, symbol: data.SymbolProfile.connect.dataSource_symbol.symbol
date: <Date>data.date, }
symbol: <string>data.symbol ]);
} }
]);
} }
await this.cacheService.flush(); await this.cacheService.flush();
delete data.currency;
delete data.dataSource;
delete data.symbol;
return this.prismaService.order.update({ return this.prismaService.order.update({
data: { data: {
...data, ...data,

View File

@ -1,8 +1,7 @@
import { TimelinePosition } from '@ghostfolio/common/interfaces'; import { ResponseError, TimelinePosition } from '@ghostfolio/common/interfaces';
import Big from 'big.js'; import Big from 'big.js';
export interface CurrentPositions { export interface CurrentPositions extends ResponseError {
hasErrors: boolean;
positions: TimelinePosition[]; positions: TimelinePosition[];
grossPerformance: Big; grossPerformance: Big;
grossPerformancePercentage: Big; grossPerformancePercentage: Big;

View File

@ -66,6 +66,7 @@ describe('PortfolioCalculatorNew', () => {
expect(currentPositions).toEqual({ expect(currentPositions).toEqual({
currentValue: new Big('0'), currentValue: new Big('0'),
errors: [],
grossPerformance: new Big('-12.6'), grossPerformance: new Big('-12.6'),
grossPerformancePercentage: new Big('-0.0440867739678096571'), grossPerformancePercentage: new Big('-0.0440867739678096571'),
hasErrors: false, hasErrors: false,

View File

@ -55,6 +55,7 @@ describe('PortfolioCalculatorNew', () => {
expect(currentPositions).toEqual({ expect(currentPositions).toEqual({
currentValue: new Big('297.8'), currentValue: new Big('297.8'),
errors: [],
grossPerformance: new Big('24.6'), grossPerformance: new Big('24.6'),
grossPerformancePercentage: new Big('0.09004392386530014641'), grossPerformancePercentage: new Big('0.09004392386530014641'),
hasErrors: false, hasErrors: false,

View File

@ -1,7 +1,11 @@
import { TimelineInfoInterface } from '@ghostfolio/api/app/portfolio/interfaces/timeline-info.interface'; import { TimelineInfoInterface } from '@ghostfolio/api/app/portfolio/interfaces/timeline-info.interface';
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
import { TimelinePosition } from '@ghostfolio/common/interfaces'; import {
ResponseError,
TimelinePosition,
UniqueAsset
} from '@ghostfolio/common/interfaces';
import { Logger } from '@nestjs/common'; import { Logger } from '@nestjs/common';
import { Type as TypeOfOrder } from '@prisma/client'; import { Type as TypeOfOrder } from '@prisma/client';
import Big from 'big.js'; import Big from 'big.js';
@ -232,6 +236,8 @@ export class PortfolioCalculatorNew {
const positions: TimelinePosition[] = []; const positions: TimelinePosition[] = [];
let hasAnySymbolMetricsErrors = false; let hasAnySymbolMetricsErrors = false;
const errors: ResponseError['errors'] = [];
for (const item of lastTransactionPoint.items) { for (const item of lastTransactionPoint.items) {
const marketValue = marketSymbolMap[todayString]?.[item.symbol]; const marketValue = marketSymbolMap[todayString]?.[item.symbol];
@ -272,12 +278,17 @@ export class PortfolioCalculatorNew {
symbol: item.symbol, symbol: item.symbol,
transactionCount: item.transactionCount transactionCount: item.transactionCount
}); });
if (hasErrors) {
errors.push({ dataSource: item.dataSource, symbol: item.symbol });
}
} }
const overall = this.calculateOverallPerformance(positions, initialValues); const overall = this.calculateOverallPerformance(positions, initialValues);
return { return {
...overall, ...overall,
errors,
positions, positions,
hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors
}; };

View File

@ -14,7 +14,7 @@ import {
PortfolioChart, PortfolioChart,
PortfolioDetails, PortfolioDetails,
PortfolioInvestments, PortfolioInvestments,
PortfolioPerformance, PortfolioPerformanceResponse,
PortfolioPublicDetails, PortfolioPublicDetails,
PortfolioReport, PortfolioReport,
PortfolioSummary PortfolioSummary
@ -204,10 +204,11 @@ export class PortfolioController {
@Get('performance') @Get('performance')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getPerformance( public async getPerformance(
@Headers('impersonation-id') impersonationId: string, @Headers('impersonation-id') impersonationId: string,
@Query('range') range @Query('range') range
): Promise<{ hasErrors: boolean; performance: PortfolioPerformance }> { ): Promise<PortfolioPerformanceResponse> {
const performanceInformation = await this.portfolioServiceStrategy const performanceInformation = await this.portfolioServiceStrategy
.get() .get()
.getPerformance(impersonationId, range); .getPerformance(impersonationId, range);

View File

@ -24,7 +24,7 @@ import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
import { import {
Accounts, Accounts,
PortfolioDetails, PortfolioDetails,
PortfolioPerformance, PortfolioPerformanceResponse,
PortfolioReport, PortfolioReport,
PortfolioSummary, PortfolioSummary,
Position, Position,
@ -100,15 +100,22 @@ export class PortfolioServiceNew {
} }
} }
const value = details.accounts[account.id]?.current ?? 0;
const result = { const result = {
...account, ...account,
transactionCount, transactionCount,
convertedBalance: this.exchangeRateDataService.toCurrency( value,
balanceInBaseCurrency: this.exchangeRateDataService.toCurrency(
account.balance, account.balance,
account.currency, account.currency,
userCurrency userCurrency
), ),
value: details.accounts[account.id]?.current ?? 0 valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
value,
account.currency,
userCurrency
)
}; };
delete result.Order; delete result.Order;
@ -119,17 +126,26 @@ export class PortfolioServiceNew {
public async getAccountsWithAggregations(aUserId: string): Promise<Accounts> { public async getAccountsWithAggregations(aUserId: string): Promise<Accounts> {
const accounts = await this.getAccounts(aUserId); const accounts = await this.getAccounts(aUserId);
let totalBalance = 0; let totalBalanceInBaseCurrency = new Big(0);
let totalValue = 0; let totalValueInBaseCurrency = new Big(0);
let transactionCount = 0; let transactionCount = 0;
for (const account of accounts) { for (const account of accounts) {
totalBalance += account.convertedBalance; totalBalanceInBaseCurrency = totalBalanceInBaseCurrency.plus(
totalValue += account.value; account.balanceInBaseCurrency
);
totalValueInBaseCurrency = totalValueInBaseCurrency.plus(
account.valueInBaseCurrency
);
transactionCount += account.transactionCount; transactionCount += account.transactionCount;
} }
return { accounts, totalBalance, totalValue, transactionCount }; return {
accounts,
transactionCount,
totalBalanceInBaseCurrency: totalBalanceInBaseCurrency.toNumber(),
totalValueInBaseCurrency: totalValueInBaseCurrency.toNumber()
};
} }
public async getInvestments( public async getInvestments(
@ -293,13 +309,11 @@ export class PortfolioServiceNew {
orders: portfolioOrders orders: portfolioOrders
}); });
if (transactionPoints?.length <= 0) {
return { accounts: {}, holdings: {}, hasErrors: false };
}
portfolioCalculator.setTransactionPoints(transactionPoints); portfolioCalculator.setTransactionPoints(transactionPoints);
const portfolioStart = parseDate(transactionPoints[0].date); const portfolioStart = parseDate(
transactionPoints[0]?.date ?? format(new Date(), DATE_FORMAT)
);
const startDate = this.getStartDate(aDateRange, portfolioStart); const startDate = this.getStartDate(aDateRange, portfolioStart);
const currentPositions = await portfolioCalculator.getCurrentPositions( const currentPositions = await portfolioCalculator.getCurrentPositions(
startDate startDate
@ -312,9 +326,11 @@ export class PortfolioServiceNew {
const holdings: PortfolioDetails['holdings'] = {}; const holdings: PortfolioDetails['holdings'] = {};
const totalInvestment = currentPositions.totalInvestment.plus( const totalInvestment = currentPositions.totalInvestment.plus(
cashDetails.balance cashDetails.balanceInBaseCurrency
);
const totalValue = currentPositions.currentValue.plus(
cashDetails.balanceInBaseCurrency
); );
const totalValue = currentPositions.currentValue.plus(cashDetails.balance);
const dataGatheringItems = currentPositions.positions.map((position) => { const dataGatheringItems = currentPositions.positions.map((position) => {
return { return {
@ -434,7 +450,7 @@ export class PortfolioServiceNew {
}; };
} }
const positionCurrency = orders[0].currency; const positionCurrency = orders[0].SymbolProfile.currency;
const [SymbolProfile] = await this.symbolProfileService.getSymbolProfiles([ const [SymbolProfile] = await this.symbolProfileService.getSymbolProfiles([
aSymbol aSymbol
]); ]);
@ -444,13 +460,13 @@ export class PortfolioServiceNew {
return order.type === 'BUY' || order.type === 'SELL'; return order.type === 'BUY' || order.type === 'SELL';
}) })
.map((order) => ({ .map((order) => ({
currency: order.currency, currency: order.SymbolProfile.currency,
dataSource: order.SymbolProfile?.dataSource ?? order.dataSource, dataSource: order.SymbolProfile.dataSource,
date: format(order.date, DATE_FORMAT), date: format(order.date, DATE_FORMAT),
fee: new Big(order.fee), fee: new Big(order.fee),
name: order.SymbolProfile?.name, name: order.SymbolProfile?.name,
quantity: new Big(order.quantity), quantity: new Big(order.quantity),
symbol: order.symbol, symbol: order.SymbolProfile.symbol,
type: order.type, type: order.type,
unitPrice: new Big(order.unitPrice) unitPrice: new Big(order.unitPrice)
})); }));
@ -714,7 +730,7 @@ export class PortfolioServiceNew {
public async getPerformance( public async getPerformance(
aImpersonationId: string, aImpersonationId: string,
aDateRange: DateRange = 'max' aDateRange: DateRange = 'max'
): Promise<{ hasErrors: boolean; performance: PortfolioPerformance }> { ): Promise<PortfolioPerformanceResponse> {
const userId = await this.getUserId(aImpersonationId, this.request.user.id); const userId = await this.getUserId(aImpersonationId, this.request.user.id);
const { portfolioOrders, transactionPoints } = const { portfolioOrders, transactionPoints } =
@ -760,6 +776,7 @@ export class PortfolioServiceNew {
currentPositions.netPerformancePercentage.toNumber(); currentPositions.netPerformancePercentage.toNumber();
return { return {
errors: currentPositions.errors,
hasErrors: currentPositions.hasErrors || hasErrors, hasErrors: currentPositions.hasErrors || hasErrors,
performance: { performance: {
currentGrossPerformance, currentGrossPerformance,
@ -869,7 +886,7 @@ export class PortfolioServiceNew {
const performanceInformation = await this.getPerformance(aImpersonationId); const performanceInformation = await this.getPerformance(aImpersonationId);
const { balance } = await this.accountService.getCashDetails( const { balanceInBaseCurrency } = await this.accountService.getCashDetails(
userId, userId,
userCurrency userCurrency
); );
@ -887,7 +904,7 @@ export class PortfolioServiceNew {
const committedFunds = new Big(totalBuy).minus(totalSell); const committedFunds = new Big(totalBuy).minus(totalSell);
const netWorth = new Big(balance) const netWorth = new Big(balanceInBaseCurrency)
.plus(performanceInformation.performance.currentValue) .plus(performanceInformation.performance.currentValue)
.plus(items) .plus(items)
.toNumber(); .toNumber();
@ -917,7 +934,7 @@ export class PortfolioServiceNew {
netWorth, netWorth,
totalBuy, totalBuy,
totalSell, totalSell,
cash: balance, cash: balanceInBaseCurrency,
committedFunds: committedFunds.toNumber(), committedFunds: committedFunds.toNumber(),
ordersCount: orders.filter((order) => { ordersCount: orders.filter((order) => {
return order.type === 'BUY' || order.type === 'SELL'; return order.type === 'BUY' || order.type === 'SELL';
@ -1006,7 +1023,7 @@ export class PortfolioServiceNew {
.map((order) => { .map((order) => {
return this.exchangeRateDataService.toCurrency( return this.exchangeRateDataService.toCurrency(
new Big(order.quantity).mul(order.unitPrice).toNumber(), new Big(order.quantity).mul(order.unitPrice).toNumber(),
order.currency, order.SymbolProfile.currency,
this.request.user.Settings.currency this.request.user.Settings.currency
); );
}) })
@ -1025,7 +1042,7 @@ export class PortfolioServiceNew {
.map((order) => { .map((order) => {
return this.exchangeRateDataService.toCurrency( return this.exchangeRateDataService.toCurrency(
order.fee, order.fee,
order.currency, order.SymbolProfile.currency,
this.request.user.Settings.currency this.request.user.Settings.currency
); );
}) })
@ -1047,7 +1064,7 @@ export class PortfolioServiceNew {
.map((order) => { .map((order) => {
return this.exchangeRateDataService.toCurrency( return this.exchangeRateDataService.toCurrency(
new Big(order.quantity).mul(order.unitPrice).toNumber(), new Big(order.quantity).mul(order.unitPrice).toNumber(),
order.currency, order.SymbolProfile.currency,
this.request.user.Settings.currency this.request.user.Settings.currency
); );
}) })
@ -1100,24 +1117,24 @@ export class PortfolioServiceNew {
} }
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({ const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
currency: order.currency, currency: order.SymbolProfile.currency,
dataSource: order.SymbolProfile?.dataSource ?? order.dataSource, dataSource: order.SymbolProfile.dataSource,
date: format(order.date, DATE_FORMAT), date: format(order.date, DATE_FORMAT),
fee: new Big( fee: new Big(
this.exchangeRateDataService.toCurrency( this.exchangeRateDataService.toCurrency(
order.fee, order.fee,
order.currency, order.SymbolProfile.currency,
userCurrency userCurrency
) )
), ),
name: order.SymbolProfile?.name, name: order.SymbolProfile?.name,
quantity: new Big(order.quantity), quantity: new Big(order.quantity),
symbol: order.symbol, symbol: order.SymbolProfile.symbol,
type: order.type, type: order.type,
unitPrice: new Big( unitPrice: new Big(
this.exchangeRateDataService.toCurrency( this.exchangeRateDataService.toCurrency(
order.unitPrice, order.unitPrice,
order.currency, order.SymbolProfile.currency,
userCurrency userCurrency
) )
) )
@ -1153,22 +1170,18 @@ export class PortfolioServiceNew {
return accountId === account.id; return accountId === account.id;
}); });
const convertedBalance = this.exchangeRateDataService.toCurrency(
account.balance,
account.currency,
userCurrency
);
accounts[account.id] = { accounts[account.id] = {
balance: convertedBalance, balance: account.balance,
currency: account.currency, currency: account.currency,
current: convertedBalance, current: account.balance,
name: account.name, name: account.name,
original: convertedBalance original: account.balance
}; };
for (const order of ordersByAccount) { for (const order of ordersByAccount) {
let currentValueOfSymbol = let currentValueOfSymbol =
order.quantity * portfolioItemsNow[order.symbol].marketPrice; order.quantity *
portfolioItemsNow[order.SymbolProfile.symbol].marketPrice;
let originalValueOfSymbol = order.quantity * order.unitPrice; let originalValueOfSymbol = order.quantity * order.unitPrice;
if (order.type === 'SELL') { if (order.type === 'SELL') {
@ -1218,7 +1231,7 @@ export class PortfolioServiceNew {
.map((order) => { .map((order) => {
return this.exchangeRateDataService.toCurrency( return this.exchangeRateDataService.toCurrency(
order.quantity * order.unitPrice, order.quantity * order.unitPrice,
order.currency, order.SymbolProfile.currency,
currency currency
); );
}) })

View File

@ -25,7 +25,7 @@ import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
import { import {
Accounts, Accounts,
PortfolioDetails, PortfolioDetails,
PortfolioPerformance, PortfolioPerformanceResponse,
PortfolioReport, PortfolioReport,
PortfolioSummary, PortfolioSummary,
Position, Position,
@ -99,15 +99,22 @@ export class PortfolioService {
} }
} }
const value = details.accounts[account.id]?.current ?? 0;
const result = { const result = {
...account, ...account,
transactionCount, transactionCount,
convertedBalance: this.exchangeRateDataService.toCurrency( value,
balanceInBaseCurrency: this.exchangeRateDataService.toCurrency(
account.balance, account.balance,
account.currency, account.currency,
userCurrency userCurrency
), ),
value: details.accounts[account.id]?.current ?? 0 valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
value,
account.currency,
userCurrency
)
}; };
delete result.Order; delete result.Order;
@ -118,17 +125,26 @@ export class PortfolioService {
public async getAccountsWithAggregations(aUserId: string): Promise<Accounts> { public async getAccountsWithAggregations(aUserId: string): Promise<Accounts> {
const accounts = await this.getAccounts(aUserId); const accounts = await this.getAccounts(aUserId);
let totalBalance = 0; let totalBalanceInBaseCurrency = new Big(0);
let totalValue = 0; let totalValueInBaseCurrency = new Big(0);
let transactionCount = 0; let transactionCount = 0;
for (const account of accounts) { for (const account of accounts) {
totalBalance += account.convertedBalance; totalBalanceInBaseCurrency = totalBalanceInBaseCurrency.plus(
totalValue += account.value; account.balanceInBaseCurrency
);
totalValueInBaseCurrency = totalValueInBaseCurrency.plus(
account.valueInBaseCurrency
);
transactionCount += account.transactionCount; transactionCount += account.transactionCount;
} }
return { accounts, totalBalance, totalValue, transactionCount }; return {
accounts,
transactionCount,
totalBalanceInBaseCurrency: totalBalanceInBaseCurrency.toNumber(),
totalValueInBaseCurrency: totalValueInBaseCurrency.toNumber()
};
} }
public async getInvestments( public async getInvestments(
@ -281,13 +297,11 @@ export class PortfolioService {
userId userId
}); });
if (transactionPoints?.length <= 0) {
return { accounts: {}, holdings: {}, hasErrors: false };
}
portfolioCalculator.setTransactionPoints(transactionPoints); portfolioCalculator.setTransactionPoints(transactionPoints);
const portfolioStart = parseDate(transactionPoints[0].date); const portfolioStart = parseDate(
transactionPoints[0]?.date ?? format(new Date(), DATE_FORMAT)
);
const startDate = this.getStartDate(aDateRange, portfolioStart); const startDate = this.getStartDate(aDateRange, portfolioStart);
const currentPositions = await portfolioCalculator.getCurrentPositions( const currentPositions = await portfolioCalculator.getCurrentPositions(
startDate startDate
@ -300,9 +314,11 @@ export class PortfolioService {
const holdings: PortfolioDetails['holdings'] = {}; const holdings: PortfolioDetails['holdings'] = {};
const totalInvestment = currentPositions.totalInvestment.plus( const totalInvestment = currentPositions.totalInvestment.plus(
cashDetails.balance cashDetails.balanceInBaseCurrency
);
const totalValue = currentPositions.currentValue.plus(
cashDetails.balanceInBaseCurrency
); );
const totalValue = currentPositions.currentValue.plus(cashDetails.balance);
const dataGatheringItems = currentPositions.positions.map((position) => { const dataGatheringItems = currentPositions.positions.map((position) => {
return { return {
@ -422,7 +438,7 @@ export class PortfolioService {
}; };
} }
const positionCurrency = orders[0].currency; const positionCurrency = orders[0].SymbolProfile.currency;
const [SymbolProfile] = await this.symbolProfileService.getSymbolProfiles([ const [SymbolProfile] = await this.symbolProfileService.getSymbolProfiles([
aSymbol aSymbol
]); ]);
@ -432,13 +448,13 @@ export class PortfolioService {
return order.type === 'BUY' || order.type === 'SELL'; return order.type === 'BUY' || order.type === 'SELL';
}) })
.map((order) => ({ .map((order) => ({
currency: order.currency, currency: order.SymbolProfile.currency,
dataSource: order.SymbolProfile?.dataSource ?? order.dataSource, dataSource: order.SymbolProfile.dataSource,
date: format(order.date, DATE_FORMAT), date: format(order.date, DATE_FORMAT),
fee: new Big(order.fee), fee: new Big(order.fee),
name: order.SymbolProfile?.name, name: order.SymbolProfile?.name,
quantity: new Big(order.quantity), quantity: new Big(order.quantity),
symbol: order.symbol, symbol: order.SymbolProfile.symbol,
type: order.type, type: order.type,
unitPrice: new Big(order.unitPrice) unitPrice: new Big(order.unitPrice)
})); }));
@ -696,7 +712,7 @@ export class PortfolioService {
public async getPerformance( public async getPerformance(
aImpersonationId: string, aImpersonationId: string,
aDateRange: DateRange = 'max' aDateRange: DateRange = 'max'
): Promise<{ hasErrors: boolean; performance: PortfolioPerformance }> { ): Promise<PortfolioPerformanceResponse> {
const userId = await this.getUserId(aImpersonationId, this.request.user.id); const userId = await this.getUserId(aImpersonationId, this.request.user.id);
const portfolioCalculator = new PortfolioCalculator( const portfolioCalculator = new PortfolioCalculator(
@ -848,7 +864,7 @@ export class PortfolioService {
const performanceInformation = await this.getPerformance(aImpersonationId); const performanceInformation = await this.getPerformance(aImpersonationId);
const { balance } = await this.accountService.getCashDetails( const { balanceInBaseCurrency } = await this.accountService.getCashDetails(
userId, userId,
userCurrency userCurrency
); );
@ -866,7 +882,7 @@ export class PortfolioService {
const committedFunds = new Big(totalBuy).minus(totalSell); const committedFunds = new Big(totalBuy).minus(totalSell);
const netWorth = new Big(balance) const netWorth = new Big(balanceInBaseCurrency)
.plus(performanceInformation.performance.currentValue) .plus(performanceInformation.performance.currentValue)
.plus(items) .plus(items)
.toNumber(); .toNumber();
@ -882,7 +898,7 @@ export class PortfolioService {
totalSell, totalSell,
annualizedPerformancePercent: annualizedPerformancePercent:
performanceInformation.performance.annualizedPerformancePercent, performanceInformation.performance.annualizedPerformancePercent,
cash: balance, cash: balanceInBaseCurrency,
committedFunds: committedFunds.toNumber(), committedFunds: committedFunds.toNumber(),
ordersCount: orders.filter((order) => { ordersCount: orders.filter((order) => {
return order.type === 'BUY' || order.type === 'SELL'; return order.type === 'BUY' || order.type === 'SELL';
@ -971,7 +987,7 @@ export class PortfolioService {
.map((order) => { .map((order) => {
return this.exchangeRateDataService.toCurrency( return this.exchangeRateDataService.toCurrency(
new Big(order.quantity).mul(order.unitPrice).toNumber(), new Big(order.quantity).mul(order.unitPrice).toNumber(),
order.currency, order.SymbolProfile.currency,
this.request.user.Settings.currency this.request.user.Settings.currency
); );
}) })
@ -990,7 +1006,7 @@ export class PortfolioService {
.map((order) => { .map((order) => {
return this.exchangeRateDataService.toCurrency( return this.exchangeRateDataService.toCurrency(
order.fee, order.fee,
order.currency, order.SymbolProfile.currency,
this.request.user.Settings.currency this.request.user.Settings.currency
); );
}) })
@ -1012,7 +1028,7 @@ export class PortfolioService {
.map((order) => { .map((order) => {
return this.exchangeRateDataService.toCurrency( return this.exchangeRateDataService.toCurrency(
new Big(order.quantity).mul(order.unitPrice).toNumber(), new Big(order.quantity).mul(order.unitPrice).toNumber(),
order.currency, order.SymbolProfile.currency,
this.request.user.Settings.currency this.request.user.Settings.currency
); );
}) })
@ -1064,24 +1080,24 @@ export class PortfolioService {
} }
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({ const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
currency: order.currency, currency: order.SymbolProfile.currency,
dataSource: order.SymbolProfile?.dataSource ?? order.dataSource, dataSource: order.SymbolProfile.dataSource,
date: format(order.date, DATE_FORMAT), date: format(order.date, DATE_FORMAT),
fee: new Big( fee: new Big(
this.exchangeRateDataService.toCurrency( this.exchangeRateDataService.toCurrency(
order.fee, order.fee,
order.currency, order.SymbolProfile.currency,
userCurrency userCurrency
) )
), ),
name: order.SymbolProfile?.name, name: order.SymbolProfile?.name,
quantity: new Big(order.quantity), quantity: new Big(order.quantity),
symbol: order.symbol, symbol: order.SymbolProfile.symbol,
type: order.type, type: order.type,
unitPrice: new Big( unitPrice: new Big(
this.exchangeRateDataService.toCurrency( this.exchangeRateDataService.toCurrency(
order.unitPrice, order.unitPrice,
order.currency, order.SymbolProfile.currency,
userCurrency userCurrency
) )
) )
@ -1113,22 +1129,18 @@ export class PortfolioService {
return accountId === account.id; return accountId === account.id;
}); });
const convertedBalance = this.exchangeRateDataService.toCurrency(
account.balance,
account.currency,
userCurrency
);
accounts[account.id] = { accounts[account.id] = {
balance: convertedBalance, balance: account.balance,
currency: account.currency, currency: account.currency,
current: convertedBalance, current: account.balance,
name: account.name, name: account.name,
original: convertedBalance original: account.balance
}; };
for (const order of ordersByAccount) { for (const order of ordersByAccount) {
let currentValueOfSymbol = let currentValueOfSymbol =
order.quantity * portfolioItemsNow[order.symbol].marketPrice; order.quantity *
portfolioItemsNow[order.SymbolProfile.symbol].marketPrice;
let originalValueOfSymbol = order.quantity * order.unitPrice; let originalValueOfSymbol = order.quantity * order.unitPrice;
if (order.type === 'SELL') { if (order.type === 'SELL') {
@ -1178,7 +1190,7 @@ export class PortfolioService {
.map((order) => { .map((order) => {
return this.exchangeRateDataService.toCurrency( return this.exchangeRateDataService.toCurrency(
order.quantity * order.unitPrice, order.quantity * order.unitPrice,
order.currency, order.SymbolProfile.currency,
currency currency
); );
}) })

View File

@ -32,7 +32,6 @@ export class TransformDataSourceInResponseInterceptor<T>
activity.SymbolProfile.dataSource = encodeDataSource( activity.SymbolProfile.dataSource = encodeDataSource(
activity.SymbolProfile.dataSource activity.SymbolProfile.dataSource
); );
activity.dataSource = encodeDataSource(activity.dataSource);
return activity; return activity;
}); });
} }
@ -41,6 +40,14 @@ export class TransformDataSourceInResponseInterceptor<T>
data.dataSource = encodeDataSource(data.dataSource); data.dataSource = encodeDataSource(data.dataSource);
} }
if (data.errors) {
for (const error of data.errors) {
if (error.dataSource) {
error.dataSource = encodeDataSource(error.dataSource);
}
}
}
if (data.holdings) { if (data.holdings) {
for (const symbol of Object.keys(data.holdings)) { for (const symbol of Object.keys(data.holdings)) {
if (data.holdings[symbol].dataSource) { if (data.holdings[symbol].dataSource) {
@ -58,13 +65,6 @@ export class TransformDataSourceInResponseInterceptor<T>
}); });
} }
if (data.orders) {
data.orders.map((order) => {
order.dataSource = encodeDataSource(order.dataSource);
return order;
});
}
if (data.positions) { if (data.positions) {
data.positions.map((position) => { data.positions.map((position) => {
position.dataSource = encodeDataSource(position.dataSource); position.dataSource = encodeDataSource(position.dataSource);

View File

@ -4,6 +4,7 @@ import {
PROPERTY_LOCKED_DATA_GATHERING PROPERTY_LOCKED_DATA_GATHERING
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { DATE_FORMAT, resetHours } from '@ghostfolio/common/helper'; import { DATE_FORMAT, resetHours } from '@ghostfolio/common/helper';
import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { Inject, Injectable, Logger } from '@nestjs/common'; import { Inject, Injectable, Logger } from '@nestjs/common';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import { import {
@ -121,13 +122,7 @@ export class DataGatheringService {
} }
} }
public async gatherSymbol({ public async gatherSymbol({ dataSource, symbol }: UniqueAsset) {
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
const isDataGatheringLocked = await this.prismaService.property.findUnique({ const isDataGatheringLocked = await this.prismaService.property.findUnique({
where: { key: PROPERTY_LOCKED_DATA_GATHERING } where: { key: PROPERTY_LOCKED_DATA_GATHERING }
}); });
@ -554,19 +549,24 @@ export class DataGatheringService {
} }
private async getSymbolsProfileData(): Promise<IDataGatheringItem[]> { private async getSymbolsProfileData(): Promise<IDataGatheringItem[]> {
const distinctOrders = await this.prismaService.order.findMany({ const symbolProfiles = await this.prismaService.symbolProfile.findMany({
distinct: ['symbol'], orderBy: [{ symbol: 'asc' }]
orderBy: [{ symbol: 'asc' }],
select: { dataSource: true, symbol: true }
}); });
return distinctOrders.filter((distinctOrder) => { return symbolProfiles
return ( .filter((symbolProfile) => {
distinctOrder.dataSource !== DataSource.GHOSTFOLIO && return (
distinctOrder.dataSource !== DataSource.MANUAL && symbolProfile.dataSource !== DataSource.GHOSTFOLIO &&
distinctOrder.dataSource !== DataSource.RAKUTEN symbolProfile.dataSource !== DataSource.MANUAL &&
); symbolProfile.dataSource !== DataSource.RAKUTEN
}); );
})
.map((symbolProfile) => {
return {
dataSource: symbolProfile.dataSource,
symbol: symbolProfile.symbol
};
});
} }
private async isDataGatheringNeeded() { private async isDataGatheringNeeded() {

View File

@ -1,5 +1,6 @@
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { import {
IDataProviderHistoricalResponse, IDataProviderHistoricalResponse,
IDataProviderResponse IDataProviderResponse
@ -10,7 +11,6 @@ import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client'; import { DataSource, SymbolProfile } from '@prisma/client';
import { isAfter, isBefore, parse } from 'date-fns'; import { isAfter, isBefore, parse } from 'date-fns';
import { DataProviderInterface } from '../interfaces/data-provider.interface';
import { IAlphaVantageHistoricalResponse } from './interfaces/interfaces'; import { IAlphaVantageHistoricalResponse } from './interfaces/interfaces';
@Injectable() @Injectable()

View File

@ -7,11 +7,7 @@ import {
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
import { import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
DATE_FORMAT,
getYesterday,
isGhostfolioScraperApiSymbol
} from '@ghostfolio/common/helper';
import { Granularity } from '@ghostfolio/common/types'; import { Granularity } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client'; import { DataSource, SymbolProfile } from '@prisma/client';
@ -29,7 +25,7 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
) {} ) {}
public canHandle(symbol: string) { public canHandle(symbol: string) {
return isGhostfolioScraperApiSymbol(symbol); return true;
} }
public async getAssetProfile( public async getAssetProfile(

View File

@ -1,5 +1,6 @@
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { import {
IDataProviderHistoricalResponse, IDataProviderHistoricalResponse,
IDataProviderResponse, IDataProviderResponse,
@ -14,8 +15,6 @@ import { DataSource, SymbolProfile } from '@prisma/client';
import * as bent from 'bent'; import * as bent from 'bent';
import { format, subMonths, subWeeks, subYears } from 'date-fns'; import { format, subMonths, subWeeks, subYears } from 'date-fns';
import { DataProviderInterface } from '../interfaces/data-provider.interface';
@Injectable() @Injectable()
export class RakutenRapidApiService implements DataProviderInterface { export class RakutenRapidApiService implements DataProviderInterface {
public static FEAR_AND_GREED_INDEX_NAME = 'Fear & Greed Index'; public static FEAR_AND_GREED_INDEX_NAME = 'Fear & Greed Index';

View File

@ -1,5 +1,6 @@
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service'; import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { import {
IDataProviderHistoricalResponse, IDataProviderHistoricalResponse,
IDataProviderResponse, IDataProviderResponse,
@ -19,9 +20,7 @@ import * as bent from 'bent';
import Big from 'big.js'; import Big from 'big.js';
import { countries } from 'countries-list'; import { countries } from 'countries-list';
import { addDays, format, isSameDay } from 'date-fns'; import { addDays, format, isSameDay } from 'date-fns';
import yahooFinance2 from 'yahoo-finance2'; import yahooFinance from 'yahoo-finance2';
import { DataProviderInterface } from '../interfaces/data-provider.interface';
@Injectable() @Injectable()
export class YahooFinanceService implements DataProviderInterface { export class YahooFinanceService implements DataProviderInterface {
@ -80,7 +79,7 @@ export class YahooFinanceService implements DataProviderInterface {
try { try {
const symbol = this.convertToYahooFinanceSymbol(aSymbol); const symbol = this.convertToYahooFinanceSymbol(aSymbol);
const assetProfile = await yahooFinance2.quoteSummary(symbol, { const assetProfile = await yahooFinance.quoteSummary(symbol, {
modules: ['price', 'summaryProfile'] modules: ['price', 'summaryProfile']
}); });
@ -143,7 +142,7 @@ export class YahooFinanceService implements DataProviderInterface {
const yahooFinanceSymbol = this.convertToYahooFinanceSymbol(aSymbol); const yahooFinanceSymbol = this.convertToYahooFinanceSymbol(aSymbol);
try { try {
const historicalResult = await yahooFinance2.historical( const historicalResult = await yahooFinance.historical(
yahooFinanceSymbol, yahooFinanceSymbol,
{ {
interval: '1d', interval: '1d',
@ -202,7 +201,7 @@ export class YahooFinanceService implements DataProviderInterface {
try { try {
const response: { [symbol: string]: IDataProviderResponse } = {}; const response: { [symbol: string]: IDataProviderResponse } = {};
const quotes = await yahooFinance2.quote(yahooFinanceSymbols); const quotes = await yahooFinance.quote(yahooFinanceSymbols);
for (const quote of quotes) { for (const quote of quotes) {
// Convert symbols back // Convert symbols back

View File

@ -114,6 +114,10 @@ export class ExchangeRateDataService {
aFromCurrency: string, aFromCurrency: string,
aToCurrency: string aToCurrency: string
) { ) {
if (aValue === 0) {
return 0;
}
const hasNaN = Object.values(this.exchangeRates).some((exchangeRate) => { const hasNaN = Object.values(this.exchangeRates).some((exchangeRate) => {
return isNaN(exchangeRate); return isNaN(exchangeRate);
}); });
@ -187,12 +191,7 @@ export class ExchangeRateDataService {
await this.prismaService.symbolProfile.findMany({ await this.prismaService.symbolProfile.findMany({
distinct: ['currency'], distinct: ['currency'],
orderBy: [{ currency: 'asc' }], orderBy: [{ currency: 'asc' }],
select: { currency: true }, select: { currency: true }
where: {
currency: {
not: null
}
}
}) })
).forEach((symbolProfile) => { ).forEach((symbolProfile) => {
currencies.push(symbolProfile.currency); currencies.push(symbolProfile.currency);
@ -206,7 +205,7 @@ export class ExchangeRateDataService {
currencies = currencies.concat(customCurrencies); currencies = currencies.concat(customCurrencies);
} }
return uniq(currencies).sort(); return uniq(currencies).filter(Boolean).sort();
} }
private prepareCurrencyPairs(aCurrencies: string[]) { private prepareCurrencyPairs(aCurrencies: string[]) {

View File

@ -2,6 +2,7 @@ import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-dat
import { DateQuery } from '@ghostfolio/api/app/portfolio/interfaces/date-query.interface'; import { DateQuery } from '@ghostfolio/api/app/portfolio/interfaces/date-query.interface';
import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { resetHours } from '@ghostfolio/common/helper'; import { resetHours } from '@ghostfolio/common/helper';
import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { DataSource, MarketData, Prisma } from '@prisma/client'; import { DataSource, MarketData, Prisma } from '@prisma/client';
@ -9,13 +10,7 @@ import { DataSource, MarketData, Prisma } from '@prisma/client';
export class MarketDataService { export class MarketDataService {
public constructor(private readonly prismaService: PrismaService) {} public constructor(private readonly prismaService: PrismaService) {}
public async deleteMany({ public async deleteMany({ dataSource, symbol }: UniqueAsset) {
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
return this.prismaService.marketData.deleteMany({ return this.prismaService.marketData.deleteMany({
where: { where: {
dataSource, dataSource,

View File

@ -86,7 +86,7 @@
class="d-inline-block justify-content-end" class="d-inline-block justify-content-end"
[isCurrency]="true" [isCurrency]="true"
[locale]="locale" [locale]="locale"
[value]="element.convertedBalance" [value]="element.balance"
></gf-value> ></gf-value>
</td> </td>
<td *matFooterCellDef class="px-1 text-right" mat-footer-cell> <td *matFooterCellDef class="px-1 text-right" mat-footer-cell>
@ -94,7 +94,7 @@
class="d-inline-block justify-content-end" class="d-inline-block justify-content-end"
[isCurrency]="true" [isCurrency]="true"
[locale]="locale" [locale]="locale"
[value]="totalBalance" [value]="totalBalanceInBaseCurrency"
></gf-value> ></gf-value>
</td> </td>
</ng-container> </ng-container>
@ -116,7 +116,7 @@
class="d-inline-block justify-content-end" class="d-inline-block justify-content-end"
[isCurrency]="true" [isCurrency]="true"
[locale]="locale" [locale]="locale"
[value]="totalValue" [value]="totalValueInBaseCurrency"
></gf-value> ></gf-value>
</td> </td>
</ng-container> </ng-container>

View File

@ -24,8 +24,8 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
@Input() deviceType: string; @Input() deviceType: string;
@Input() locale: string; @Input() locale: string;
@Input() showActions: boolean; @Input() showActions: boolean;
@Input() totalBalance: number; @Input() totalBalanceInBaseCurrency: number;
@Input() totalValue: number; @Input() totalValueInBaseCurrency: number;
@Input() transactionCount: number; @Input() transactionCount: number;
@Output() accountDeleted = new EventEmitter<string>(); @Output() accountDeleted = new EventEmitter<string>();

View File

@ -8,6 +8,7 @@ import {
import { AdminService } from '@ghostfolio/client/services/admin.service'; import { AdminService } from '@ghostfolio/client/services/admin.service';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config'; import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface'; import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface';
import { DataSource, MarketData } from '@prisma/client'; import { DataSource, MarketData } from '@prisma/client';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
@ -44,39 +45,21 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
this.fetchAdminMarketData(); this.fetchAdminMarketData();
} }
public onDeleteProfileData({ public onDeleteProfileData({ dataSource, symbol }: UniqueAsset) {
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
this.adminService this.adminService
.deleteProfileData({ dataSource, symbol }) .deleteProfileData({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {}); .subscribe(() => {});
} }
public onGatherProfileDataBySymbol({ public onGatherProfileDataBySymbol({ dataSource, symbol }: UniqueAsset) {
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
this.adminService this.adminService
.gatherProfileDataBySymbol({ dataSource, symbol }) .gatherProfileDataBySymbol({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {}); .subscribe(() => {});
} }
public onGatherSymbol({ public onGatherSymbol({ dataSource, symbol }: UniqueAsset) {
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
this.adminService this.adminService
.gatherSymbol({ dataSource, symbol }) .gatherSymbol({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
@ -93,13 +76,7 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
} }
} }
public setCurrentProfile({ public setCurrentProfile({ dataSource, symbol }: UniqueAsset) {
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
this.marketDataDetails = []; this.marketDataDetails = [];
if (this.currentSymbol === symbol) { if (this.currentSymbol === symbol) {
@ -129,13 +106,7 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
}); });
} }
private fetchAdminMarketDataBySymbol({ private fetchAdminMarketDataBySymbol({ dataSource, symbol }: UniqueAsset) {
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
this.adminService this.adminService
.fetchAdminMarketDataBySymbol({ dataSource, symbol }) .fetchAdminMarketDataBySymbol({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))

View File

@ -7,7 +7,11 @@ import {
} from '@ghostfolio/client/services/settings-storage.service'; } from '@ghostfolio/client/services/settings-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { defaultDateRangeOptions } from '@ghostfolio/common/config'; import { defaultDateRangeOptions } from '@ghostfolio/common/config';
import { PortfolioPerformance, User } from '@ghostfolio/common/interfaces'; import {
PortfolioPerformance,
UniqueAsset,
User
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { DateRange } from '@ghostfolio/common/types'; import { DateRange } from '@ghostfolio/common/types';
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface'; import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
@ -24,6 +28,7 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
public dateRange: DateRange; public dateRange: DateRange;
public dateRangeOptions = defaultDateRangeOptions; public dateRangeOptions = defaultDateRangeOptions;
public deviceType: string; public deviceType: string;
public errors: UniqueAsset[];
public hasError: boolean; public hasError: boolean;
public hasImpersonationId: boolean; public hasImpersonationId: boolean;
public hasPermissionToCreateOrder: boolean; public hasPermissionToCreateOrder: boolean;
@ -126,6 +131,7 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
.fetchPortfolioPerformance({ range: this.dateRange }) .fetchPortfolioPerformance({ range: this.dateRange })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => { .subscribe((response) => {
this.errors = response.errors;
this.hasError = response.hasErrors; this.hasError = response.hasErrors;
this.performance = response.performance; this.performance = response.performance;
this.isLoadingPerformance = false; this.isLoadingPerformance = false;

View File

@ -28,6 +28,7 @@
class="pb-4" class="pb-4"
[baseCurrency]="user?.settings?.baseCurrency" [baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType" [deviceType]="deviceType"
[errors]="errors"
[hasError]="hasError" [hasError]="hasError"
[isAllTimeHigh]="isAllTimeHigh" [isAllTimeHigh]="isAllTimeHigh"
[isAllTimeLow]="isAllTimeLow" [isAllTimeLow]="isAllTimeLow"

View File

@ -7,6 +7,7 @@
? 'Sorry! Our data provider partner is experiencing the hiccups.' ? 'Sorry! Our data provider partner is experiencing the hiccups.'
: '' : ''
" "
(click)="errors?.length > 0 && onShowErrors()"
> >
<ion-icon <ion-icon
*ngIf="hasError && !isLoading" *ngIf="hasError && !isLoading"

View File

@ -7,7 +7,10 @@ import {
OnInit, OnInit,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { PortfolioPerformance } from '@ghostfolio/common/interfaces'; import {
PortfolioPerformance,
ResponseError
} from '@ghostfolio/common/interfaces';
import { CountUp } from 'countup.js'; import { CountUp } from 'countup.js';
import { isNumber } from 'lodash'; import { isNumber } from 'lodash';
@ -20,6 +23,7 @@ import { isNumber } from 'lodash';
export class PortfolioPerformanceComponent implements OnChanges, OnInit { export class PortfolioPerformanceComponent implements OnChanges, OnInit {
@Input() baseCurrency: string; @Input() baseCurrency: string;
@Input() deviceType: string; @Input() deviceType: string;
@Input() errors: ResponseError['errors'];
@Input() hasError: boolean; @Input() hasError: boolean;
@Input() isAllTimeHigh: boolean; @Input() isAllTimeHigh: boolean;
@Input() isAllTimeLow: boolean; @Input() isAllTimeLow: boolean;
@ -69,4 +73,12 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
} }
} }
} }
public onShowErrors() {
const errorMessageParts = this.errors.map((error) => {
return `${error.symbol} (${error.dataSource})`;
});
alert(errorMessageParts.join('\n'));
}
} }

View File

@ -13,8 +13,8 @@ import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort'; import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { PortfolioPosition } from '@ghostfolio/common/interfaces'; import { PortfolioPosition, UniqueAsset } from '@ghostfolio/common/interfaces';
import { AssetClass, DataSource, Order as OrderModel } from '@prisma/client'; import { AssetClass, Order as OrderModel } from '@prisma/client';
import { Subject, Subscription } from 'rxjs'; import { Subject, Subscription } from 'rxjs';
@Component({ @Component({
@ -75,13 +75,7 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
this.dataSource.filter = filterValue.trim().toLowerCase(); this.dataSource.filter = filterValue.trim().toLowerCase();
}*/ }*/
public onOpenPositionDialog({ public onOpenPositionDialog({ dataSource, symbol }: UniqueAsset): void {
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}): void {
this.router.navigate([], { this.router.navigate([], {
queryParams: { dataSource, symbol, positionDetailDialog: true } queryParams: { dataSource, symbol, positionDetailDialog: true }
}); });

View File

@ -28,8 +28,8 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
public hasPermissionToCreateAccount: boolean; public hasPermissionToCreateAccount: boolean;
public hasPermissionToDeleteAccount: boolean; public hasPermissionToDeleteAccount: boolean;
public routeQueryParams: Subscription; public routeQueryParams: Subscription;
public totalBalance = 0; public totalBalanceInBaseCurrency = 0;
public totalValue = 0; public totalValueInBaseCurrency = 0;
public transactionCount = 0; public transactionCount = 0;
public user: User; public user: User;
@ -106,18 +106,25 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
this.dataService this.dataService
.fetchAccounts() .fetchAccounts()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ accounts, totalBalance, totalValue, transactionCount }) => { .subscribe(
this.accounts = accounts; ({
this.totalBalance = totalBalance; accounts,
this.totalValue = totalValue; totalBalanceInBaseCurrency,
this.transactionCount = transactionCount; totalValueInBaseCurrency,
transactionCount
}) => {
this.accounts = accounts;
this.totalBalanceInBaseCurrency = totalBalanceInBaseCurrency;
this.totalValueInBaseCurrency = totalValueInBaseCurrency;
this.transactionCount = transactionCount;
if (this.accounts?.length <= 0) { if (this.accounts?.length <= 0) {
this.router.navigate([], { queryParams: { createDialog: true } }); this.router.navigate([], { queryParams: { createDialog: true } });
}
this.changeDetectorRef.markForCheck();
} }
);
this.changeDetectorRef.markForCheck();
});
} }
public onDeleteAccount(aId: string) { public onDeleteAccount(aId: string) {

View File

@ -9,8 +9,8 @@
[deviceType]="deviceType" [deviceType]="deviceType"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[showActions]="!hasImpersonationId && hasPermissionToDeleteAccount && !user.settings.isRestrictedView" [showActions]="!hasImpersonationId && hasPermissionToDeleteAccount && !user.settings.isRestrictedView"
[totalBalance]="totalBalance" [totalBalanceInBaseCurrency]="totalBalanceInBaseCurrency"
[totalValue]="totalValue" [totalValueInBaseCurrency]="totalValueInBaseCurrency"
[transactionCount]="transactionCount" [transactionCount]="transactionCount"
(accountDeleted)="onDeleteAccount($event)" (accountDeleted)="onDeleteAccount($event)"
(accountToUpdate)="onUpdateAccount($event)" (accountToUpdate)="onUpdateAccount($event)"

View File

@ -10,6 +10,7 @@ import { prettifySymbol } from '@ghostfolio/common/helper';
import { import {
PortfolioDetails, PortfolioDetails,
PortfolioPosition, PortfolioPosition,
UniqueAsset,
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
@ -64,7 +65,12 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
[name: string]: { name: string; value: number }; [name: string]: { name: string; value: number };
}; };
public symbols: { public symbols: {
[name: string]: { name: string; symbol: string; value: number }; [name: string]: {
dataSource?: DataSource;
name: string;
symbol: string;
value: number;
};
}; };
public user: User; public user: User;
@ -281,6 +287,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
if (position.assetClass === AssetClass.EQUITY) { if (position.assetClass === AssetClass.EQUITY) {
this.symbols[prettifySymbol(symbol)] = { this.symbols[prettifySymbol(symbol)] = {
dataSource: position.dataSource,
name: position.name, name: position.name,
symbol: prettifySymbol(symbol), symbol: prettifySymbol(symbol),
value: aPeriod === 'original' ? position.investment : position.value value: aPeriod === 'original' ? position.investment : position.value
@ -295,6 +302,14 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
this.initializeAnalysisData(this.period); this.initializeAnalysisData(this.period);
} }
public onProportionChartClicked({ dataSource, symbol }: UniqueAsset) {
if (dataSource && symbol) {
this.router.navigate([], {
queryParams: { dataSource, symbol, positionDetailDialog: true }
});
}
}
public ngOnDestroy() { public ngOnDestroy() {
this.unsubscribeSubject.next(); this.unsubscribeSubject.next();
this.unsubscribeSubject.complete(); this.unsubscribeSubject.complete();

View File

@ -89,12 +89,14 @@
<mat-card-content> <mat-card-content>
<gf-portfolio-proportion-chart <gf-portfolio-proportion-chart
class="mx-auto" class="mx-auto"
cursor="pointer"
[baseCurrency]="user?.settings?.baseCurrency" [baseCurrency]="user?.settings?.baseCurrency"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView" [isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[keys]="['symbol']" [keys]="['symbol']"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[positions]="symbols" [positions]="symbols"
[showLabels]="deviceType !== 'mobile'" [showLabels]="deviceType !== 'mobile'"
(proportionChartClicked)="onProportionChartClicked($event)"
></gf-portfolio-proportion-chart> ></gf-portfolio-proportion-chart>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>

View File

@ -158,11 +158,11 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
this.activityForm.controls['type'].disable(); this.activityForm.controls['type'].disable();
} }
if (this.data.activity?.symbol) { if (this.data.activity?.SymbolProfile?.symbol) {
this.dataService this.dataService
.fetchSymbolItem({ .fetchSymbolItem({
dataSource: this.data.activity?.dataSource, dataSource: this.data.activity?.SymbolProfile?.dataSource,
symbol: this.data.activity?.symbol symbol: this.data.activity?.SymbolProfile?.symbol
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ marketPrice }) => { .subscribe(({ marketPrice }) => {
@ -196,9 +196,7 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
} else { } else {
this.activityForm.controls['searchSymbol'].setErrors({ incorrect: true }); this.activityForm.controls['searchSymbol'].setErrors({ incorrect: true });
this.data.activity.currency = null; this.data.activity.SymbolProfile = null;
this.data.activity.dataSource = null;
this.data.activity.symbol = null;
} }
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
@ -259,9 +257,7 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
}) })
.pipe( .pipe(
catchError(() => { catchError(() => {
this.data.activity.currency = null; this.data.activity.SymbolProfile = null;
this.data.activity.dataSource = null;
this.data.activity.unitPrice = null;
this.isLoading = false; this.isLoading = false;

View File

@ -3,7 +3,10 @@ import { Injectable } from '@angular/core';
import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-data.dto'; import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-data.dto';
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { AdminMarketDataDetails } from '@ghostfolio/common/interfaces'; import {
AdminMarketDataDetails,
UniqueAsset
} from '@ghostfolio/common/interfaces';
import { DataSource, MarketData } from '@prisma/client'; import { DataSource, MarketData } from '@prisma/client';
import { format, parseISO } from 'date-fns'; import { format, parseISO } from 'date-fns';
import { Observable, map } from 'rxjs'; import { Observable, map } from 'rxjs';
@ -14,13 +17,7 @@ import { Observable, map } from 'rxjs';
export class AdminService { export class AdminService {
public constructor(private http: HttpClient) {} public constructor(private http: HttpClient) {}
public deleteProfileData({ public deleteProfileData({ dataSource, symbol }: UniqueAsset) {
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
return this.http.delete<void>( return this.http.delete<void>(
`/api/admin/profile-data/${dataSource}/${symbol}` `/api/admin/profile-data/${dataSource}/${symbol}`
); );
@ -53,13 +50,7 @@ export class AdminService {
return this.http.post<void>(`/api/admin/gather/profile-data`, {}); return this.http.post<void>(`/api/admin/gather/profile-data`, {});
} }
public gatherProfileDataBySymbol({ public gatherProfileDataBySymbol({ dataSource, symbol }: UniqueAsset) {
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
return this.http.post<void>( return this.http.post<void>(
`/api/admin/gather/profile-data/${dataSource}/${symbol}`, `/api/admin/gather/profile-data/${dataSource}/${symbol}`,
{} {}
@ -70,10 +61,8 @@ export class AdminService {
dataSource, dataSource,
date, date,
symbol symbol
}: { }: UniqueAsset & {
dataSource: DataSource;
date?: Date; date?: Date;
symbol: string;
}) { }) {
let url = `/api/admin/gather/${dataSource}/${symbol}`; let url = `/api/admin/gather/${dataSource}/${symbol}`;

View File

@ -24,9 +24,11 @@ import {
PortfolioDetails, PortfolioDetails,
PortfolioInvestments, PortfolioInvestments,
PortfolioPerformance, PortfolioPerformance,
PortfolioPerformanceResponse,
PortfolioPublicDetails, PortfolioPublicDetails,
PortfolioReport, PortfolioReport,
PortfolioSummary, PortfolioSummary,
UniqueAsset,
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions'; import { permissions } from '@ghostfolio/common/permissions';
@ -188,13 +190,13 @@ export class DataService {
}); });
} }
public fetchPortfolioPerformance(aParams: { [param: string]: any }) { public fetchPortfolioPerformance(params: { [param: string]: any }) {
return this.http.get<{ return this.http.get<PortfolioPerformanceResponse>(
hasErrors: boolean; '/api/portfolio/performance',
performance: PortfolioPerformance; {
}>('/api/portfolio/performance', { params
params: aParams }
}); );
} }
public fetchPortfolioPublic(aId: string) { public fetchPortfolioPublic(aId: string) {

View File

@ -102,10 +102,6 @@ export function isCurrency(aSymbol = '') {
return currencies[aSymbol]; return currencies[aSymbol];
} }
export function isGhostfolioScraperApiSymbol(aSymbol = '') {
return aSymbol.startsWith(ghostfolioScraperApiSymbolPrefix);
}
export function resetHours(aDate: Date) { export function resetHours(aDate: Date) {
const year = getYear(aDate); const year = getYear(aDate);
const month = getMonth(aDate); const month = getMonth(aDate);

View File

@ -2,7 +2,7 @@ import { AccountWithValue } from '@ghostfolio/common/types';
export interface Accounts { export interface Accounts {
accounts: AccountWithValue[]; accounts: AccountWithValue[];
totalBalance: number; totalBalanceInBaseCurrency: number;
totalValue: number; totalValueInBaseCurrency: number;
transactionCount: number; transactionCount: number;
} }

View File

@ -1,5 +1,3 @@
import { Property } from '@prisma/client';
export interface AdminData { export interface AdminData {
dataGatheringProgress?: number; dataGatheringProgress?: number;
exchangeRates: { label1: string; label2: string; value: number }[]; exchangeRates: { label1: string; label2: string; value: number }[];

View File

@ -21,7 +21,10 @@ import { PortfolioReportRule } from './portfolio-report-rule.interface';
import { PortfolioReport } from './portfolio-report.interface'; import { PortfolioReport } from './portfolio-report.interface';
import { PortfolioSummary } from './portfolio-summary.interface'; import { PortfolioSummary } from './portfolio-summary.interface';
import { Position } from './position.interface'; import { Position } from './position.interface';
import { ResponseError } from './responses/errors.interface';
import { PortfolioPerformanceResponse } from './responses/portfolio-performance-response.interface';
import { TimelinePosition } from './timeline-position.interface'; import { TimelinePosition } from './timeline-position.interface';
import { UniqueAsset } from './unique-asset.interface';
import { UserSettings } from './user-settings.interface'; import { UserSettings } from './user-settings.interface';
import { UserWithSettings } from './user-with-settings'; import { UserWithSettings } from './user-with-settings';
import { User } from './user.interface'; import { User } from './user.interface';
@ -42,13 +45,16 @@ export {
PortfolioItem, PortfolioItem,
PortfolioOverview, PortfolioOverview,
PortfolioPerformance, PortfolioPerformance,
PortfolioPerformanceResponse,
PortfolioPosition, PortfolioPosition,
PortfolioPublicDetails, PortfolioPublicDetails,
PortfolioReport, PortfolioReport,
PortfolioReportRule, PortfolioReportRule,
PortfolioSummary, PortfolioSummary,
Position, Position,
ResponseError,
TimelinePosition, TimelinePosition,
UniqueAsset,
User, User,
UserSettings, UserSettings,
UserWithSettings UserWithSettings

View File

@ -0,0 +1,6 @@
import { UniqueAsset } from '../unique-asset.interface';
export interface ResponseError {
errors?: UniqueAsset[];
hasErrors: boolean;
}

View File

@ -0,0 +1,6 @@
import { PortfolioPerformance } from '../portfolio-performance.interface';
import { ResponseError } from './errors.interface';
export interface PortfolioPerformanceResponse extends ResponseError {
performance: PortfolioPerformance;
}

View File

@ -0,0 +1,6 @@
import { DataSource } from '@prisma/client';
export interface UniqueAsset {
dataSource: DataSource;
symbol: string;
}

View File

@ -1,7 +1,8 @@
import { Account as AccountModel } from '@prisma/client'; import { Account as AccountModel } from '@prisma/client';
export type AccountWithValue = AccountModel & { export type AccountWithValue = AccountModel & {
convertedBalance: number; balanceInBaseCurrency: number;
transactionCount: number; transactionCount: number;
value: number; value: number;
valueInBaseCurrency: number;
}; };

View File

@ -144,7 +144,7 @@
class="d-none d-lg-table-cell px-1" class="d-none d-lg-table-cell px-1"
mat-cell mat-cell
> >
{{ element.currency }} {{ element.SymbolProfile.currency }}
</td> </td>
<td *matFooterCellDef class="d-none d-lg-table-cell px-1" mat-footer-cell> <td *matFooterCellDef class="d-none d-lg-table-cell px-1" mat-footer-cell>
{{ baseCurrency }} {{ baseCurrency }}
@ -362,7 +362,7 @@
!row.isDraft && !row.isDraft &&
row.type !== 'ITEM' && row.type !== 'ITEM' &&
onOpenPositionDialog({ onOpenPositionDialog({
dataSource: row.dataSource, dataSource: row.SymbolProfile.dataSource,
symbol: row.SymbolProfile.symbol symbol: row.SymbolProfile.symbol
}) })
" "

View File

@ -21,6 +21,7 @@ import { MatTableDataSource } from '@angular/material/table';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config'; import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { OrderWithAccount } from '@ghostfolio/common/types'; import { OrderWithAccount } from '@ghostfolio/common/types';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import Big from 'big.js'; import Big from 'big.js';
@ -199,13 +200,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
this.import.emit(); this.import.emit();
} }
public onOpenPositionDialog({ public onOpenPositionDialog({ dataSource, symbol }: UniqueAsset): void {
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}): void {
this.router.navigate([], { this.router.navigate([], {
queryParams: { dataSource, symbol, positionDetailDialog: true } queryParams: { dataSource, symbol, positionDetailDialog: true }
}); });

View File

@ -3,14 +3,17 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
ElementRef, ElementRef,
EventEmitter,
Input, Input,
OnChanges, OnChanges,
OnDestroy, OnDestroy,
Output,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { UNKNOWN_KEY } from '@ghostfolio/common/config'; import { UNKNOWN_KEY } from '@ghostfolio/common/config';
import { getTextColor } from '@ghostfolio/common/helper'; import { getTextColor } from '@ghostfolio/common/helper';
import { PortfolioPosition } from '@ghostfolio/common/interfaces'; import { PortfolioPosition, UniqueAsset } from '@ghostfolio/common/interfaces';
import { DataSource } from '@prisma/client';
import Big from 'big.js'; import Big from 'big.js';
import { Tooltip } from 'chart.js'; import { Tooltip } from 'chart.js';
import { LinearScale } from 'chart.js'; import { LinearScale } from 'chart.js';
@ -30,6 +33,7 @@ export class PortfolioProportionChartComponent
implements AfterViewInit, OnChanges, OnDestroy implements AfterViewInit, OnChanges, OnDestroy
{ {
@Input() baseCurrency: string; @Input() baseCurrency: string;
@Input() cursor: string;
@Input() isInPercent = false; @Input() isInPercent = false;
@Input() keys: string[] = []; @Input() keys: string[] = [];
@Input() locale = ''; @Input() locale = '';
@ -37,11 +41,14 @@ export class PortfolioProportionChartComponent
@Input() showLabels = false; @Input() showLabels = false;
@Input() positions: { @Input() positions: {
[symbol: string]: Pick<PortfolioPosition, 'type'> & { [symbol: string]: Pick<PortfolioPosition, 'type'> & {
dataSource?: DataSource;
name: string; name: string;
value: number; value: number;
}; };
} = {}; } = {};
@Output() proportionChartClicked = new EventEmitter<UniqueAsset>();
@ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>; @ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>;
public chart: Chart; public chart: Chart;
@ -256,6 +263,21 @@ export class PortfolioProportionChartComponent
layout: { layout: {
padding: this.showLabels === true ? 100 : 0 padding: this.showLabels === true ? 100 : 0
}, },
onClick: (event, activeElements) => {
const dataIndex = activeElements[0].index;
const symbol: string = event.chart.data.labels[dataIndex];
const dataSource = this.positions[symbol]?.dataSource;
this.proportionChartClicked.emit({ dataSource, symbol });
},
onHover: (event, chartElement) => {
if (this.cursor) {
event.native.target.style.cursor = chartElement[0]
? this.cursor
: 'default';
}
},
plugins: { plugins: {
datalabels: { datalabels: {
color: (context) => { color: (context) => {

View File

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "1.121.0", "version": "1.123.0",
"homepage": "https://ghostfol.io", "homepage": "https://ghostfol.io",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Order" DROP COLUMN "dataSource";

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Order" DROP COLUMN "currency";

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Order" DROP COLUMN "symbol";

View File

@ -0,0 +1,5 @@
-- Set default value
UPDATE "SymbolProfile" SET "currency" = 'USD' WHERE "currency" IS NULL;
-- AlterTable
ALTER TABLE "SymbolProfile" ALTER COLUMN "currency" SET NOT NULL;

View File

@ -74,14 +74,11 @@ model Order {
accountId String? accountId String?
accountUserId String? accountUserId String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
currency String?
dataSource DataSource?
date DateTime date DateTime
fee Float fee Float
id String @default(uuid()) id String @default(uuid())
isDraft Boolean @default(false) isDraft Boolean @default(false)
quantity Float quantity Float
symbol String?
SymbolProfile SymbolProfile @relation(fields: [symbolProfileId], references: [id]) SymbolProfile SymbolProfile @relation(fields: [symbolProfileId], references: [id])
symbolProfileId String symbolProfileId String
type Type type Type
@ -119,7 +116,7 @@ model SymbolProfile {
assetSubClass AssetSubClass? assetSubClass AssetSubClass?
countries Json? countries Json?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
currency String? currency String
dataSource DataSource dataSource DataSource
id String @id @default(uuid()) id String @id @default(uuid())
name String? name String?

View File

@ -192,14 +192,11 @@ async function main() {
{ {
accountId: '65cfb79d-b6c7-4591-9d46-73426bc62094', accountId: '65cfb79d-b6c7-4591-9d46-73426bc62094',
accountUserId: userDemo.id, accountUserId: userDemo.id,
currency: 'USD',
dataSource: DataSource.YAHOO,
date: new Date(Date.UTC(2017, 0, 3, 0, 0, 0)), date: new Date(Date.UTC(2017, 0, 3, 0, 0, 0)),
fee: 30, fee: 30,
id: 'cf7c0418-8535-4089-ae3d-5dbfa0aec2e1', id: 'cf7c0418-8535-4089-ae3d-5dbfa0aec2e1',
quantity: 50, quantity: 50,
symbol: 'TSLA', symbolProfileId: 'd1ee9681-fb21-4f99-a3b7-afd4fc04df2e', // TSLA
symbolProfileId: 'd1ee9681-fb21-4f99-a3b7-afd4fc04df2e',
type: Type.BUY, type: Type.BUY,
unitPrice: 42.97, unitPrice: 42.97,
userId: userDemo.id userId: userDemo.id
@ -207,14 +204,11 @@ async function main() {
{ {
accountId: 'd804de69-0429-42dc-b6ca-b308fd7dd926', accountId: 'd804de69-0429-42dc-b6ca-b308fd7dd926',
accountUserId: userDemo.id, accountUserId: userDemo.id,
currency: 'USD',
dataSource: DataSource.YAHOO,
date: new Date(Date.UTC(2017, 7, 16, 0, 0, 0)), date: new Date(Date.UTC(2017, 7, 16, 0, 0, 0)),
fee: 29.9, fee: 29.9,
id: 'a1c5d73a-8631-44e5-ac44-356827a5212c', id: 'a1c5d73a-8631-44e5-ac44-356827a5212c',
quantity: 0.5614682, quantity: 0.5614682,
symbol: 'BTCUSD', symbolProfileId: 'fdc42ea6-1321-44f5-9fb0-d7f1f2cf9b1e', // BTCUSD
symbolProfileId: 'fdc42ea6-1321-44f5-9fb0-d7f1f2cf9b1e',
type: Type.BUY, type: Type.BUY,
unitPrice: 3562.089535970158, unitPrice: 3562.089535970158,
userId: userDemo.id userId: userDemo.id
@ -222,14 +216,11 @@ async function main() {
{ {
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c', accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
accountUserId: userDemo.id, accountUserId: userDemo.id,
currency: 'USD',
dataSource: DataSource.YAHOO,
date: new Date(Date.UTC(2018, 9, 1, 0, 0, 0)), date: new Date(Date.UTC(2018, 9, 1, 0, 0, 0)),
fee: 80.79, fee: 80.79,
id: '71c08e2a-4a86-44ae-a890-c337de5d5f9b', id: '71c08e2a-4a86-44ae-a890-c337de5d5f9b',
quantity: 5, quantity: 5,
symbol: 'AMZN', symbolProfileId: '2bd26362-136e-411c-b578-334084b4cdcc', // AMZN
symbolProfileId: '2bd26362-136e-411c-b578-334084b4cdcc',
type: Type.BUY, type: Type.BUY,
unitPrice: 2021.99, unitPrice: 2021.99,
userId: userDemo.id userId: userDemo.id
@ -237,14 +228,11 @@ async function main() {
{ {
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c', accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
accountUserId: userDemo.id, accountUserId: userDemo.id,
currency: 'USD',
dataSource: DataSource.YAHOO,
date: new Date(Date.UTC(2019, 2, 1, 0, 0, 0)), date: new Date(Date.UTC(2019, 2, 1, 0, 0, 0)),
fee: 19.9, fee: 19.9,
id: '385f2c2c-d53e-4937-b0e5-e92ef6020d4e', id: '385f2c2c-d53e-4937-b0e5-e92ef6020d4e',
quantity: 10, quantity: 10,
symbol: 'VTI', symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796', // VTI
symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796',
type: Type.BUY, type: Type.BUY,
unitPrice: 144.38, unitPrice: 144.38,
userId: userDemo.id userId: userDemo.id
@ -252,14 +240,11 @@ async function main() {
{ {
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c', accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
accountUserId: userDemo.id, accountUserId: userDemo.id,
currency: 'USD',
dataSource: DataSource.YAHOO,
date: new Date(Date.UTC(2019, 8, 3, 0, 0, 0)), date: new Date(Date.UTC(2019, 8, 3, 0, 0, 0)),
fee: 19.9, fee: 19.9,
id: '185f2c2c-d53e-4937-b0e5-a93ef6020d4e', id: '185f2c2c-d53e-4937-b0e5-a93ef6020d4e',
quantity: 10, quantity: 10,
symbol: 'VTI', symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796', // VTI
symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796',
type: Type.BUY, type: Type.BUY,
unitPrice: 147.99, unitPrice: 147.99,
userId: userDemo.id userId: userDemo.id
@ -267,14 +252,11 @@ async function main() {
{ {
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c', accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
accountUserId: userDemo.id, accountUserId: userDemo.id,
currency: 'USD',
dataSource: DataSource.YAHOO,
date: new Date(Date.UTC(2020, 2, 2, 0, 0, 0)), date: new Date(Date.UTC(2020, 2, 2, 0, 0, 0)),
fee: 19.9, fee: 19.9,
id: '347b0430-a84f-4031-a0f9-390399066ad6', id: '347b0430-a84f-4031-a0f9-390399066ad6',
quantity: 10, quantity: 10,
symbol: 'VTI', symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796', // VTI
symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796',
type: Type.BUY, type: Type.BUY,
unitPrice: 151.41, unitPrice: 151.41,
userId: userDemo.id userId: userDemo.id
@ -282,14 +264,11 @@ async function main() {
{ {
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c', accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
accountUserId: userDemo.id, accountUserId: userDemo.id,
currency: 'USD',
dataSource: DataSource.YAHOO,
date: new Date(Date.UTC(2020, 8, 1, 0, 0, 0)), date: new Date(Date.UTC(2020, 8, 1, 0, 0, 0)),
fee: 19.9, fee: 19.9,
id: '67ec3f47-3189-4b63-ba05-60d3a06b302f', id: '67ec3f47-3189-4b63-ba05-60d3a06b302f',
quantity: 10, quantity: 10,
symbol: 'VTI', symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796', // VTI
symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796',
type: Type.BUY, type: Type.BUY,
unitPrice: 177.69, unitPrice: 177.69,
userId: userDemo.id userId: userDemo.id
@ -297,14 +276,11 @@ async function main() {
{ {
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c', accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
accountUserId: userDemo.id, accountUserId: userDemo.id,
currency: 'USD',
dataSource: DataSource.YAHOO,
date: new Date(Date.UTC(2020, 2, 1, 0, 0, 0)), date: new Date(Date.UTC(2020, 2, 1, 0, 0, 0)),
fee: 19.9, fee: 19.9,
id: 'd01c6fbc-fa8d-47e6-8e80-66f882d2bfd2', id: 'd01c6fbc-fa8d-47e6-8e80-66f882d2bfd2',
quantity: 10, quantity: 10,
symbol: 'VTI', symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796', // VTI
symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796',
type: Type.BUY, type: Type.BUY,
unitPrice: 203.15, unitPrice: 203.15,
userId: userDemo.id userId: userDemo.id