Browse Source

Feature/integrate wealth items into transaction point concept (#3084)

* Integrate (wealth) items into transaction point concept

* Update changelog
pull/3085/head^2
Thomas Kaul 11 months ago
committed by GitHub
parent
commit
5596e5f03b
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      CHANGELOG.md
  2. 2
      apps/api/src/app/import/import.controller.ts
  3. 13
      apps/api/src/app/order/order.service.ts
  4. 1
      apps/api/src/app/portfolio/current-rate.service.spec.ts
  5. 10
      apps/api/src/app/portfolio/current-rate.service.ts
  6. 2
      apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell.spec.ts
  7. 2
      apps/api/src/app/portfolio/portfolio-calculator-baln-buy.spec.ts
  8. 2
      apps/api/src/app/portfolio/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts
  9. 2
      apps/api/src/app/portfolio/portfolio-calculator-googl-buy.spec.ts
  10. 2
      apps/api/src/app/portfolio/portfolio-calculator-no-orders.spec.ts
  11. 2
      apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell-partially.spec.ts
  12. 2
      apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts
  13. 2
      apps/api/src/app/portfolio/portfolio-calculator.spec.ts
  14. 1
      apps/api/src/app/portfolio/portfolio-calculator.ts
  15. 7
      apps/api/src/app/portfolio/portfolio.controller.ts
  16. 40
      apps/api/src/app/portfolio/portfolio.service.ts
  17. 2
      apps/api/src/app/symbol/symbol.controller.ts
  18. 14
      apps/api/src/services/data-provider/manual/manual.service.ts
  19. 3
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts
  20. 8
      apps/client/src/app/services/data.service.ts
  21. 2
      libs/common/src/lib/interfaces/portfolio-public-details.interface.ts

2
CHANGELOG.md

@ -10,10 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Improved the usability of the benchmarks in the markets overview - Improved the usability of the benchmarks in the markets overview
- Integrated (wealth) items into the transaction point concept in the portfolio service
### Fixed ### Fixed
- Fixed a missing value in the activities table on mobile - Fixed a missing value in the activities table on mobile
- Fixed a missing value on the public page
- Displayed the button to fetch the current market price only if the activity is from today - Displayed the button to fetch the current market price only if the activity is from today
## 2.59.0 - 2024-02-29 ## 2.59.0 - 2024-02-29

2
apps/api/src/app/import/import.controller.ts

@ -43,7 +43,7 @@ export class ImportController {
@UseInterceptors(TransformDataSourceInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor)
public async import( public async import(
@Body() importData: ImportDataDto, @Body() importData: ImportDataDto,
@Query('dryRun') isDryRun?: boolean @Query('dryRun') isDryRun = false
): Promise<ImportResponse> { ): Promise<ImportResponse> {
if ( if (
!hasPermission(this.request.user.permissions, permissions.createAccount) !hasPermission(this.request.user.permissions, permissions.createAccount)

13
apps/api/src/app/order/order.service.ts

@ -8,7 +8,7 @@ import {
GATHER_ASSET_PROFILE_PROCESS_OPTIONS GATHER_ASSET_PROFILE_PROCESS_OPTIONS
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper'; import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import { Filter } from '@ghostfolio/common/interfaces'; import { Filter, UniqueAsset } from '@ghostfolio/common/interfaces';
import { OrderWithAccount } from '@ghostfolio/common/types'; import { OrderWithAccount } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
@ -200,6 +200,17 @@ export class OrderService {
return count; return count;
} }
public async getLatestOrder({ dataSource, symbol }: UniqueAsset) {
return this.prismaService.order.findFirst({
orderBy: {
date: 'desc'
},
where: {
SymbolProfile: { dataSource, symbol }
}
});
}
public async getOrders({ public async getOrders({
filters, filters,
includeDrafts = false, includeDrafts = false,

1
apps/api/src/app/portfolio/current-rate.service.spec.ts

@ -108,6 +108,7 @@ describe('CurrentRateService', () => {
currentRateService = new CurrentRateService( currentRateService = new CurrentRateService(
dataProviderService, dataProviderService,
marketDataService, marketDataService,
null,
null null
); );
}); });

10
apps/api/src/app/portfolio/current-rate.service.ts

@ -1,3 +1,4 @@
import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { resetHours } from '@ghostfolio/common/helper'; import { resetHours } from '@ghostfolio/common/helper';
@ -22,6 +23,7 @@ export class CurrentRateService {
public constructor( public constructor(
private readonly dataProviderService: DataProviderService, private readonly dataProviderService: DataProviderService,
private readonly marketDataService: MarketDataService, private readonly marketDataService: MarketDataService,
private readonly orderService: OrderService,
@Inject(REQUEST) private readonly request: RequestWithUser @Inject(REQUEST) private readonly request: RequestWithUser
) {} ) {}
@ -121,11 +123,17 @@ export class CurrentRateService {
}); });
if (!value) { if (!value) {
// Fallback to unit price of latest activity
const latestActivity = await this.orderService.getLatestOrder({
dataSource,
symbol
});
value = { value = {
dataSource, dataSource,
symbol, symbol,
date: today, date: today,
marketPrice: 0 marketPrice: latestActivity?.unitPrice ?? 0
}; };
response.values.push(value); response.values.push(value);

2
apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell.spec.ts

@ -21,7 +21,7 @@ describe('PortfolioCalculator', () => {
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
beforeEach(() => { beforeEach(() => {
currentRateService = new CurrentRateService(null, null, null); currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
null, null,

2
apps/api/src/app/portfolio/portfolio-calculator-baln-buy.spec.ts

@ -21,7 +21,7 @@ describe('PortfolioCalculator', () => {
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
beforeEach(() => { beforeEach(() => {
currentRateService = new CurrentRateService(null, null, null); currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
null, null,

2
apps/api/src/app/portfolio/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts

@ -34,7 +34,7 @@ describe('PortfolioCalculator', () => {
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
beforeEach(() => { beforeEach(() => {
currentRateService = new CurrentRateService(null, null, null); currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
null, null,

2
apps/api/src/app/portfolio/portfolio-calculator-googl-buy.spec.ts

@ -34,7 +34,7 @@ describe('PortfolioCalculator', () => {
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
beforeEach(() => { beforeEach(() => {
currentRateService = new CurrentRateService(null, null, null); currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
null, null,

2
apps/api/src/app/portfolio/portfolio-calculator-no-orders.spec.ts

@ -21,7 +21,7 @@ describe('PortfolioCalculator', () => {
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
beforeEach(() => { beforeEach(() => {
currentRateService = new CurrentRateService(null, null, null); currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
null, null,

2
apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell-partially.spec.ts

@ -21,7 +21,7 @@ describe('PortfolioCalculator', () => {
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
beforeEach(() => { beforeEach(() => {
currentRateService = new CurrentRateService(null, null, null); currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
null, null,

2
apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts

@ -21,7 +21,7 @@ describe('PortfolioCalculator', () => {
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
beforeEach(() => { beforeEach(() => {
currentRateService = new CurrentRateService(null, null, null); currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
null, null,

2
apps/api/src/app/portfolio/portfolio-calculator.spec.ts

@ -10,7 +10,7 @@ describe('PortfolioCalculator', () => {
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
beforeEach(() => { beforeEach(() => {
currentRateService = new CurrentRateService(null, null, null); currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
null, null,

1
apps/api/src/app/portfolio/portfolio-calculator.ts

@ -825,6 +825,7 @@ export class PortfolioCalculator {
switch (type) { switch (type) {
case 'BUY': case 'BUY':
case 'ITEM':
factor = 1; factor = 1;
break; break;
case 'SELL': case 'SELL':

7
apps/api/src/app/portfolio/portfolio.controller.ts

@ -342,7 +342,8 @@ export class PortfolioController {
@Query('assetClasses') filterByAssetClasses?: string, @Query('assetClasses') filterByAssetClasses?: string,
@Query('range') dateRange: DateRange = 'max', @Query('range') dateRange: DateRange = 'max',
@Query('tags') filterByTags?: string, @Query('tags') filterByTags?: string,
@Query('withExcludedAccounts') withExcludedAccounts = false @Query('withExcludedAccounts') withExcludedAccounts = false,
@Query('withItems') withItems = false
): Promise<PortfolioPerformanceResponse> { ): Promise<PortfolioPerformanceResponse> {
const hasReadRestrictedAccessPermission = const hasReadRestrictedAccessPermission =
this.userService.hasReadRestrictedAccessPermission({ this.userService.hasReadRestrictedAccessPermission({
@ -361,6 +362,7 @@ export class PortfolioController {
filters, filters,
impersonationId, impersonationId,
withExcludedAccounts, withExcludedAccounts,
withItems,
userId: this.request.user.id userId: this.request.user.id
}); });
@ -515,7 +517,8 @@ export class PortfolioController {
dateOfFirstActivity: portfolioPosition.dateOfFirstActivity, dateOfFirstActivity: portfolioPosition.dateOfFirstActivity,
markets: hasDetails ? portfolioPosition.markets : undefined, markets: hasDetails ? portfolioPosition.markets : undefined,
name: portfolioPosition.name, name: portfolioPosition.name,
netPerformancePercent: portfolioPosition.netPerformancePercent, netPerformancePercentWithCurrencyEffect:
portfolioPosition.netPerformancePercentWithCurrencyEffect,
sectors: hasDetails ? portfolioPosition.sectors : [], sectors: hasDetails ? portfolioPosition.sectors : [],
symbol: portfolioPosition.symbol, symbol: portfolioPosition.symbol,
url: portfolioPosition.url, url: portfolioPosition.url,

40
apps/api/src/app/portfolio/portfolio.service.ts

@ -277,7 +277,8 @@ export class PortfolioService {
await this.getTransactionPoints({ await this.getTransactionPoints({
filters, filters,
userId, userId,
includeDrafts: true includeDrafts: true,
types: ['BUY', 'SELL']
}); });
if (transactionPoints.length === 0) { if (transactionPoints.length === 0) {
@ -702,7 +703,7 @@ export class PortfolioService {
.filter((order) => { .filter((order) => {
tags = tags.concat(order.tags); tags = tags.concat(order.tags);
return order.type === 'BUY' || order.type === 'SELL'; return ['BUY', 'ITEM', 'SELL'].includes(order.type);
}) })
.map((order) => ({ .map((order) => ({
currency: order.SymbolProfile.currency, currency: order.SymbolProfile.currency,
@ -957,7 +958,8 @@ export class PortfolioService {
const { portfolioOrders, transactionPoints } = const { portfolioOrders, transactionPoints } =
await this.getTransactionPoints({ await this.getTransactionPoints({
filters, filters,
userId userId,
types: ['BUY', 'SELL']
}); });
if (transactionPoints?.length <= 0) { if (transactionPoints?.length <= 0) {
@ -1087,13 +1089,15 @@ export class PortfolioService {
filters, filters,
impersonationId, impersonationId,
userId, userId,
withExcludedAccounts = false withExcludedAccounts = false,
withItems = false
}: { }: {
dateRange?: DateRange; dateRange?: DateRange;
filters?: Filter[]; filters?: Filter[];
impersonationId: string; impersonationId: string;
userId: string; userId: string;
withExcludedAccounts?: boolean; withExcludedAccounts?: boolean;
withItems?: boolean;
}): Promise<PortfolioPerformanceResponse> { }): Promise<PortfolioPerformanceResponse> {
userId = await this.getUserId(impersonationId, userId); userId = await this.getUserId(impersonationId, userId);
const user = await this.userService.user({ id: userId }); const user = await this.userService.user({ id: userId });
@ -1128,7 +1132,8 @@ export class PortfolioService {
await this.getTransactionPoints({ await this.getTransactionPoints({
filters, filters,
userId, userId,
withExcludedAccounts withExcludedAccounts,
types: withItems ? ['BUY', 'ITEM', 'SELL'] : ['BUY', 'SELL']
}); });
const portfolioCalculator = new PortfolioCalculator({ const portfolioCalculator = new PortfolioCalculator({
@ -1280,7 +1285,8 @@ export class PortfolioService {
const { orders, portfolioOrders, transactionPoints } = const { orders, portfolioOrders, transactionPoints } =
await this.getTransactionPoints({ await this.getTransactionPoints({
userId userId,
types: ['BUY', 'SELL']
}); });
const portfolioCalculator = new PortfolioCalculator({ const portfolioCalculator = new PortfolioCalculator({
@ -1913,11 +1919,13 @@ export class PortfolioService {
private async getTransactionPoints({ private async getTransactionPoints({
filters, filters,
includeDrafts = false, includeDrafts = false,
types = ['BUY', 'ITEM', 'SELL'],
userId, userId,
withExcludedAccounts = false withExcludedAccounts = false
}: { }: {
filters?: Filter[]; filters?: Filter[];
includeDrafts?: boolean; includeDrafts?: boolean;
types?: ActivityType[];
userId: string; userId: string;
withExcludedAccounts?: boolean; withExcludedAccounts?: boolean;
}): Promise<{ }): Promise<{
@ -1931,10 +1939,10 @@ export class PortfolioService {
const { activities, count } = await this.orderService.getOrders({ const { activities, count } = await this.orderService.getOrders({
filters, filters,
includeDrafts, includeDrafts,
types,
userCurrency, userCurrency,
userId, userId,
withExcludedAccounts, withExcludedAccounts
types: ['BUY', 'SELL']
}); });
if (count <= 0) { if (count <= 0) {
@ -2006,7 +2014,7 @@ export class PortfolioService {
userCurrency, userCurrency,
userId, userId,
withExcludedAccounts, withExcludedAccounts,
types: ['ITEM', 'LIABILITY'] types: ['LIABILITY']
}); });
const accounts: PortfolioDetails['accounts'] = {}; const accounts: PortfolioDetails['accounts'] = {};
@ -2094,18 +2102,14 @@ export class PortfolioService {
Account, Account,
quantity, quantity,
SymbolProfile, SymbolProfile,
type, type
valueInBaseCurrency
} of ordersByAccount) { } of ordersByAccount) {
const unitPriceInBaseCurrency =
portfolioItemsNow[SymbolProfile.symbol]?.marketPriceInBaseCurrency ??
valueInBaseCurrency ??
0;
let currentValueOfSymbolInBaseCurrency = let currentValueOfSymbolInBaseCurrency =
quantity * unitPriceInBaseCurrency; quantity *
portfolioItemsNow[SymbolProfile.symbol]
?.marketPriceInBaseCurrency ?? 0;
if (type === 'LIABILITY' || type === 'SELL') { if (['LIABILITY', 'SELL'].includes(type)) {
currentValueOfSymbolInBaseCurrency *= -1; currentValueOfSymbolInBaseCurrency *= -1;
} }

2
apps/api/src/app/symbol/symbol.controller.ts

@ -39,7 +39,7 @@ export class SymbolController {
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(TransformDataSourceInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor)
public async lookupSymbol( public async lookupSymbol(
@Query('includeIndices') includeIndices: boolean = false, @Query('includeIndices') includeIndices = false,
@Query('query') query = '' @Query('query') query = ''
): Promise<{ items: LookupItem[] }> { ): Promise<{ items: LookupItem[] }> {
try { try {

14
apps/api/src/services/data-provider/manual/manual.service.ts

@ -166,13 +166,15 @@ export class ManualService implements DataProviderInterface {
} }
}); });
for (const symbolProfile of symbolProfiles) { for (const { currency, symbol } of symbolProfiles) {
response[symbolProfile.symbol] = { let marketPrice = marketData.find((marketDataItem) => {
currency: symbolProfile.currency, return marketDataItem.symbol === symbol;
})?.marketPrice;
response[symbol] = {
currency,
marketPrice,
dataSource: this.getName(), dataSource: this.getName(),
marketPrice: marketData.find((marketDataItem) => {
return marketDataItem.symbol === symbolProfile.symbol;
})?.marketPrice,
marketState: 'delayed' marketState: 'delayed'
}; };
} }

3
apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts

@ -227,7 +227,8 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
} }
], ],
range: 'max', range: 'max',
withExcludedAccounts: true withExcludedAccounts: true,
withItems: true
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ chart }) => { .subscribe(({ chart }) => {

8
apps/client/src/app/services/data.service.ts

@ -437,11 +437,13 @@ export class DataService {
public fetchPortfolioPerformance({ public fetchPortfolioPerformance({
filters, filters,
range, range,
withExcludedAccounts = false withExcludedAccounts = false,
withItems = false
}: { }: {
filters?: Filter[]; filters?: Filter[];
range: DateRange; range: DateRange;
withExcludedAccounts?: boolean; withExcludedAccounts?: boolean;
withItems?: boolean;
}): Observable<PortfolioPerformanceResponse> { }): Observable<PortfolioPerformanceResponse> {
let params = this.buildFiltersAsQueryParams({ filters }); let params = this.buildFiltersAsQueryParams({ filters });
params = params.append('range', range); params = params.append('range', range);
@ -450,6 +452,10 @@ export class DataService {
params = params.append('withExcludedAccounts', withExcludedAccounts); params = params.append('withExcludedAccounts', withExcludedAccounts);
} }
if (withItems) {
params = params.append('withItems', withItems);
}
return this.http return this.http
.get<any>(`/api/v2/portfolio/performance`, { .get<any>(`/api/v2/portfolio/performance`, {
params params

2
libs/common/src/lib/interfaces/portfolio-public-details.interface.ts

@ -13,7 +13,7 @@ export interface PortfolioPublicDetails {
| 'dateOfFirstActivity' | 'dateOfFirstActivity'
| 'markets' | 'markets'
| 'name' | 'name'
| 'netPerformancePercent' | 'netPerformancePercentWithCurrencyEffect'
| 'sectors' | 'sectors'
| 'symbol' | 'symbol'
| 'url' | 'url'

Loading…
Cancel
Save