Compare commits

...

19 Commits

Author SHA1 Message Date
80464c7846 Release 2.80.0 (#3386) 2024-05-08 20:55:39 +02:00
74f4323903 Feature/increase spacing around floating action buttons (#3385)
* Increase spacing around floating action buttons

* Update changelog
2024-05-08 20:54:13 +02:00
127dbf9dcd Update translations (#3384) 2024-05-08 20:37:53 +02:00
66bdb374e8 Feature/set icon columns of tables to stick at beginning (#3377)
* Set icon columns to stick at the beginning

* Update changelog
2024-05-08 20:04:58 +02:00
4ad4fa2b30 Feature/clean up deprecated GET api/portfolio/positions endpoint (#3373) 2024-05-08 20:04:32 +02:00
1fd836194f Feature/add absolute change column to holdings table (#3378)
* Add absolute change column

* Update changelog
2024-05-08 20:02:50 +02:00
2090db1199 Feature/increase number of attempts of queue jobs (#3376)
* Increase number of attempts

* Update changelog
2024-05-07 20:48:02 +02:00
053c7e591e Feature/upgrade ionicons to version 7.4.0 (#3356)
* Upgrade ionicons to version 7.4.0

* Update changelog
2024-05-07 19:00:55 +02:00
9b5e350e3b Feature/harmonize log message (#3343) 2024-05-06 17:03:58 +02:00
378e57c3bc Feature/add links to Home Assistant add-on (#3367)
* Add links to Home Assistant add-on
2024-05-06 17:02:18 +02:00
6765191a8c Bugfix/fix position detail dialog open in holding search of assistant (#3374)
* Open position detail dialog (via holding search of assistant)

* Update changelog
2024-05-05 09:09:57 +02:00
8438a45bcf Update changelog (#3372) 2024-05-04 16:32:32 +02:00
30a64e7fc1 Release 2.79.0 (#3371) 2024-05-04 15:54:29 +02:00
f2cb671c7f Feature/optimize get porfolio details endpoint (#3366)
* Eliminate getPerformance() from getSummary() function

* Disable cache for getDetails()

* Add hint to portfolio summary

* Update changelog
2024-05-04 15:53:02 +02:00
3f41e5c5de Bugfix/fix locale in markets overview (#3369)
* Fix locale if no user is logged in

* Update changelog
2024-05-04 15:51:09 +02:00
c1ad483f33 Improve alignment (#3370) 2024-05-04 15:50:47 +02:00
f3d961bc16 Feature/move holdings table to holdings tab of home page (#3368)
* Move holdings table to holdings tab of home page

* Deprecate api/v1/portfolio/positions endpoint

* Update changelog
2024-05-04 14:11:37 +02:00
42b70ef568 Feature/improve performance labels in position detail dialog (#3363)
* Improve performance labels (with and without currency effects)

* Update changelog
2024-05-04 07:49:37 +02:00
77beaaba08 Refactoring portfolio service (#3365) 2024-05-03 21:48:46 +02:00
71 changed files with 2775 additions and 3082 deletions

View File

@ -5,6 +5,37 @@ 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).
## 2.80.0 - 2024-05-08
### Added
- Added the absolute change column to the holdings table on the home page
### Changed
- Increased the spacing around the floating action buttons (FAB)
- Set the icon column of the activities table to stick at the beginning
- Set the icon column of the holdings table to stick at the beginning
- Increased the number of attempts of queue jobs from `10` to `12` (fail later)
- Upgraded `ionicons` from version `7.3.0` to `7.4.0`
### Fixed
- Fixed the position detail dialog open functionality when searching for a holding in the assistant
## 2.79.0 - 2024-05-04
### Changed
- Moved the holdings table to the holdings tab of the home page
- Improved the performance labels (with and without currency effects) in the position detail dialog
- Optimized the calculations of the portfolio details endpoint
### Fixed
- Fixed an issue with the benchmarks in the markets overview
- Fixed an issue with the _Fear & Greed Index_ (market mood) in the markets overview
## 2.78.0 - 2024-05-02
### Added

View File

@ -142,7 +142,7 @@ docker compose --env-file ./.env -f docker/docker-compose.build.yml up -d
### Home Server Systems (Community)
Ghostfolio is available for various home server systems, including [CasaOS](https://github.com/bigbeartechworld/big-bear-casaos), Home Assistant, [Runtipi](https://www.runtipi.io/docs/apps-available), [TrueCharts](https://truecharts.org/charts/stable/ghostfolio), [Umbrel](https://apps.umbrel.com/app/ghostfolio), and [Unraid](https://unraid.net/community/apps?q=ghostfolio).
Ghostfolio is available for various home server systems, including [CasaOS](https://github.com/bigbeartechworld/big-bear-casaos), [Home Assistant](https://github.com/lildude/ha-addon-ghostfolio), [Runtipi](https://www.runtipi.io/docs/apps-available), [TrueCharts](https://truecharts.org/charts/stable/ghostfolio), [Umbrel](https://apps.umbrel.com/app/ghostfolio), and [Unraid](https://unraid.net/community/apps?q=ghostfolio).
## Development

View File

@ -1,5 +0,0 @@
import { Position } from '@ghostfolio/common/interfaces';
export interface PortfolioPositions {
positions: Position[];
}

View File

@ -52,7 +52,6 @@ import { Big } from 'big.js';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface';
import { PortfolioPositions } from './interfaces/portfolio-positions.interface';
import { PortfolioService } from './portfolio.service';
@Controller('portfolio')
@ -165,21 +164,21 @@ export class PortfolioController {
portfolioSummary = nullifyValuesInObject(summary, [
'cash',
'committedFunds',
'currentGrossPerformance',
'currentGrossPerformanceWithCurrencyEffect',
'currentNetPerformance',
'currentNetPerformanceWithCurrencyEffect',
'currentNetWorth',
'currentValue',
'currentValueInBaseCurrency',
'dividendInBaseCurrency',
'emergencyFund',
'excludedAccountsAndActivities',
'fees',
'filteredValueInBaseCurrency',
'fireWealth',
'grossPerformance',
'grossPerformanceWithCurrencyEffect',
'interest',
'items',
'liabilities',
'netPerformance',
'netPerformanceWithCurrencyEffect',
'totalBuy',
'totalInvestment',
'totalSell',
@ -449,10 +448,14 @@ export class PortfolioController {
.div(performanceInformation.performance.totalInvestment)
.toNumber(),
valueInPercentage:
performanceInformation.performance.currentValue === 0
performanceInformation.performance.currentValueInBaseCurrency ===
0
? 0
: new Big(value)
.div(performanceInformation.performance.currentValue)
.div(
performanceInformation.performance
.currentValueInBaseCurrency
)
.toNumber()
};
}
@ -461,12 +464,12 @@ export class PortfolioController {
performanceInformation.performance = nullifyValuesInObject(
performanceInformation.performance,
[
'currentGrossPerformance',
'currentGrossPerformanceWithCurrencyEffect',
'currentNetPerformance',
'currentNetPerformanceWithCurrencyEffect',
'currentNetWorth',
'currentValue',
'currentValueInBaseCurrency',
'grossPerformance',
'grossPerformanceWithCurrencyEffect',
'netPerformance',
'netPerformanceWithCurrencyEffect',
'totalInvestment'
]
);
@ -483,39 +486,13 @@ export class PortfolioController {
);
performanceInformation.performance = nullifyValuesInObject(
performanceInformation.performance,
['currentNetPerformance', 'currentNetPerformancePercent']
['netPerformance']
);
}
return performanceInformation;
}
@Get('positions')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(RedactValuesInResponseInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getPositions(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('query') filterBySearchQuery?: string,
@Query('range') dateRange: DateRange = 'max',
@Query('tags') filterByTags?: string
): Promise<PortfolioPositions> {
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
filterBySearchQuery,
filterByTags
});
return this.portfolioService.getPositions({
dateRange,
filters,
impersonationId
});
}
@Get('public/:accessId')
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getPublic(

View File

@ -27,7 +27,7 @@ describe('PortfolioService', () => {
portfolioService
.getAnnualizedPerformancePercent({
daysInMarket: NaN, // differenceInDays of date-fns returns NaN for the same day
netPerformancePercent: new Big(0)
netPerformancePercentage: new Big(0)
})
.toNumber()
).toEqual(0);
@ -36,7 +36,7 @@ describe('PortfolioService', () => {
portfolioService
.getAnnualizedPerformancePercent({
daysInMarket: 0,
netPerformancePercent: new Big(0)
netPerformancePercentage: new Big(0)
})
.toNumber()
).toEqual(0);
@ -48,7 +48,7 @@ describe('PortfolioService', () => {
portfolioService
.getAnnualizedPerformancePercent({
daysInMarket: 65, // < 1 year
netPerformancePercent: new Big(0.1025)
netPerformancePercentage: new Big(0.1025)
})
.toNumber()
).toBeCloseTo(0.729705);
@ -57,7 +57,7 @@ describe('PortfolioService', () => {
portfolioService
.getAnnualizedPerformancePercent({
daysInMarket: 365, // 1 year
netPerformancePercent: new Big(0.05)
netPerformancePercentage: new Big(0.05)
})
.toNumber()
).toBeCloseTo(0.05);
@ -69,7 +69,7 @@ describe('PortfolioService', () => {
portfolioService
.getAnnualizedPerformancePercent({
daysInMarket: 575, // > 1 year
netPerformancePercent: new Big(0.2374)
netPerformancePercentage: new Big(0.2374)
})
.toNumber()
).toBeCloseTo(0.145);

View File

@ -208,16 +208,16 @@ export class PortfolioService {
public getAnnualizedPerformancePercent({
daysInMarket,
netPerformancePercent
netPerformancePercentage
}: {
daysInMarket: number;
netPerformancePercent: Big;
netPerformancePercentage: Big;
}): Big {
if (isNumber(daysInMarket) && daysInMarket > 0) {
const exponent = new Big(365).div(daysInMarket).toNumber();
return new Big(
Math.pow(netPerformancePercent.plus(1).toNumber(), exponent)
Math.pow(netPerformancePercentage.plus(1).toNumber(), exponent)
).minus(1);
}
@ -360,7 +360,7 @@ export class PortfolioService {
userId,
calculationType: PerformanceCalculationType.TWR,
currency: userCurrency,
hasFilters: filters?.length > 0,
hasFilters: true, // disable cache
isExperimentalFeatures:
this.request.user?.Settings.settings.isExperimentalFeatures
});
@ -704,7 +704,7 @@ export class PortfolioService {
const dividendYieldPercent = this.getAnnualizedPerformancePercent({
daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)),
netPerformancePercent: timeWeightedInvestment.eq(0)
netPerformancePercentage: timeWeightedInvestment.eq(0)
? new Big(0)
: dividendInBaseCurrency.div(timeWeightedInvestment)
});
@ -712,7 +712,9 @@ export class PortfolioService {
const dividendYieldPercentWithCurrencyEffect =
this.getAnnualizedPerformancePercent({
daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)),
netPerformancePercent: timeWeightedInvestmentWithCurrencyEffect.eq(0)
netPerformancePercentage: timeWeightedInvestmentWithCurrencyEffect.eq(
0
)
? new Big(0)
: dividendInBaseCurrency.div(
timeWeightedInvestmentWithCurrencyEffect
@ -1108,16 +1110,16 @@ export class PortfolioService {
firstOrderDate: undefined,
hasErrors: false,
performance: {
currentGrossPerformance: 0,
currentGrossPerformancePercent: 0,
currentGrossPerformancePercentWithCurrencyEffect: 0,
currentGrossPerformanceWithCurrencyEffect: 0,
currentNetPerformance: 0,
currentNetPerformancePercent: 0,
currentNetPerformancePercentWithCurrencyEffect: 0,
currentNetPerformanceWithCurrencyEffect: 0,
currentNetWorth: 0,
currentValue: 0,
currentValueInBaseCurrency: 0,
grossPerformance: 0,
grossPerformancePercentage: 0,
grossPerformancePercentageWithCurrencyEffect: 0,
grossPerformanceWithCurrencyEffect: 0,
netPerformance: 0,
netPerformancePercentage: 0,
netPerformancePercentageWithCurrencyEffect: 0,
netPerformanceWithCurrencyEffect: 0,
totalInvestment: 0
}
};
@ -1152,9 +1154,9 @@ export class PortfolioService {
let currentNetPerformance = netPerformance;
let currentNetPerformancePercent = netPerformancePercentage;
let currentNetPerformancePercentage = netPerformancePercentage;
let currentNetPerformancePercentWithCurrencyEffect =
let currentNetPerformancePercentageWithCurrencyEffect =
netPerformancePercentageWithCurrencyEffect;
let currentNetPerformanceWithCurrencyEffect =
@ -1173,11 +1175,11 @@ export class PortfolioService {
if (itemOfToday) {
currentNetPerformance = new Big(itemOfToday.netPerformance);
currentNetPerformancePercent = new Big(
currentNetPerformancePercentage = new Big(
itemOfToday.netPerformanceInPercentage
).div(100);
currentNetPerformancePercentWithCurrencyEffect = new Big(
currentNetPerformancePercentageWithCurrencyEffect = new Big(
itemOfToday.netPerformanceInPercentageWithCurrencyEffect
).div(100);
@ -1195,19 +1197,19 @@ export class PortfolioService {
firstOrderDate: parseDate(items[0]?.date),
performance: {
currentNetWorth,
currentGrossPerformance: grossPerformance.toNumber(),
currentGrossPerformancePercent: grossPerformancePercentage.toNumber(),
currentGrossPerformancePercentWithCurrencyEffect:
currentValueInBaseCurrency: currentValueInBaseCurrency.toNumber(),
grossPerformance: grossPerformance.toNumber(),
grossPerformancePercentage: grossPerformancePercentage.toNumber(),
grossPerformancePercentageWithCurrencyEffect:
grossPerformancePercentageWithCurrencyEffect.toNumber(),
currentGrossPerformanceWithCurrencyEffect:
grossPerformanceWithCurrencyEffect:
grossPerformanceWithCurrencyEffect.toNumber(),
currentNetPerformance: currentNetPerformance.toNumber(),
currentNetPerformancePercent: currentNetPerformancePercent.toNumber(),
currentNetPerformancePercentWithCurrencyEffect:
currentNetPerformancePercentWithCurrencyEffect.toNumber(),
currentNetPerformanceWithCurrencyEffect:
netPerformance: currentNetPerformance.toNumber(),
netPerformancePercentage: currentNetPerformancePercentage.toNumber(),
netPerformancePercentageWithCurrencyEffect:
currentNetPerformancePercentageWithCurrencyEffect.toNumber(),
netPerformanceWithCurrencyEffect:
currentNetPerformanceWithCurrencyEffect.toNumber(),
currentValue: currentValueInBaseCurrency.toNumber(),
totalInvestment: totalInvestment.toNumber()
}
};
@ -1604,11 +1606,6 @@ export class PortfolioService {
userId = await this.getUserId(impersonationId, userId);
const user = await this.userService.user({ id: userId });
const performanceInformation = await this.getPerformance({
impersonationId,
userId
});
const { activities } = await this.orderService.getOrders({
userCurrency,
userId,
@ -1626,6 +1623,19 @@ export class PortfolioService {
}
}
const {
currentValueInBaseCurrency,
grossPerformance,
grossPerformancePercentage,
grossPerformancePercentageWithCurrencyEffect,
grossPerformanceWithCurrencyEffect,
netPerformance,
netPerformancePercentage,
netPerformancePercentageWithCurrencyEffect,
netPerformanceWithCurrencyEffect,
totalInvestment
} = await portfolioCalculator.getSnapshot();
const dividendInBaseCurrency =
await portfolioCalculator.getDividendInBaseCurrency();
@ -1694,7 +1704,7 @@ export class PortfolioService {
.toNumber();
const netWorth = new Big(balanceInBaseCurrency)
.plus(performanceInformation.performance.currentValue)
.plus(currentValueInBaseCurrency)
.plus(valuables)
.plus(excludedAccountsAndActivities)
.minus(liabilities)
@ -1704,21 +1714,18 @@ export class PortfolioService {
const annualizedPerformancePercent = this.getAnnualizedPerformancePercent({
daysInMarket,
netPerformancePercent: new Big(
performanceInformation.performance.currentNetPerformancePercent
)
netPerformancePercentage: new Big(netPerformancePercentage)
})?.toNumber();
const annualizedPerformancePercentWithCurrencyEffect =
this.getAnnualizedPerformancePercent({
daysInMarket,
netPerformancePercent: new Big(
performanceInformation.performance.currentNetPerformancePercentWithCurrencyEffect
netPerformancePercentage: new Big(
netPerformancePercentageWithCurrencyEffect
)
})?.toNumber();
return {
...performanceInformation.performance,
annualizedPerformancePercent,
annualizedPerformancePercentWithCurrencyEffect,
cash,
@ -1727,6 +1734,7 @@ export class PortfolioService {
totalBuy,
totalSell,
committedFunds: committedFunds.toNumber(),
currentValueInBaseCurrency: currentValueInBaseCurrency.toNumber(),
dividendInBaseCurrency: dividendInBaseCurrency.toNumber(),
emergencyFund: {
assets: emergencyFundPositionsValueInBaseCurrency,
@ -1740,15 +1748,28 @@ export class PortfolioService {
filteredValueInPercentage: netWorth
? filteredValueInBaseCurrency.div(netWorth).toNumber()
: undefined,
fireWealth: new Big(performanceInformation.performance.currentValue)
fireWealth: new Big(currentValueInBaseCurrency)
.minus(emergencyFundPositionsValueInBaseCurrency)
.toNumber(),
grossPerformance: grossPerformance.toNumber(),
grossPerformancePercentage: grossPerformancePercentage.toNumber(),
grossPerformancePercentageWithCurrencyEffect:
grossPerformancePercentageWithCurrencyEffect.toNumber(),
grossPerformanceWithCurrencyEffect:
grossPerformanceWithCurrencyEffect.toNumber(),
interest: interest.toNumber(),
items: valuables.toNumber(),
liabilities: liabilities.toNumber(),
netPerformance: netPerformance.toNumber(),
netPerformancePercentage: netPerformancePercentage.toNumber(),
netPerformancePercentageWithCurrencyEffect:
netPerformancePercentageWithCurrencyEffect.toNumber(),
netPerformanceWithCurrencyEffect:
netPerformanceWithCurrencyEffect.toNumber(),
ordersCount: activities.filter(({ type }) => {
return type === 'BUY' || type === 'SELL';
return ['BUY', 'SELL'].includes(type);
}).length,
totalInvestment: totalInvestment.toNumber(),
totalValueInBaseCurrency: netWorth
};
}

View File

@ -12,7 +12,7 @@ export class PortfolioChangedListener {
@OnEvent(PortfolioChangedEvent.getName())
handlePortfolioChangedEvent(event: PortfolioChangedEvent) {
Logger.log(
`Portfolio of user with id ${event.getUserId()} has changed`,
`Portfolio of user '${event.getUserId()}' has changed`,
'PortfolioChangedListener'
);

View File

@ -38,6 +38,7 @@ import { CreateAssetProfileDialogParams } from './create-asset-profile-dialog/in
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'has-fab' },
selector: 'gf-admin-market-data',
styleUrls: ['./admin-market-data.scss'],
templateUrl: './admin-market-data.html'

View File

@ -1,11 +1,11 @@
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component';
import { PositionDetailDialogParams } from '@ghostfolio/client/components/position-detail-dialog/interfaces/interfaces';
import { PositionDetailDialog } from '@ghostfolio/client/components/position-detail-dialog/position-detail-dialog.component';
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 { Position, User } from '@ghostfolio/common/interfaces';
import { PortfolioPosition, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { DateRange } from '@ghostfolio/common/types';
import { HoldingType, ToggleOption } from '@ghostfolio/common/types';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
@ -15,19 +15,21 @@ import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { PositionDetailDialogParams } from '../position/position-detail-dialog/interfaces/interfaces';
@Component({
selector: 'gf-home-holdings',
styleUrls: ['./home-holdings.scss'],
templateUrl: './home-holdings.html'
})
export class HomeHoldingsComponent implements OnDestroy, OnInit {
public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS;
public deviceType: string;
public hasImpersonationId: boolean;
public hasPermissionToCreateOrder: boolean;
public positions: Position[];
public holdings: PortfolioPosition[];
public holdingType: HoldingType = 'ACTIVE';
public holdingTypeOptions: ToggleOption[] = [
{ label: $localize`Active`, value: 'ACTIVE' },
{ label: $localize`Closed`, value: 'CLOSED' }
];
public user: User;
private unsubscribeSubject = new Subject<void>();
@ -56,6 +58,17 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
});
}
});
}
public ngOnInit() {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
this.impersonationStorageService
.onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId;
});
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
@ -68,37 +81,32 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
permissions.createOrder
);
this.update();
this.holdings = undefined;
this.fetchHoldings()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ holdings }) => {
this.holdings = holdings;
this.changeDetectorRef.markForCheck();
});
this.changeDetectorRef.markForCheck();
}
});
}
public ngOnInit() {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
public onChangeHoldingType(aHoldingType: HoldingType) {
this.holdingType = aHoldingType;
this.impersonationStorageService
.onChangeHasImpersonation()
this.holdings = undefined;
this.fetchHoldings()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId;
});
}
.subscribe(({ holdings }) => {
this.holdings = holdings;
public onChangeDateRange(dateRange: DateRange) {
this.dataService
.putUserSetting({ dateRange })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.userService.remove();
this.userService
.get()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((user) => {
this.user = user;
this.changeDetectorRef.markForCheck();
});
this.changeDetectorRef.markForCheck();
});
}
@ -107,6 +115,19 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
this.unsubscribeSubject.complete();
}
private fetchHoldings() {
const filters = this.userService.getFilters();
if (this.holdingType === 'CLOSED') {
filters.push({ id: 'CLOSED', type: 'HOLDING_TYPE' });
}
return this.dataService.fetchPortfolioHoldings({
filters,
range: this.user?.settings?.dateRange
});
}
private openPositionDialog({
dataSource,
symbol
@ -147,19 +168,4 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
});
});
}
private update() {
this.positions = undefined;
this.dataService
.fetchPositions({ range: this.user?.settings?.dateRange })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ positions }) => {
this.positions = positions;
this.changeDetectorRef.markForCheck();
});
this.changeDetectorRef.markForCheck();
}
}

View File

@ -1,27 +1,38 @@
<div class="container justify-content-center p-3">
<div class="container">
<div class="row">
<div class="align-items-center col-xs-12 col-md-8 offset-md-2">
<mat-card appearance="outlined">
<mat-card-content class="p-0">
<gf-positions
[baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType"
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
[locale]="user?.settings?.locale"
[positions]="positions"
[range]="user?.settings?.dateRange"
/>
</mat-card-content>
</mat-card>
<div *ngIf="hasPermissionToCreateOrder" class="text-center">
<a
class="mt-3"
i18n
mat-stroked-button
[routerLink]="['/portfolio', 'activities']"
>Manage Activities</a
>
<div class="col">
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Holdings</h1>
</div>
</div>
<div class="row">
<div class="col-lg">
<div class="d-flex justify-content-end">
<gf-toggle
class="d-none d-lg-block"
[defaultValue]="holdingType"
[isLoading]="false"
[options]="holdingTypeOptions"
(change)="onChangeHoldingType($event.value)"
/>
</div>
<gf-holdings-table
[baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType"
[hasPermissionToCreateActivity]="hasPermissionToCreateOrder"
[holdings]="holdings"
[locale]="user?.settings?.locale"
/>
@if (hasPermissionToCreateOrder && holdings?.length > 0) {
<div class="text-center">
<a
class="mt-3"
i18n
mat-stroked-button
[routerLink]="['/portfolio', 'activities']"
>Manage Activities</a
>
</div>
}
</div>
</div>
</div>

View File

@ -1,11 +1,9 @@
import { GfPositionDetailDialogModule } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.module';
import { GfPositionsModule } from '@ghostfolio/client/components/positions/positions.module';
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
import { GfHoldingsTableComponent } from '@ghostfolio/ui/holdings-table';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { RouterModule } from '@angular/router';
import { HomeHoldingsComponent } from './home-holdings.component';
@ -14,11 +12,9 @@ import { HomeHoldingsComponent } from './home-holdings.component';
declarations: [HomeHoldingsComponent],
imports: [
CommonModule,
GfPositionDetailDialogModule,
GfPositionsModule,
GfHoldingsTableComponent,
GfToggleModule,
MatButtonModule,
MatCardModule,
RouterModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]

View File

@ -11,7 +11,7 @@
[colorScheme]="user?.settings?.colorScheme"
[historicalDataItems]="historicalDataItems"
[isAnimated]="true"
[locale]="user?.settings?.locale"
[locale]="user?.settings?.locale || undefined"
[showXAxis]="true"
[showYAxis]="true"
[yMax]="100"
@ -30,7 +30,7 @@
<div class="col-xs-12 col-md-8 offset-md-2">
<gf-benchmark
[benchmarks]="benchmarks"
[locale]="user?.settings?.locale"
[locale]="user?.settings?.locale || undefined"
[user]="user"
/>
<ngx-skeleton-loader

View File

@ -41,9 +41,7 @@
[isCurrency]="true"
[locale]="locale"
[value]="
isLoading
? undefined
: performance?.currentNetPerformanceWithCurrencyEffect
isLoading ? undefined : performance?.netPerformanceWithCurrencyEffect
"
/>
</div>
@ -55,7 +53,7 @@
[value]="
isLoading
? undefined
: performance?.currentNetPerformancePercentWithCurrencyEffect
: performance?.netPerformancePercentageWithCurrencyEffect
"
/>
</div>

View File

@ -49,12 +49,12 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
this.value.nativeElement.innerHTML = '';
}
} else {
if (isNumber(this.performance?.currentValue)) {
new CountUp('value', this.performance?.currentValue, {
if (isNumber(this.performance?.currentValueInBaseCurrency)) {
new CountUp('value', this.performance?.currentValueInBaseCurrency, {
decimal: getNumberFormatDecimal(this.locale),
decimalPlaces:
this.deviceType === 'mobile' &&
this.performance?.currentValue >= 100000
this.performance?.currentValueInBaseCurrency >= 100000
? 0
: 2,
duration: 1,
@ -63,8 +63,7 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
} else if (this.showDetails === false) {
new CountUp(
'value',
this.performance?.currentNetPerformancePercentWithCurrencyEffect *
100,
this.performance?.netPerformancePercentageWithCurrencyEffect * 100,
{
decimal: getNumberFormatDecimal(this.locale),
decimalPlaces: 2,

View File

@ -9,9 +9,19 @@
class="flex-nowrap px-3 py-1 row"
[hidden]="summary?.ordersCount === null"
>
<div class="flex-grow-1 ml-3 text-truncate" i18n>
<div class="d-flex flex-grow-1 ml-3 text-truncate">
{{ summary?.ordersCount }}
{summary?.ordersCount, plural, =1 {transaction} other {transactions}}
<ng-container i18n>{summary?.ordersCount, plural,
=1 {activity}
other {activities}
}</ng-container>
<span
class="align-items-center d-flex ml-1"
matTooltipPosition="above"
[matTooltip]="buyAndSellActivitiesTooltip"
>
<ion-icon name="information-circle-outline" />
</span>
</div>
</div>
<div class="row">
@ -65,9 +75,7 @@
[locale]="locale"
[unit]="baseCurrency"
[value]="
isLoading
? undefined
: summary?.currentGrossPerformanceWithCurrencyEffect
isLoading ? undefined : summary?.grossPerformanceWithCurrencyEffect
"
/>
</div>
@ -91,7 +99,7 @@
[value]="
isLoading
? undefined
: summary?.currentGrossPerformancePercentWithCurrencyEffect
: summary?.grossPerformancePercentageWithCurrencyEffect
"
/>
</div>
@ -121,9 +129,7 @@
[locale]="locale"
[unit]="baseCurrency"
[value]="
isLoading
? undefined
: summary?.currentNetPerformanceWithCurrencyEffect
isLoading ? undefined : summary?.netPerformanceWithCurrencyEffect
"
/>
</div>
@ -147,7 +153,7 @@
[value]="
isLoading
? undefined
: summary?.currentNetPerformancePercentWithCurrencyEffect
: summary?.netPerformancePercentageWithCurrencyEffect
"
/>
</div>
@ -164,7 +170,7 @@
[isCurrency]="true"
[locale]="locale"
[unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.currentValue"
[value]="isLoading ? undefined : summary?.currentValueInBaseCurrency"
/>
</div>
</div>

View File

@ -1,5 +1,6 @@
import { getDateFnsLocale, getLocale } from '@ghostfolio/common/helper';
import { PortfolioSummary } from '@ghostfolio/common/interfaces';
import { translate } from '@ghostfolio/ui/i18n';
import {
ChangeDetectionStrategy,
@ -28,6 +29,9 @@ export class PortfolioSummaryComponent implements OnChanges, OnInit {
@Output() emergencyFundChanged = new EventEmitter<number>();
public buyAndSellActivitiesTooltip = translate(
'BUY_AND_SELL_ACTIVITIES_TOOLTIP'
);
public timeInMarket: string;
public constructor() {}

View File

@ -2,13 +2,14 @@ import { GfValueComponent } from '@ghostfolio/ui/value';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatTooltipModule } from '@angular/material/tooltip';
import { PortfolioSummaryComponent } from './portfolio-summary.component';
@NgModule({
declarations: [PortfolioSummaryComponent],
exports: [PortfolioSummaryComponent],
imports: [CommonModule, GfValueComponent],
imports: [CommonModule, GfValueComponent, MatTooltipModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfPortfolioSummaryModule {}

View File

@ -56,6 +56,8 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
public marketPrice: number;
public maxPrice: number;
public minPrice: number;
public netPerformance: number;
public netPerformancePercent: number;
public netPerformancePercentWithCurrencyEffect: number;
public netPerformanceWithCurrencyEffect: number;
public quantity: number;
@ -104,6 +106,8 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
marketPrice,
maxPrice,
minPrice,
netPerformance,
netPerformancePercent,
netPerformancePercentWithCurrencyEffect,
netPerformanceWithCurrencyEffect,
orders,
@ -126,15 +130,15 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
this.feeInBaseCurrency = feeInBaseCurrency;
this.firstBuyDate = firstBuyDate;
this.historicalDataItems = historicalData.map(
(historicalDataItem) => {
({ averagePrice, date, marketPrice }) => {
this.benchmarkDataItems.push({
date: historicalDataItem.date,
value: historicalDataItem.averagePrice
date,
value: averagePrice
});
return {
date: historicalDataItem.date,
value: historicalDataItem.marketPrice
date,
value: marketPrice
};
}
);
@ -142,6 +146,8 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
this.marketPrice = marketPrice;
this.maxPrice = maxPrice;
this.minPrice = minPrice;
this.netPerformance = netPerformance;
this.netPerformancePercent = netPerformancePercent;
this.netPerformancePercentWithCurrencyEffect =
netPerformancePercentWithCurrencyEffect;
this.netPerformanceWithCurrencyEffect =

View File

@ -37,27 +37,58 @@
<div class="row">
<div class="col-6 mb-3">
<gf-value
i18n
size="medium"
[colorizeSign]="true"
[isCurrency]="true"
[locale]="data.locale"
[unit]="data.baseCurrency"
[value]="netPerformanceWithCurrencyEffect"
>Change</gf-value
>
@if (
SymbolProfile?.currency &&
data.baseCurrency !== SymbolProfile?.currency
) {
<gf-value
i18n
size="medium"
[colorizeSign]="true"
[isCurrency]="true"
[locale]="data.locale"
[unit]="data.baseCurrency"
[value]="netPerformanceWithCurrencyEffect"
>Change with currency effect</gf-value
>
} @else {
<gf-value
i18n
size="medium"
[colorizeSign]="true"
[isCurrency]="true"
[locale]="data.locale"
[unit]="data.baseCurrency"
[value]="netPerformance"
>Change</gf-value
>
}
</div>
<div class="col-6 mb-3">
<gf-value
i18n
size="medium"
[colorizeSign]="true"
[isPercent]="true"
[locale]="data.locale"
[value]="netPerformancePercentWithCurrencyEffect"
>Performance</gf-value
>
@if (
SymbolProfile?.currency &&
data.baseCurrency !== SymbolProfile?.currency
) {
<gf-value
i18n
size="medium"
[colorizeSign]="true"
[isPercent]="true"
[locale]="data.locale"
[value]="netPerformancePercentWithCurrencyEffect"
>Performance with currency effect</gf-value
>
} @else {
<gf-value
i18n
size="medium"
[colorizeSign]="true"
[isPercent]="true"
[locale]="data.locale"
[value]="netPerformancePercent"
>Performance</gf-value
>
}
</div>
<div class="col-6 mb-3">
<gf-value

View File

@ -1,72 +0,0 @@
<div class="container p-0">
<div class="flex-nowrap no-gutters row">
<a
class="d-flex p-3 w-100"
[ngClass]="{ 'cursor-default': isLoading }"
[queryParams]="{
dataSource: position?.dataSource,
positionDetailDialog: true,
symbol: position?.symbol
}"
[routerLink]="[]"
>
<div class="d-flex mr-2">
<gf-trend-indicator
class="d-flex"
size="large"
[isLoading]="isLoading"
[marketState]="position?.marketState"
[range]="range"
[value]="position?.netPerformancePercentageWithCurrencyEffect"
/>
</div>
<div *ngIf="isLoading" class="flex-grow-1">
<ngx-skeleton-loader
animation="pulse"
class="mb-1"
[theme]="{
height: '1.2rem',
width: '12rem'
}"
/>
<ngx-skeleton-loader
animation="pulse"
[theme]="{
height: '1rem',
width: '8rem'
}"
/>
</div>
<div *ngIf="!isLoading" class="flex-grow-1 text-truncate">
<div class="h6 m-0 text-truncate">{{ position?.name }}</div>
<div class="d-flex">
<small class="text-muted">{{ position?.symbol | gfSymbol }}</small>
</div>
<div class="d-flex mt-1">
<gf-value
class="mr-3"
[colorizeSign]="true"
[isCurrency]="true"
[locale]="locale"
[unit]="baseCurrency"
[value]="position?.netPerformanceWithCurrencyEffect"
/>
<gf-value
[colorizeSign]="true"
[isPercent]="true"
[locale]="locale"
[value]="position?.netPerformancePercentageWithCurrencyEffect"
/>
</div>
</div>
<div class="align-items-center d-flex">
<ion-icon
*ngIf="!isLoading"
class="chevron text-muted"
name="chevron-forward-outline"
size="small"
/>
</div>
</a>
</div>
</div>

View File

@ -1,13 +0,0 @@
:host {
display: block;
.container {
gf-trend-indicator {
padding-top: 0.15rem;
}
.chevron {
opacity: 0.33;
}
}
}

View File

@ -1,40 +0,0 @@
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
import { getLocale } from '@ghostfolio/common/helper';
import { Position } from '@ghostfolio/common/interfaces';
import {
ChangeDetectionStrategy,
Component,
Input,
OnDestroy,
OnInit
} from '@angular/core';
import { Subject } from 'rxjs';
@Component({
selector: 'gf-position',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './position.component.html',
styleUrls: ['./position.component.scss']
})
export class PositionComponent implements OnDestroy, OnInit {
@Input() baseCurrency: string;
@Input() deviceType: string;
@Input() isLoading: boolean;
@Input() locale = getLocale();
@Input() position: Position;
@Input() range: string;
public unknownKey = UNKNOWN_KEY;
private unsubscribeSubject = new Subject<void>();
public constructor() {}
public ngOnInit() {}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

View File

@ -1,29 +0,0 @@
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
import { GfTrendIndicatorComponent } from '@ghostfolio/ui/trend-indicator';
import { GfValueComponent } from '@ghostfolio/ui/value';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatDialogModule } from '@angular/material/dialog';
import { RouterModule } from '@angular/router';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { GfPositionDetailDialogModule } from './position-detail-dialog/position-detail-dialog.module';
import { PositionComponent } from './position.component';
@NgModule({
declarations: [PositionComponent],
exports: [PositionComponent],
imports: [
CommonModule,
GfPositionDetailDialogModule,
GfSymbolModule,
GfTrendIndicatorComponent,
GfValueComponent,
MatDialogModule,
NgxSkeletonLoaderModule,
RouterModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfPositionModule {}

View File

@ -1,35 +0,0 @@
<div class="container p-0">
<div class="row no-gutters">
<div class="col">
<ng-container *ngIf="positions === undefined">
<gf-position [isLoading]="true" />
</ng-container>
<ng-container *ngIf="positions !== undefined">
<ng-container *ngIf="hasPositions">
<gf-position
*ngFor="let position of positionsWithPriority"
[baseCurrency]="baseCurrency"
[deviceType]="deviceType"
[locale]="locale"
[position]="position"
[range]="range"
/>
<gf-position
*ngFor="let position of positionsRest"
[baseCurrency]="baseCurrency"
[deviceType]="deviceType"
[locale]="locale"
[position]="position"
[range]="range"
/>
</ng-container>
<div
*ngIf="hasPermissionToCreateOrder && !hasPositions"
class="p-3 text-center"
>
<gf-no-transactions-info-indicator [hasBorder]="false" />
</div>
</ng-container>
</div>
</div>
</div>

View File

@ -1,17 +0,0 @@
:host {
display: block;
gf-position {
&:nth-child(even) {
background-color: rgba(0, 0, 0, var(--gf-theme-alpha-hover));
}
}
}
:host-context(.is-dark-theme) {
gf-position {
&:nth-child(even) {
background-color: rgba(255, 255, 255, var(--gf-theme-alpha-hover));
}
}
}

View File

@ -1,70 +0,0 @@
import { getLocale } from '@ghostfolio/common/helper';
import { Position } from '@ghostfolio/common/interfaces';
import {
ChangeDetectionStrategy,
Component,
Input,
OnChanges,
OnInit
} from '@angular/core';
@Component({
selector: 'gf-positions',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './positions.component.html',
styleUrls: ['./positions.component.scss']
})
export class PositionsComponent implements OnChanges, OnInit {
@Input() baseCurrency: string;
@Input() deviceType: string;
@Input() hasPermissionToCreateOrder: boolean;
@Input() locale = getLocale();
@Input() positions: Position[];
@Input() range: string;
public hasPositions: boolean;
public positionsRest: Position[] = [];
public positionsWithPriority: Position[] = [];
public constructor() {}
public ngOnInit() {}
public ngOnChanges() {
if (this.positions) {
this.hasPositions = this.positions.length > 0;
if (!this.hasPositions) {
return;
}
this.positionsRest = [];
this.positionsWithPriority = [];
for (const portfolioPosition of this.positions) {
if (portfolioPosition.marketState === 'open' || this.range !== '1d') {
// Only show positions where the market is open in today's view
this.positionsWithPriority.push(portfolioPosition);
} else {
this.positionsRest.push(portfolioPosition);
}
}
this.positionsRest.sort((a, b) =>
(a.name || a.symbol)?.toLowerCase() >
(b.name || b.symbol)?.toLowerCase()
? 1
: -1
);
this.positionsWithPriority.sort((a, b) =>
(a.name || a.symbol)?.toLowerCase() >
(b.name || b.symbol)?.toLowerCase()
? 1
: -1
);
} else {
this.hasPositions = false;
}
}
}

View File

@ -1,21 +0,0 @@
import { GfNoTransactionsInfoComponent } from '@ghostfolio/ui/no-transactions-info';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { GfPositionModule } from '../position/position.module';
import { PositionsComponent } from './positions.component';
@NgModule({
declarations: [PositionsComponent],
exports: [PositionsComponent],
imports: [
CommonModule,
GfNoTransactionsInfoComponent,
GfPositionModule,
MatButtonModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfPositionsModule {}

View File

@ -6,7 +6,6 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { GfPositionModule } from '../position/position.module';
import { RulesComponent } from './rules.component';
@NgModule({
@ -15,7 +14,6 @@ import { RulesComponent } from './rules.component';
imports: [
CommonModule,
GfNoTransactionsInfoComponent,
GfPositionModule,
GfRuleModule,
MatButtonModule,
MatCardModule

View File

@ -21,6 +21,7 @@ import { CreateOrUpdateAccessDialog } from './create-or-update-access-dialog/cre
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'has-fab' },
selector: 'gf-user-account-access',
styleUrls: ['./user-account-access.scss'],
templateUrl: './user-account-access.html'

View File

@ -77,7 +77,7 @@
mat-icon-button
title="Follow Ghostfolio on X (formerly Twitter)"
>
<span class="line-height-1 text-center w-100">𝕏</span>
<ion-icon name="logo-x" />
</a>
<a
*ngIf="user?.subscription?.type === 'Premium'"

View File

@ -21,7 +21,7 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog/c
import { TransferBalanceDialog } from './transfer-balance/transfer-balance-dialog.component';
@Component({
host: { class: 'page' },
host: { class: 'has-fab page' },
selector: 'gf-accounts-page',
styleUrls: ['./accounts-page.scss'],
templateUrl: './accounts-page.html'

View File

@ -30,8 +30,10 @@
systems, including
<a href="https://github.com/bigbeartechworld/big-bear-casaos"
>CasaOS</a
>, Home Assistant,
<a href="https://www.runtipi.io/docs/apps-available">Runtipi</a>,
>,
<a href="https://github.com/lildude/ha-addon-ghostfolio"
>Home Assistant</a
>, <a href="https://www.runtipi.io/docs/apps-available">Runtipi</a>,
<a href="https://truecharts.org/charts/stable/ghostfolio"
>TrueCharts</a
>, <a href="https://apps.umbrel.com/app/ghostfolio">Umbrel</a>, and

View File

@ -22,6 +22,11 @@ const routes: Routes = [
component: HomeHoldingsComponent,
title: $localize`Holdings`
},
{
path: 'holdings',
component: HomeHoldingsComponent,
title: $localize`Holdings`
},
{
path: 'summary',
component: HomeSummaryComponent,

View File

@ -1,7 +1,14 @@
import { PositionDetailDialogParams } from '@ghostfolio/client/components/position-detail-dialog/interfaces/interfaces';
import { PositionDetailDialog } from '@ghostfolio/client/components/position-detail-dialog/position-detail-dialog.component';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { TabConfiguration, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router';
import { DataSource } from '@prisma/client';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@ -14,6 +21,7 @@ import { takeUntil } from 'rxjs/operators';
})
export class HomePageComponent implements OnDestroy, OnInit {
public deviceType: string;
public hasImpersonationId: boolean;
public tabs: TabConfiguration[] = [];
public user: User;
@ -22,8 +30,27 @@ export class HomePageComponent implements OnDestroy, OnInit {
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private deviceService: DeviceDetectorService,
private dialog: MatDialog,
private impersonationStorageService: ImpersonationStorageService,
private route: ActivatedRoute,
private router: Router,
private userService: UserService
) {
this.route.queryParams
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((params) => {
if (
params['dataSource'] &&
params['positionDetailDialog'] &&
params['symbol']
) {
this.openPositionDialog({
dataSource: params['dataSource'],
symbol: params['symbol']
});
}
});
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
@ -59,10 +86,58 @@ export class HomePageComponent implements OnDestroy, OnInit {
public ngOnInit() {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
this.impersonationStorageService
.onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId;
});
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private openPositionDialog({
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
this.userService
.get()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((user) => {
this.user = user;
const dialogRef = this.dialog.open(PositionDetailDialog, {
autoFocus: false,
data: <PositionDetailDialogParams>{
dataSource,
symbol,
baseCurrency: this.user?.settings?.baseCurrency,
colorScheme: this.user?.settings?.colorScheme,
deviceType: this.deviceType,
hasImpersonationId: this.hasImpersonationId,
hasPermissionToReportDataGlitch: hasPermission(
this.user?.permissions,
permissions.reportDataGlitch
),
locale: this.user?.settings?.locale
},
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
});
dialogRef
.afterClosed()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.router.navigate(['.'], { relativeTo: this.route });
});
});
}
}

View File

@ -1,8 +1,8 @@
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
import { PositionDetailDialogParams } from '@ghostfolio/client/components/position/position-detail-dialog/interfaces/interfaces';
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
import { PositionDetailDialogParams } from '@ghostfolio/client/components/position-detail-dialog/interfaces/interfaces';
import { PositionDetailDialog } from '@ghostfolio/client/components/position-detail-dialog/position-detail-dialog.component';
import { DataService } from '@ghostfolio/client/services/data.service';
import { IcsService } from '@ghostfolio/client/services/ics/ics.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
@ -29,6 +29,7 @@ import { ImportActivitiesDialog } from './import-activities-dialog/import-activi
import { ImportActivitiesDialogParams } from './import-activities-dialog/interfaces/interfaces';
@Component({
host: { class: 'has-fab' },
selector: 'gf-activities-page',
styleUrls: ['./activities-page.scss'],
templateUrl: './activities-page.html'

View File

@ -2,7 +2,7 @@ import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { DataService } from '@ghostfolio/client/services/data.service';
import { ImportActivitiesService } from '@ghostfolio/client/services/import-activities.service';
import { Position } from '@ghostfolio/common/interfaces';
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
import {
StepperOrientation,
@ -43,7 +43,7 @@ export class ImportActivitiesDialog implements OnDestroy {
public deviceType: string;
public dialogTitle = $localize`Import Activities`;
public errorMessages: string[] = [];
public holdings: Position[] = [];
public holdings: PortfolioPosition[] = [];
public importStep: ImportStep = ImportStep.UPLOAD_FILE;
public isLoading = false;
public maxSafeInteger = Number.MAX_SAFE_INTEGER;
@ -88,7 +88,7 @@ export class ImportActivitiesDialog implements OnDestroy {
this.uniqueAssetForm.get('uniqueAsset').disable();
this.dataService
.fetchPositions({
.fetchPortfolioHoldings({
filters: [
{
id: AssetClass.EQUITY,
@ -98,8 +98,8 @@ export class ImportActivitiesDialog implements OnDestroy {
range: 'max'
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ positions }) => {
this.holdings = sortBy(positions, ({ name }) => {
.subscribe(({ holdings }) => {
this.holdings = sortBy(holdings, ({ name }) => {
return name.toLowerCase();
});
this.uniqueAssetForm.get('uniqueAsset').enable();

View File

@ -1,7 +1,7 @@
import { AccountDetailDialog } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.component';
import { AccountDetailDialogParams } from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces';
import { PositionDetailDialogParams } from '@ghostfolio/client/components/position/position-detail-dialog/interfaces/interfaces';
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
import { PositionDetailDialogParams } from '@ghostfolio/client/components/position-detail-dialog/interfaces/interfaces';
import { PositionDetailDialog } from '@ghostfolio/client/components/position-detail-dialog/position-detail-dialog.component';
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';
@ -103,7 +103,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
private router: Router,
private userService: UserService
) {
route.queryParams
this.route.queryParams
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((params) => {
if (params['accountId'] && params['accountDetailDialog']) {

View File

@ -1,5 +1,5 @@
import { PositionDetailDialogParams } from '@ghostfolio/client/components/position/position-detail-dialog/interfaces/interfaces';
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
import { PositionDetailDialogParams } from '@ghostfolio/client/components/position-detail-dialog/interfaces/interfaces';
import { PositionDetailDialog } from '@ghostfolio/client/components/position-detail-dialog/position-detail-dialog.component';
import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component';
import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
@ -8,7 +8,7 @@ import {
HistoricalDataItem,
PortfolioInvestments,
PortfolioPerformance,
Position,
PortfolioPosition,
User
} from '@ghostfolio/common/interfaces';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
@ -35,7 +35,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
public benchmark: Partial<SymbolProfile>;
public benchmarkDataItems: HistoricalDataItem[] = [];
public benchmarks: Partial<SymbolProfile>[];
public bottom3: Position[];
public bottom3: PortfolioPosition[];
public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS;
public daysInMarket: number;
public deviceType: string;
@ -60,7 +60,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
public performanceDataItemsInPercentage: HistoricalDataItem[];
public portfolioEvolutionDataLabel = $localize`Investment`;
public streaks: PortfolioInvestments['streaks'];
public top3: Position[];
public top3: PortfolioPosition[];
public unitCurrentStreak: string;
public unitLongestStreak: string;
public user: User;
@ -80,7 +80,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
const { benchmarks } = this.dataService.fetchInfo();
this.benchmarks = benchmarks;
route.queryParams
this.route.queryParams
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((params) => {
if (
@ -308,23 +308,23 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
});
this.dataService
.fetchPositions({
.fetchPortfolioHoldings({
filters: this.userService.getFilters(),
range: this.user?.settings?.dateRange
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ positions }) => {
const positionsSorted = sortBy(
positions.filter(({ netPerformancePercentageWithCurrencyEffect }) => {
return isNumber(netPerformancePercentageWithCurrencyEffect);
.subscribe(({ holdings }) => {
const holdingsSorted = sortBy(
holdings.filter(({ netPerformancePercentWithCurrencyEffect }) => {
return isNumber(netPerformancePercentWithCurrencyEffect);
}),
'netPerformancePercentageWithCurrencyEffect'
'netPerformancePercentWithCurrencyEffect'
).reverse();
this.top3 = positionsSorted.slice(0, 3);
this.top3 = holdingsSorted.slice(0, 3);
if (positions?.length > 3) {
this.bottom3 = positionsSorted.slice(-3).reverse();
if (holdings?.length > 3) {
this.bottom3 = holdingsSorted.slice(-3).reverse();
} else {
this.bottom3 = [];
}

View File

@ -42,7 +42,7 @@
[value]="
isLoadingInvestmentChart
? undefined
: performance?.currentNetPerformance
: performance?.netPerformance
"
/>
</div>
@ -61,7 +61,7 @@
[value]="
isLoadingInvestmentChart
? undefined
: performance?.currentNetPerformancePercent
: performance?.netPerformancePercentage
"
/>
</div>
@ -86,10 +86,10 @@
[value]="
isLoadingInvestmentChart
? undefined
: performance?.currentNetPerformance === null
: performance?.netPerformance === null
? null
: performance?.currentNetPerformanceWithCurrencyEffect -
performance?.currentNetPerformance
: performance?.netPerformanceWithCurrencyEffect -
performance?.netPerformance
"
/>
</div>
@ -108,10 +108,10 @@
[value]="
isLoadingInvestmentChart
? undefined
: performance?.currentNetPerformancePercent === null
: performance?.netPerformancePercentage === null
? null
: performance?.currentNetPerformancePercentWithCurrencyEffect -
performance?.currentNetPerformancePercent
: performance?.netPerformancePercentageWithCurrencyEffect -
performance?.netPerformancePercentage
"
/>
</div>
@ -131,7 +131,7 @@
[value]="
isLoadingInvestmentChart
? undefined
: performance?.currentNetPerformanceWithCurrencyEffect
: performance?.netPerformanceWithCurrencyEffect
"
/>
</div>
@ -150,7 +150,7 @@
[value]="
isLoadingInvestmentChart
? undefined
: performance?.currentNetPerformancePercentWithCurrencyEffect
: performance?.netPerformancePercentageWithCurrencyEffect
"
/>
</div>
@ -170,17 +170,17 @@
</mat-card-header>
<mat-card-content>
<ol class="mb-0 ml-1 pl-3">
<li *ngFor="let position of top3" class="py-1">
<li *ngFor="let holding of top3" class="py-1">
<a
class="d-flex"
[queryParams]="{
dataSource: position.dataSource,
dataSource: holding.dataSource,
positionDetailDialog: true,
symbol: position.symbol
symbol: holding.symbol
}"
[routerLink]="[]"
>
<div class="flex-grow-1 mr-2">{{ position.name }}</div>
<div class="flex-grow-1 mr-2">{{ holding.name }}</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
@ -188,9 +188,7 @@
[colorizeSign]="true"
[isPercent]="true"
[locale]="user?.settings?.locale"
[value]="
position.netPerformancePercentageWithCurrencyEffect
"
[value]="holding.netPerformancePercentWithCurrencyEffect"
/>
</div>
</a>
@ -218,17 +216,17 @@
</mat-card-header>
<mat-card-content>
<ol class="mb-0 ml-1 pl-3">
<li *ngFor="let position of bottom3" class="py-1">
<li *ngFor="let holding of bottom3" class="py-1">
<a
class="d-flex"
[queryParams]="{
dataSource: position.dataSource,
dataSource: holding.dataSource,
positionDetailDialog: true,
symbol: position.symbol
symbol: holding.symbol
}"
[routerLink]="[]"
>
<div class="flex-grow-1 mr-2">{{ position.name }}</div>
<div class="flex-grow-1 mr-2">{{ holding.name }}</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
@ -236,9 +234,7 @@
[colorizeSign]="true"
[isPercent]="true"
[locale]="user?.settings?.locale"
[value]="
position.netPerformancePercentageWithCurrencyEffect
"
[value]="holding.netPerformancePercentWithCurrencyEffect"
/>
</div>
</a>

View File

@ -1,21 +0,0 @@
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HoldingsPageComponent } from './holdings-page.component';
const routes: Routes = [
{
canActivate: [AuthGuard],
component: HoldingsPageComponent,
path: '',
title: $localize`Holdings`
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class HoldingsPageRoutingModule {}

View File

@ -1,171 +0,0 @@
import { PositionDetailDialogParams } from '@ghostfolio/client/components/position/position-detail-dialog/interfaces/interfaces';
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
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 { PortfolioPosition, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { HoldingType, ToggleOption } from '@ghostfolio/common/types';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router';
import { DataSource } from '@prisma/client';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'gf-holdings-page',
styleUrls: ['./holdings-page.scss'],
templateUrl: './holdings-page.html'
})
export class HoldingsPageComponent implements OnDestroy, OnInit {
public deviceType: string;
public hasImpersonationId: boolean;
public hasPermissionToCreateOrder: boolean;
public holdings: PortfolioPosition[];
public holdingType: HoldingType = 'ACTIVE';
public holdingTypeOptions: ToggleOption[] = [
{ label: $localize`Active`, value: 'ACTIVE' },
{ label: $localize`Closed`, value: 'CLOSED' }
];
public user: User;
private unsubscribeSubject = new Subject<void>();
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private deviceService: DeviceDetectorService,
private dialog: MatDialog,
private impersonationStorageService: ImpersonationStorageService,
private route: ActivatedRoute,
private router: Router,
private userService: UserService
) {
route.queryParams
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((params) => {
if (
params['dataSource'] &&
params['positionDetailDialog'] &&
params['symbol']
) {
this.openPositionDialog({
dataSource: params['dataSource'],
symbol: params['symbol']
});
}
});
}
public ngOnInit() {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
this.impersonationStorageService
.onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId;
});
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
if (state?.user) {
this.user = state.user;
this.hasPermissionToCreateOrder = hasPermission(
this.user.permissions,
permissions.createOrder
);
this.holdings = undefined;
this.fetchHoldings()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ holdings }) => {
this.holdings = holdings;
this.changeDetectorRef.markForCheck();
});
this.changeDetectorRef.markForCheck();
}
});
}
public onChangeHoldingType(aHoldingType: HoldingType) {
this.holdingType = aHoldingType;
this.holdings = undefined;
this.fetchHoldings()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ holdings }) => {
this.holdings = holdings;
this.changeDetectorRef.markForCheck();
});
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private fetchHoldings() {
const filters = this.userService.getFilters();
if (this.holdingType === 'CLOSED') {
filters.push({ id: 'CLOSED', type: 'HOLDING_TYPE' });
}
return this.dataService.fetchPortfolioHoldings({
filters,
range: this.user?.settings?.dateRange
});
}
private openPositionDialog({
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
this.userService
.get()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((user) => {
this.user = user;
const dialogRef = this.dialog.open(PositionDetailDialog, {
autoFocus: false,
data: <PositionDetailDialogParams>{
dataSource,
symbol,
baseCurrency: this.user?.settings?.baseCurrency,
colorScheme: this.user?.settings?.colorScheme,
deviceType: this.deviceType,
hasImpersonationId: this.hasImpersonationId,
hasPermissionToReportDataGlitch: hasPermission(
this.user?.permissions,
permissions.reportDataGlitch
),
locale: this.user?.settings?.locale
},
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
});
dialogRef
.afterClosed()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.router.navigate(['.'], { relativeTo: this.route });
});
});
}
}

View File

@ -1,38 +0,0 @@
<div class="container">
<div class="row">
<div class="col">
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Holdings</h1>
</div>
</div>
<div class="row">
<div class="col-lg">
<div class="d-flex justify-content-end">
<gf-toggle
class="d-none d-lg-block"
[defaultValue]="holdingType"
[isLoading]="false"
[options]="holdingTypeOptions"
(change)="onChangeHoldingType($event.value)"
/>
</div>
<gf-holdings-table
[baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType"
[hasPermissionToCreateActivity]="hasPermissionToCreateOrder"
[holdings]="holdings"
[locale]="user?.settings?.locale"
/>
@if (hasPermissionToCreateOrder && holdings?.length > 0) {
<div class="text-center">
<a
class="mt-3"
i18n
mat-stroked-button
[routerLink]="['/portfolio', 'activities']"
>Manage Activities</a
>
</div>
}
</div>
</div>
</div>

View File

@ -1,22 +0,0 @@
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
import { GfHoldingsTableComponent } from '@ghostfolio/ui/holdings-table';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { HoldingsPageRoutingModule } from './holdings-page-routing.module';
import { HoldingsPageComponent } from './holdings-page.component';
@NgModule({
declarations: [HoldingsPageComponent],
imports: [
CommonModule,
GfHoldingsTableComponent,
GfToggleModule,
HoldingsPageRoutingModule,
MatButtonModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class HoldingsPageModule {}

View File

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

View File

@ -16,13 +16,6 @@ const routes: Routes = [
(m) => m.AnalysisPageModule
)
},
{
path: 'holdings',
loadChildren: () =>
import('./holdings/holdings-page.module').then(
(m) => m.HoldingsPageModule
)
},
{
path: 'activities',
loadChildren: () =>

View File

@ -34,11 +34,6 @@ export class PortfolioPageComponent implements OnDestroy, OnInit {
label: $localize`Analysis`,
path: ['/portfolio']
},
{
iconName: 'wallet-outline',
label: $localize`Holdings`,
path: ['/portfolio', 'holdings']
},
{
iconName: 'swap-vertical-outline',
label: $localize`Activities`,

View File

@ -6,7 +6,6 @@ import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { Activities } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
import { PortfolioPositionDetail } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position-detail.interface';
import { PortfolioPositions } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-positions.interface';
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface';
import { UserItem } from '@ghostfolio/api/app/user/interfaces/user-item.interface';
@ -376,21 +375,6 @@ export class DataService {
});
}
public fetchPositions({
filters,
range
}: {
filters?: Filter[];
range: DateRange;
}): Observable<PortfolioPositions> {
let params = this.buildFiltersAsQueryParams({ filters });
params = params.append('range', range);
return this.http.get<PortfolioPositions>('/api/v1/portfolio/positions', {
params
});
}
public fetchSymbols({
includeIndices = false,
query

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -387,6 +387,10 @@ ngx-skeleton-loader {
@include gf-table;
}
.has-fab {
padding-bottom: 3rem !important;
}
.has-info-message {
.page.has-tabs {
height: calc(100svh - 2 * var(--mat-toolbar-standard-height));
@ -543,6 +547,10 @@ ngx-skeleton-loader {
--mdc-tab-indicator-active-indicator-color: transparent;
}
.mat-mdc-tab-nav-panel {
padding: 2rem 0;
}
@media (max-width: 575.98px) {
.mat-mdc-tab-link {
--mdc-secondary-navigation-tab-container-height: 3rem;
@ -567,10 +575,6 @@ ngx-skeleton-loader {
}
}
}
.mat-mdc-tab-nav-panel {
padding: 2rem 0;
}
}
}
}

View File

@ -66,7 +66,7 @@ export const EMERGENCY_FUND_TAG_ID = '4452656d-9fa4-4bd0-ba38-70492e31d180';
export const GATHER_ASSET_PROFILE_PROCESS = 'GATHER_ASSET_PROFILE';
export const GATHER_ASSET_PROFILE_PROCESS_OPTIONS: JobOptions = {
attempts: 10,
attempts: 12,
backoff: {
delay: ms('1 minute'),
type: 'exponential'
@ -76,7 +76,7 @@ export const GATHER_ASSET_PROFILE_PROCESS_OPTIONS: JobOptions = {
export const GATHER_HISTORICAL_MARKET_DATA_PROCESS =
'GATHER_HISTORICAL_MARKET_DATA';
export const GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS: JobOptions = {
attempts: 10,
attempts: 12,
backoff: {
delay: ms('1 minute'),
type: 'exponential'

View File

@ -1,14 +1,14 @@
export interface PortfolioPerformance {
annualizedPerformancePercent?: number;
currentGrossPerformance: number;
currentGrossPerformancePercent: number;
currentGrossPerformancePercentWithCurrencyEffect: number;
currentGrossPerformanceWithCurrencyEffect: number;
currentNetPerformance: number;
currentNetPerformancePercent: number;
currentNetPerformancePercentWithCurrencyEffect: number;
currentNetPerformanceWithCurrencyEffect: number;
currentNetWorth: number;
currentValue: number;
currentNetWorth?: number;
currentValueInBaseCurrency: number;
grossPerformance: number;
grossPerformancePercentage: number;
grossPerformancePercentageWithCurrencyEffect: number;
grossPerformanceWithCurrencyEffect: number;
netPerformance: number;
netPerformancePercentage: number;
netPerformancePercentageWithCurrencyEffect: number;
netPerformanceWithCurrencyEffect: number;
totalInvestment: number;
}

View File

@ -119,7 +119,7 @@
</td>
</ng-container>
<ng-container matColumnDef="icon">
<ng-container matColumnDef="icon" sticky>
<th *matHeaderCellDef class="px-1" mat-header-cell></th>
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
<gf-asset-profile-icon

View File

@ -416,7 +416,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
private searchHoldings(aSearchTerm: string): Observable<ISearchResultItem[]> {
return this.dataService
.fetchPositions({
.fetchPortfolioHoldings({
filters: [
{
id: aSearchTerm,
@ -429,8 +429,8 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
catchError(() => {
return EMPTY;
}),
map(({ positions }) => {
return positions.map(
map(({ holdings }) => {
return holdings.map(
({ assetSubClass, currency, dataSource, name, symbol }) => {
return {
currency,

View File

@ -7,7 +7,7 @@
matSortDirection="desc"
[dataSource]="dataSource"
>
<ng-container matColumnDef="icon">
<ng-container matColumnDef="icon" sticky>
<th *matHeaderCellDef class="px-1" mat-header-cell></th>
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
<gf-asset-profile-icon
@ -109,7 +109,30 @@
</td>
</ng-container>
<ng-container matColumnDef="performance" stickyEnd>
<ng-container matColumnDef="performance">
<th
*matHeaderCellDef
class="justify-content-end px-1"
mat-header-cell
mat-sort-header="netPerformanceWithCurrencyEffect"
>
<ng-container i18n>Change</ng-container>
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
<div class="d-flex justify-content-end">
<gf-value
[colorizeSign]="true"
[isCurrency]="true"
[locale]="locale"
[value]="
isLoading ? undefined : element.netPerformanceWithCurrencyEffect
"
/>
</div>
</td>
</ng-container>
<ng-container matColumnDef="performanceInPercentage" stickyEnd>
<th
*matHeaderCellDef
class="justify-content-end px-1"

View File

@ -1,5 +1,5 @@
import { GfAssetProfileIconComponent } from '@ghostfolio/client/components/asset-profile-icon/asset-profile-icon.component';
import { GfPositionDetailDialogModule } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.module';
import { GfPositionDetailDialogModule } from '@ghostfolio/client/components/position-detail-dialog/position-detail-dialog.module';
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
import { getLocale } from '@ghostfolio/common/helper';
import { PortfolioPosition, UniqueAsset } from '@ghostfolio/common/interfaces';
@ -84,7 +84,12 @@ export class GfHoldingsTableComponent implements OnChanges, OnDestroy, OnInit {
}
this.displayedColumns.push('allocationInPercentage');
this.displayedColumns.push('performance');
if (this.hasPermissionToShowValues) {
this.displayedColumns.push('performance');
}
this.displayedColumns.push('performanceInPercentage');
this.isLoading = true;

View File

@ -5,6 +5,7 @@ const locales = {
'Asia-Pacific': $localize`Asia-Pacific`,
ASSET_CLASS: $localize`Asset Class`,
ASSET_SUB_CLASS: $localize`Asset Sub Class`,
BUY_AND_SELL_ACTIVITIES_TOOLTIP: $localize`Buy and sell`,
CORE: $localize`Core`,
DATA_IMPORT_AND_EXPORT_TOOLTIP_BASIC: $localize`Switch to Ghostfolio Premium or Ghostfolio Open Source easily`,
DATA_IMPORT_AND_EXPORT_TOOLTIP_OSS: $localize`Switch to Ghostfolio Premium easily`,

View File

@ -1,7 +1,7 @@
<div *ngIf="icon" class="align-self-center mr-3">
<ion-icon class="h3 m-0" [name]="icon" />
</div>
<div class="w-100">
<div class="d-flex flex-column w-100">
<ng-template #label><ng-content></ng-content></ng-template>
<ng-container *ngIf="value || value === 0 || value === null">
<div
@ -73,13 +73,13 @@
/>
<ng-container>
<div *ngIf="size === 'large'">
<div *ngIf="size === 'large'" class="text-truncate">
<span class="h6"
><ng-container *ngTemplateOutlet="label"></ng-container
></span>
<span *ngIf="subLabel" class="text-muted"> {{ subLabel }}</span>
</div>
<small *ngIf="size !== 'large'">
<small *ngIf="size !== 'large'" class="d-block text-truncate">
<ng-container *ngTemplateOutlet="label"></ng-container>
</small>
</ng-container>

View File

@ -1,6 +1,6 @@
{
"name": "ghostfolio",
"version": "2.78.0",
"version": "2.80.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",
@ -112,7 +112,7 @@
"got": "11.8.6",
"helmet": "7.0.0",
"http-status-codes": "2.3.0",
"ionicons": "7.3.0",
"ionicons": "7.4.0",
"jsonpath": "1.1.1",
"lodash": "4.17.21",
"marked": "9.1.6",

View File

@ -13104,10 +13104,10 @@ invariant@^2.2.4:
dependencies:
loose-envify "^1.0.0"
ionicons@7.3.0:
version "7.3.0"
resolved "https://registry.yarnpkg.com/ionicons/-/ionicons-7.3.0.tgz#d2385e87dbe6a8c79fd44fb2e7c87b0de4a2cbcb"
integrity sha512-l9quySYi+o4T6mFzhKRyU/1nKc2Zs0zxs7jWcq9iVRhRPQondV11jYqLTed0lVVXHfGrBCfnedKl9D6BCnA1UQ==
ionicons@7.4.0:
version "7.4.0"
resolved "https://registry.yarnpkg.com/ionicons/-/ionicons-7.4.0.tgz#9c285aaa8089befbd6c5a89ae13292d364cd9ace"
integrity sha512-ZK94MMqgzMCPPMhmk8Ouu6goyVHFIlw/ACP6oe3FrikcI0N7CX0xcwVaEbUc0G/v3W0shI93vo+9ve/KpvcNhQ==
dependencies:
"@stencil/core" "^4.0.3"