Exclude accounts (#1289)

* Exclude accounts

* Update changelog
pull/1294/head
Thomas Kaul 2 years ago committed by GitHub
parent 72974e888f
commit f01a3f893d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -5,6 +5,12 @@ 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).
## Unreleased
### Added
- Added support to exclude an account from analysis
## 1.197.0 - 24.09.2022
### Added

@ -96,7 +96,9 @@ export class AccountController {
let accountsWithAggregations =
await this.portfolioService.getAccountsWithAggregations(
impersonationUserId || this.request.user.id
impersonationUserId || this.request.user.id,
undefined,
true
);
if (
@ -139,7 +141,8 @@ export class AccountController {
let accountsWithAggregations =
await this.portfolioService.getAccountsWithAggregations(
impersonationUserId || this.request.user.id,
[{ id, type: 'ACCOUNT' }]
[{ id, type: 'ACCOUNT' }],
true
);
if (

@ -107,15 +107,23 @@ export class AccountService {
public async getCashDetails({
currency,
filters = [],
userId
userId,
withExcludedAccounts = false
}: {
currency: string;
filters?: Filter[];
userId: string;
withExcludedAccounts?: boolean;
}): Promise<CashDetails> {
let totalCashBalanceInBaseCurrency = new Big(0);
const where: Prisma.AccountWhereInput = { userId };
const where: Prisma.AccountWhereInput = {
userId
};
if (withExcludedAccounts === false) {
where.isExcluded = false;
}
const {
ACCOUNT: filtersByAccount,

@ -1,5 +1,11 @@
import { AccountType } from '@prisma/client';
import { IsNumber, IsString, ValidateIf } from 'class-validator';
import {
IsBoolean,
IsNumber,
IsOptional,
IsString,
ValidateIf
} from 'class-validator';
export class CreateAccountDto {
@IsString()
@ -11,6 +17,10 @@ export class CreateAccountDto {
@IsString()
currency: string;
@IsBoolean()
@IsOptional()
isExcluded?: boolean;
@IsString()
name: string;

@ -1,5 +1,11 @@
import { AccountType } from '@prisma/client';
import { IsNumber, IsString, ValidateIf } from 'class-validator';
import {
IsBoolean,
IsNumber,
IsOptional,
IsString,
ValidateIf
} from 'class-validator';
export class UpdateAccountDto {
@IsString()
@ -14,6 +20,10 @@ export class UpdateAccountDto {
@IsString()
id: string;
@IsBoolean()
@IsOptional()
isExcluded?: boolean;
@IsString()
name: string;

@ -109,7 +109,8 @@ export class OrderController {
filters,
userCurrency,
includeDrafts: true,
userId: impersonationUserId || this.request.user.id
userId: impersonationUserId || this.request.user.id,
withExcludedAccounts: true
});
if (

@ -189,13 +189,15 @@ export class OrderService {
includeDrafts = false,
types,
userCurrency,
userId
userId,
withExcludedAccounts = false
}: {
filters?: Filter[];
includeDrafts?: boolean;
types?: TypeOfOrder[];
userCurrency: string;
userId: string;
withExcludedAccounts?: boolean;
}): Promise<Activity[]> {
const where: Prisma.OrderWhereInput = { userId };
@ -284,24 +286,28 @@ export class OrderService {
},
orderBy: { date: 'asc' }
})
).map((order) => {
const value = new Big(order.quantity).mul(order.unitPrice).toNumber();
return {
...order,
value,
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
order.fee,
order.SymbolProfile.currency,
userCurrency
),
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
)
.filter((order) => {
return withExcludedAccounts || order.Account?.isExcluded === false;
})
.map((order) => {
const value = new Big(order.quantity).mul(order.unitPrice).toNumber();
return {
...order,
value,
order.SymbolProfile.currency,
userCurrency
)
};
});
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
order.fee,
order.SymbolProfile.currency,
userCurrency
),
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
value,
order.SymbolProfile.currency,
userCurrency
)
};
});
}
public async updateOrder({

@ -148,12 +148,15 @@ export class PortfolioController {
})
];
let portfolioSummary: PortfolioSummary;
const {
accounts,
filteredValueInBaseCurrency,
filteredValueInPercentage,
hasErrors,
holdings,
summary,
totalValueInBaseCurrency
} = await this.portfolioService.getDetails(
impersonationId,
@ -166,6 +169,8 @@ export class PortfolioController {
hasError = true;
}
portfolioSummary = summary;
if (
impersonationId ||
this.userService.isRestrictedView(this.request.user)
@ -199,6 +204,22 @@ export class PortfolioController {
accounts[name].current = current / totalValue;
accounts[name].original = original / totalInvestment;
}
portfolioSummary = nullifyValuesInObject(summary, [
'cash',
'committedFunds',
'currentGrossPerformance',
'currentNetPerformance',
'currentValue',
'dividend',
'emergencyFund',
'excludedAccountsAndActivities',
'fees',
'items',
'netWorth',
'totalBuy',
'totalSell'
]);
}
let hasDetails = true;
@ -224,7 +245,8 @@ export class PortfolioController {
filteredValueInPercentage,
hasError,
holdings,
totalValueInBaseCurrency
totalValueInBaseCurrency,
summary: hasDetails ? portfolioSummary : undefined
};
}
@ -420,46 +442,6 @@ export class PortfolioController {
return portfolioPublicDetails;
}
@Get('summary')
@UseGuards(AuthGuard('jwt'))
public async getSummary(
@Headers('impersonation-id') impersonationId
): Promise<PortfolioSummary> {
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === 'Basic'
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
let summary = await this.portfolioService.getSummary(impersonationId);
if (
impersonationId ||
this.userService.isRestrictedView(this.request.user)
) {
summary = nullifyValuesInObject(summary, [
'cash',
'committedFunds',
'currentGrossPerformance',
'currentNetPerformance',
'currentValue',
'dividend',
'emergencyFund',
'fees',
'items',
'netWorth',
'totalBuy',
'totalSell'
]);
}
return summary;
}
@Get('position/:dataSource/:symbol')
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)

@ -50,8 +50,11 @@ import type {
import { Inject, Injectable } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import {
Account,
AssetClass,
DataSource,
Order,
Platform,
Prisma,
Tag,
Type as TypeOfOrder
@ -106,7 +109,8 @@ export class PortfolioService {
public async getAccounts(
aUserId: string,
aFilters?: Filter[]
aFilters?: Filter[],
withExcludedAccounts = false
): Promise<AccountWithValue[]> {
const where: Prisma.AccountWhereInput = { userId: aUserId };
@ -120,7 +124,13 @@ export class PortfolioService {
include: { Order: true, Platform: true },
orderBy: { name: 'asc' }
}),
this.getDetails(aUserId, aUserId, undefined, aFilters)
this.getDetails(
aUserId,
aUserId,
undefined,
aFilters,
withExcludedAccounts
)
]);
const userCurrency = this.request.user.Settings.settings.baseCurrency;
@ -160,9 +170,14 @@ export class PortfolioService {
public async getAccountsWithAggregations(
aUserId: string,
aFilters?: Filter[]
aFilters?: Filter[],
withExcludedAccounts = false
): Promise<Accounts> {
const accounts = await this.getAccounts(aUserId, aFilters);
const accounts = await this.getAccounts(
aUserId,
aFilters,
withExcludedAccounts
);
let totalBalanceInBaseCurrency = new Big(0);
let totalValueInBaseCurrency = new Big(0);
let transactionCount = 0;
@ -410,7 +425,8 @@ export class PortfolioService {
aImpersonationId: string,
aUserId: string,
aDateRange: DateRange = 'max',
aFilters?: Filter[]
aFilters?: Filter[],
withExcludedAccounts = false
): Promise<PortfolioDetails & { hasErrors: boolean }> {
const userId = await this.getUserId(aImpersonationId, aUserId);
const user = await this.userService.user({ id: userId });
@ -426,6 +442,7 @@ export class PortfolioService {
const { orders, portfolioOrders, transactionPoints } =
await this.getTransactionPoints({
userId,
withExcludedAccounts,
filters: aFilters
});
@ -580,6 +597,7 @@ export class PortfolioService {
portfolioItemsNow,
userCurrency,
userId,
withExcludedAccounts,
filters: aFilters
});
@ -588,6 +606,7 @@ export class PortfolioService {
return {
accounts,
holdings,
summary,
filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(),
filteredValueInPercentage: summary.netWorth
? filteredValueInBaseCurrency.div(summary.netWorth).toNumber()
@ -606,7 +625,11 @@ export class PortfolioService {
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
const orders = (
await this.orderService.getOrders({ userCurrency, userId })
await this.orderService.getOrders({
userCurrency,
userId,
withExcludedAccounts: true
})
).filter(({ SymbolProfile }) => {
return (
SymbolProfile.dataSource === aDataSource &&
@ -1181,74 +1204,6 @@ export class PortfolioService {
};
}
public async getSummary(aImpersonationId: string): Promise<PortfolioSummary> {
const userCurrency = this.request.user.Settings.settings.baseCurrency;
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
const user = await this.userService.user({ id: userId });
const performanceInformation = await this.getPerformance(aImpersonationId);
const { balanceInBaseCurrency } = await this.accountService.getCashDetails({
userId,
currency: userCurrency
});
const orders = await this.orderService.getOrders({
userCurrency,
userId
});
const dividend = this.getDividend(orders).toNumber();
const emergencyFund = new Big(
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
);
const fees = this.getFees(orders).toNumber();
const firstOrderDate = orders[0]?.date;
const items = this.getItems(orders).toNumber();
const totalBuy = this.getTotalByType(orders, userCurrency, 'BUY');
const totalSell = this.getTotalByType(orders, userCurrency, 'SELL');
const cash = new Big(balanceInBaseCurrency).minus(emergencyFund).toNumber();
const committedFunds = new Big(totalBuy).minus(totalSell);
const netWorth = new Big(balanceInBaseCurrency)
.plus(performanceInformation.performance.currentValue)
.plus(items)
.toNumber();
const daysInMarket = differenceInDays(new Date(), firstOrderDate);
const annualizedPerformancePercent = new PortfolioCalculator({
currency: userCurrency,
currentRateService: this.currentRateService,
orders: []
})
.getAnnualizedPerformancePercent({
daysInMarket,
netPerformancePercent: new Big(
performanceInformation.performance.currentNetPerformancePercent
)
})
?.toNumber();
return {
...performanceInformation.performance,
annualizedPerformancePercent,
cash,
dividend,
fees,
firstOrderDate,
items,
netWorth,
totalBuy,
totalSell,
committedFunds: committedFunds.toNumber(),
emergencyFund: emergencyFund.toNumber(),
ordersCount: orders.filter((order) => {
return order.type === 'BUY' || order.type === 'SELL';
}).length
};
}
private async getCashPositions({
cashDetails,
emergencyFund,
@ -1424,14 +1379,117 @@ export class PortfolioService {
return portfolioStart;
}
private async getSummary(
aImpersonationId: string
): Promise<PortfolioSummary> {
const userCurrency = this.request.user.Settings.settings.baseCurrency;
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
const user = await this.userService.user({ id: userId });
const performanceInformation = await this.getPerformance(aImpersonationId);
const { balanceInBaseCurrency } = await this.accountService.getCashDetails({
userId,
currency: userCurrency
});
const orders = await this.orderService.getOrders({
userCurrency,
userId
});
const excludedActivities = (
await this.orderService.getOrders({
userCurrency,
userId,
withExcludedAccounts: true
})
).filter(({ Account: account }) => {
return account?.isExcluded ?? false;
});
const dividend = this.getDividend(orders).toNumber();
const emergencyFund = new Big(
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
);
const fees = this.getFees(orders).toNumber();
const firstOrderDate = orders[0]?.date;
const items = this.getItems(orders).toNumber();
const totalBuy = this.getTotalByType(orders, userCurrency, 'BUY');
const totalSell = this.getTotalByType(orders, userCurrency, 'SELL');
const cash = new Big(balanceInBaseCurrency).minus(emergencyFund).toNumber();
const committedFunds = new Big(totalBuy).minus(totalSell);
const totalOfExcludedActivities = new Big(
this.getTotalByType(excludedActivities, userCurrency, 'BUY')
).minus(this.getTotalByType(excludedActivities, userCurrency, 'SELL'));
const cashDetailsWithExcludedAccounts =
await this.accountService.getCashDetails({
userId,
currency: userCurrency,
withExcludedAccounts: true
});
const excludedBalanceInBaseCurrency = new Big(
cashDetailsWithExcludedAccounts.balanceInBaseCurrency
).minus(balanceInBaseCurrency);
const excludedAccountsAndActivities = excludedBalanceInBaseCurrency
.plus(totalOfExcludedActivities)
.toNumber();
const netWorth = new Big(balanceInBaseCurrency)
.plus(performanceInformation.performance.currentValue)
.plus(items)
.plus(excludedAccountsAndActivities)
.toNumber();
const daysInMarket = differenceInDays(new Date(), firstOrderDate);
const annualizedPerformancePercent = new PortfolioCalculator({
currency: userCurrency,
currentRateService: this.currentRateService,
orders: []
})
.getAnnualizedPerformancePercent({
daysInMarket,
netPerformancePercent: new Big(
performanceInformation.performance.currentNetPerformancePercent
)
})
?.toNumber();
return {
...performanceInformation.performance,
annualizedPerformancePercent,
cash,
dividend,
excludedAccountsAndActivities,
fees,
firstOrderDate,
items,
netWorth,
totalBuy,
totalSell,
committedFunds: committedFunds.toNumber(),
emergencyFund: emergencyFund.toNumber(),
ordersCount: orders.filter((order) => {
return order.type === 'BUY' || order.type === 'SELL';
}).length
};
}
private async getTransactionPoints({
filters,
includeDrafts = false,
userId
userId,
withExcludedAccounts
}: {
filters?: Filter[];
includeDrafts?: boolean;
userId: string;
withExcludedAccounts?: boolean;
}): Promise<{
transactionPoints: TransactionPoint[];
orders: OrderWithAccount[];
@ -1445,6 +1503,7 @@ export class PortfolioService {
includeDrafts,
userCurrency,
userId,
withExcludedAccounts,
types: ['BUY', 'SELL']
});
@ -1496,17 +1555,22 @@ export class PortfolioService {
orders,
portfolioItemsNow,
userCurrency,
userId
userId,
withExcludedAccounts
}: {
filters?: Filter[];
orders: OrderWithAccount[];
portfolioItemsNow: { [p: string]: TimelinePosition };
userCurrency: string;
userId: string;
withExcludedAccounts?: boolean;
}) {
const accounts: PortfolioDetails['accounts'] = {};
let currentAccounts = [];
let currentAccounts: (Account & {
Order?: Order[];
Platform?: Platform;
})[] = [];
if (filters.length === 0) {
currentAccounts = await this.accountService.getAccounts(userId);
@ -1526,6 +1590,10 @@ export class PortfolioService {
});
}
currentAccounts = currentAccounts.filter((account) => {
return withExcludedAccounts || account.isExcluded === false;
});
for (const account of currentAccounts) {
const ordersByAccount = orders.filter(({ accountId }) => {
return accountId === account.id;

@ -61,7 +61,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
.subscribe(({ accountType, name, Platform, valueInBaseCurrency }) => {
this.accountType = accountType;
this.name = name;
this.platformName = Platform?.name;
this.platformName = Platform?.name ?? '-';
this.valueInBaseCurrency = valueInBaseCurrency;
this.changeDetectorRef.markForCheck();

@ -21,10 +21,12 @@
<div class="row">
<div class="col-6 mb-3">
<gf-value size="medium" [value]="accountType">Account Type</gf-value>
<gf-value i18n size="medium" [value]="accountType"
>Account Type</gf-value
>
</div>
<div class="col-6 mb-3">
<gf-value size="medium" [value]="platformName">Platform</gf-value>
<gf-value i18n size="medium" [value]="platformName">Platform</gf-value>
</div>
</div>

@ -1,8 +1,18 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import {
MatSnackBar,
MatSnackBarRef,
TextOnlySnackBar
} from '@angular/material/snack-bar';
import { Router } from '@angular/router';
import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { PortfolioSummary, User } from '@ghostfolio/common/interfaces';
import {
InfoItem,
PortfolioSummary,
User
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@ -14,8 +24,11 @@ import { takeUntil } from 'rxjs/operators';
})
export class HomeSummaryComponent implements OnDestroy, OnInit {
public hasImpersonationId: boolean;
public hasPermissionForSubscription: boolean;
public hasPermissionToUpdateUserSettings: boolean;
public info: InfoItem;
public isLoading = true;
public snackBarRef: MatSnackBarRef<TextOnlySnackBar>;
public summary: PortfolioSummary;
public user: User;
@ -25,8 +38,17 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private impersonationStorageService: ImpersonationStorageService,
private router: Router,
private snackBar: MatSnackBar,
private userService: UserService
) {
this.info = this.dataService.fetchInfo();
this.hasPermissionForSubscription = hasPermission(
this.info?.globalPermissions,
permissions.enableSubscription
);
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
@ -50,8 +72,6 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
.subscribe((aId) => {
this.hasImpersonationId = !!aId;
});
this.update();
}
public onChangeEmergencyFund(emergencyFund: number) {
@ -81,12 +101,30 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
this.isLoading = true;
this.dataService
.fetchPortfolioSummary()
.fetchPortfolioDetails({})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => {
this.summary = response;
.subscribe(({ summary }) => {
this.summary = summary;
this.isLoading = false;
if (!this.summary) {
this.snackBarRef = this.snackBar.open(
$localize`This feature requires a subscription.`,
this.hasPermissionForSubscription
? $localize`Upgrade Plan`
: undefined,
{ duration: 6000 }
);
this.snackBarRef.afterDismissed().subscribe(() => {
this.snackBarRef = undefined;
});
this.snackBarRef.onAction().subscribe(() => {
this.router.navigate(['/pricing']);
});
}
this.changeDetectorRef.markForCheck();
});

@ -172,6 +172,17 @@
></gf-value>
</div>
</div>
<div class="row px-3 py-1">
<div class="d-flex flex-grow-1" i18n>Excluded from Analysis</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
[currency]="baseCurrency"
[locale]="locale"
[value]="isLoading ? undefined : summary?.excludedAccountsAndActivities"
></gf-value>
</div>
</div>
<div class="row">
<div class="col"><hr /></div>
</div>

@ -59,8 +59,8 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
this.openCreateAccountDialog();
} else if (params['editDialog']) {
if (this.accounts) {
const account = this.accounts.find((account) => {
return account.id === params['accountId'];
const account = this.accounts.find(({ id }) => {
return id === params['accountId'];
});
this.openUpdateAccountDialog(account);
@ -155,6 +155,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
balance,
currency,
id,
isExcluded,
name,
platformId
}: AccountModel): void {
@ -165,6 +166,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
balance,
currency,
id,
isExcluded,
name,
platformId
}
@ -231,6 +233,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
accountType: AccountType.SECURITIES,
balance: 0,
currency: this.user?.settings?.baseCurrency,
isExcluded: false,
name: null,
platformId: null
}

@ -50,6 +50,14 @@
</mat-select>
</mat-form-field>
</div>
<div class="mb-3 px-2">
<mat-checkbox
color="primary"
name="isExcluded"
[(ngModel)]="data.account.isExcluded"
>Exclude from Analysis</mat-checkbox
>
</div>
<div *ngIf="data.account.id">
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Account ID</mat-label>

@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
@ -15,6 +16,7 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.c
CommonModule,
FormsModule,
MatButtonModule,
MatCheckboxModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule,

@ -37,14 +37,14 @@ export class FirePageComponent implements OnDestroy, OnInit {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
this.dataService
.fetchPortfolioSummary()
.fetchPortfolioDetails({})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ cash, currentValue }) => {
if (cash === null || currentValue === null) {
.subscribe(({ summary }) => {
if (summary.cash === null || summary.currentValue === null) {
return;
}
this.fireWealth = new Big(currentValue);
this.fireWealth = new Big(summary.currentValue);
this.withdrawalRatePerYear = this.fireWealth.mul(4).div(100);
this.withdrawalRatePerMonth = this.withdrawalRatePerYear.div(12);

@ -31,7 +31,6 @@ import {
PortfolioPerformanceResponse,
PortfolioPublicDetails,
PortfolioReport,
PortfolioSummary,
UniqueAsset,
User
} from '@ghostfolio/common/interfaces';
@ -302,7 +301,11 @@ export class DataService {
);
}
public fetchPortfolioDetails({ filters }: { filters?: Filter[] }) {
public fetchPortfolioDetails({
filters
}: {
filters?: Filter[];
}): Observable<PortfolioDetails> {
let params = new HttpParams();
if (filters?.length > 0) {
@ -348,9 +351,20 @@ export class DataService {
}
}
return this.http.get<PortfolioDetails>('/api/v1/portfolio/details', {
params
});
return this.http
.get<any>('/api/v1/portfolio/details', {
params
})
.pipe(
map((response) => {
if (response.summary?.firstOrderDate) {
response.summary.firstOrderDate = parseISO(
response.summary.firstOrderDate
);
}
return response;
})
);
}
public fetchPortfolioPerformance({
@ -376,18 +390,6 @@ export class DataService {
return this.http.get<PortfolioReport>('/api/v1/portfolio/report');
}
public fetchPortfolioSummary(): Observable<PortfolioSummary> {
return this.http.get<any>('/api/v1/portfolio/summary').pipe(
map((summary) => {
if (summary.firstOrderDate) {
summary.firstOrderDate = parseISO(summary.firstOrderDate);
}
return summary;
})
);
}
public fetchPositionDetail({
dataSource,
symbol

@ -1,4 +1,7 @@
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
import {
PortfolioPosition,
PortfolioSummary
} from '@ghostfolio/common/interfaces';
export interface PortfolioDetails {
accounts: {
@ -13,5 +16,6 @@ export interface PortfolioDetails {
filteredValueInBaseCurrency?: number;
filteredValueInPercentage: number;
holdings: { [symbol: string]: PortfolioPosition };
summary: PortfolioSummary;
totalValueInBaseCurrency?: number;
}

@ -3,9 +3,10 @@ import { PortfolioPerformance } from './portfolio-performance.interface';
export interface PortfolioSummary extends PortfolioPerformance {
annualizedPerformancePercent: number;
cash: number;
dividend: number;
committedFunds: number;
dividend: number;
emergencyFund: number;
excludedAccountsAndActivities: number;
fees: number;
firstOrderDate: Date;
items: number;

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Account" ADD COLUMN "isExcluded" BOOLEAN NOT NULL DEFAULT false;

@ -27,6 +27,7 @@ model Account {
currency String?
id String @default(uuid())
isDefault Boolean @default(false)
isExcluded Boolean @default(false)
name String?
platformId String?
updatedAt DateTime @updatedAt

Loading…
Cancel
Save