Merge branch 'main' of github.com:ghostfolio/ghostfolio
All checks were successful
Docker image CD / build_and_push (push) Successful in 21m26s

This commit is contained in:
sudacode 2025-05-04 01:46:12 -07:00
commit e4073608e5
23 changed files with 317 additions and 135 deletions

View File

@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Changed
- Deprecated the endpoint to get a portfolio position in favor of get a holding
- Deprecated the endpoint to update portfolio position tags in favor of update holding tags
## 2.159.0 - 2025-05-02
### Added
- Extended the watchlist by the date of the last all time high, the current change to the all time high and the current market condition (experimental)
- Added support for the impersonation mode in the watchlist (experimental)
### Changed
- Improved the language localization for Français (`fr`)
- Upgraded `bootstrap` from version `4.6.0` to `4.6.2`
### Fixed
- Fixed the currency code validation by allowing `GBp`
## 2.158.0 - 2025-04-30 ## 2.158.0 - 2025-04-30
### Added ### Added

View File

@ -9,7 +9,7 @@ import { ImpersonationService } from '@ghostfolio/api/services/impersonation/imp
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
import { import {
AccountBalancesResponse, AccountBalancesResponse,
Accounts AccountsResponse
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions'; import { permissions } from '@ghostfolio/common/permissions';
import type { import type {
@ -90,7 +90,7 @@ export class AccountController {
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId, @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId,
@Query('dataSource') filterByDataSource?: string, @Query('dataSource') filterByDataSource?: string,
@Query('symbol') filterBySymbol?: string @Query('symbol') filterBySymbol?: string
): Promise<Accounts> { ): Promise<AccountsResponse> {
const impersonationUserId = const impersonationUserId =
await this.impersonationService.validateImpersonationId(impersonationId); await this.impersonationService.validateImpersonationId(impersonationId);

View File

@ -2,6 +2,8 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorat
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
import { WatchlistResponse } from '@ghostfolio/common/interfaces'; import { WatchlistResponse } from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions'; import { permissions } from '@ghostfolio/common/permissions';
import { RequestWithUser } from '@ghostfolio/common/types'; import { RequestWithUser } from '@ghostfolio/common/types';
@ -11,6 +13,7 @@ import {
Controller, Controller,
Delete, Delete,
Get, Get,
Headers,
HttpException, HttpException,
Inject, Inject,
Param, Param,
@ -29,6 +32,7 @@ import { WatchlistService } from './watchlist.service';
@Controller('watchlist') @Controller('watchlist')
export class WatchlistController { export class WatchlistController {
public constructor( public constructor(
private readonly impersonationService: ImpersonationService,
@Inject(REQUEST) private readonly request: RequestWithUser, @Inject(REQUEST) private readonly request: RequestWithUser,
private readonly watchlistService: WatchlistService private readonly watchlistService: WatchlistService
) {} ) {}
@ -79,9 +83,14 @@ export class WatchlistController {
@HasPermission(permissions.readWatchlist) @HasPermission(permissions.readWatchlist)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(TransformDataSourceInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getWatchlistItems(): Promise<WatchlistResponse> { public async getWatchlistItems(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string
): Promise<WatchlistResponse> {
const impersonationUserId =
await this.impersonationService.validateImpersonationId(impersonationId);
const watchlist = await this.watchlistService.getWatchlistItems( const watchlist = await this.watchlistService.getWatchlistItems(
this.request.user.id impersonationUserId || this.request.user.id
); );
return { return {

View File

@ -1,6 +1,9 @@
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module'; import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module'; import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module';
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { DataGatheringModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module'; import { DataGatheringModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
@ -13,8 +16,11 @@ import { WatchlistService } from './watchlist.service';
@Module({ @Module({
controllers: [WatchlistController], controllers: [WatchlistController],
imports: [ imports: [
BenchmarkModule,
DataGatheringModule, DataGatheringModule,
DataProviderModule, DataProviderModule,
ImpersonationModule,
MarketDataModule,
PrismaModule, PrismaModule,
SymbolProfileModule, SymbolProfileModule,
TransformDataSourceInRequestModule, TransformDataSourceInRequestModule,

View File

@ -1,8 +1,10 @@
import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.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 { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service'; import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; import { WatchlistResponse } from '@ghostfolio/common/interfaces';
import { BadRequestException, Injectable } from '@nestjs/common'; import { BadRequestException, Injectable } from '@nestjs/common';
import { DataSource, Prisma } from '@prisma/client'; import { DataSource, Prisma } from '@prisma/client';
@ -10,8 +12,10 @@ import { DataSource, Prisma } from '@prisma/client';
@Injectable() @Injectable()
export class WatchlistService { export class WatchlistService {
public constructor( public constructor(
private readonly benchmarkService: BenchmarkService,
private readonly dataGatheringService: DataGatheringService, private readonly dataGatheringService: DataGatheringService,
private readonly dataProviderService: DataProviderService, private readonly dataProviderService: DataProviderService,
private readonly marketDataService: MarketDataService,
private readonly prismaService: PrismaService, private readonly prismaService: PrismaService,
private readonly symbolProfileService: SymbolProfileService private readonly symbolProfileService: SymbolProfileService
) {} ) {}
@ -87,7 +91,7 @@ export class WatchlistService {
public async getWatchlistItems( public async getWatchlistItems(
userId: string userId: string
): Promise<AssetProfileIdentifier[]> { ): Promise<WatchlistResponse['watchlist']> {
const user = await this.prismaService.user.findUnique({ const user = await this.prismaService.user.findUnique({
select: { select: {
watchlist: { watchlist: {
@ -97,6 +101,50 @@ export class WatchlistService {
where: { id: userId } where: { id: userId }
}); });
return user.watchlist ?? []; const [assetProfiles, quotes] = await Promise.all([
this.symbolProfileService.getSymbolProfiles(user.watchlist),
this.dataProviderService.getQuotes({
items: user.watchlist.map(({ dataSource, symbol }) => {
return { dataSource, symbol };
})
})
]);
const watchlist = await Promise.all(
user.watchlist.map(async ({ dataSource, symbol }) => {
const assetProfile = assetProfiles.find((profile) => {
return profile.dataSource === dataSource && profile.symbol === symbol;
});
const allTimeHigh = await this.marketDataService.getMax({
dataSource,
symbol
});
const performancePercent =
this.benchmarkService.calculateChangeInPercentage(
allTimeHigh?.marketPrice,
quotes[symbol]?.marketPrice
);
return {
dataSource,
symbol,
marketCondition:
this.benchmarkService.getMarketCondition(performancePercent),
name: assetProfile?.name,
performances: {
allTimeHigh: {
performancePercent,
date: allTimeHigh?.date
}
}
};
})
);
return watchlist.sort((a, b) => {
return a.name.localeCompare(b.name);
});
} }
} }

View File

@ -49,8 +49,8 @@ export class ImportService {
symbol symbol
}: AssetProfileIdentifier): Promise<Activity[]> { }: AssetProfileIdentifier): Promise<Activity[]> {
try { try {
const { firstBuyDate, historicalData, orders } = const { activities, firstBuyDate, historicalData } =
await this.portfolioService.getPosition(dataSource, undefined, symbol); await this.portfolioService.getHolding(dataSource, undefined, symbol);
const [[assetProfile], dividends] = await Promise.all([ const [[assetProfile], dividends] = await Promise.all([
this.symbolProfileService.getSymbolProfiles([ this.symbolProfileService.getSymbolProfiles([
@ -68,7 +68,7 @@ export class ImportService {
}) })
]); ]);
const accounts = orders const accounts = activities
.filter(({ Account }) => { .filter(({ Account }) => {
return !!Account; return !!Account;
}) })
@ -88,7 +88,7 @@ export class ImportService {
const value = new Big(quantity).mul(marketPrice).toNumber(); const value = new Big(quantity).mul(marketPrice).toNumber();
const date = parseDate(dateString); const date = parseDate(dateString);
const isDuplicate = orders.some((activity) => { const isDuplicate = activities.some((activity) => {
return ( return (
activity.accountId === Account?.id && activity.accountId === Account?.id &&
activity.SymbolProfile.currency === assetProfile.currency && activity.SymbolProfile.currency === assetProfile.currency &&

View File

@ -20,6 +20,7 @@ import {
import { import {
PortfolioDetails, PortfolioDetails,
PortfolioDividends, PortfolioDividends,
PortfolioHoldingResponse,
PortfolioHoldingsResponse, PortfolioHoldingsResponse,
PortfolioInvestments, PortfolioInvestments,
PortfolioPerformanceResponse, PortfolioPerformanceResponse,
@ -56,7 +57,6 @@ import { AssetClass, AssetSubClass, DataSource } from '@prisma/client';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { PortfolioHoldingDetail } from './interfaces/portfolio-holding-detail.interface';
import { PortfolioService } from './portfolio.service'; import { PortfolioService } from './portfolio.service';
import { UpdateHoldingTagsDto } from './update-holding-tags.dto'; import { UpdateHoldingTagsDto } from './update-holding-tags.dto';
@ -365,6 +365,32 @@ export class PortfolioController {
return { dividends }; return { dividends };
} }
@Get('holding/:dataSource/:symbol')
@UseInterceptors(RedactValuesInResponseInterceptor)
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getHolding(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string
): Promise<PortfolioHoldingResponse> {
const holding = await this.portfolioService.getHolding(
dataSource,
impersonationId,
symbol
);
if (!holding) {
throw new HttpException(
getReasonPhrase(StatusCodes.NOT_FOUND),
StatusCodes.NOT_FOUND
);
}
return holding;
}
@Get('holdings') @Get('holdings')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(RedactValuesInResponseInterceptor) @UseInterceptors(RedactValuesInResponseInterceptor)
@ -583,6 +609,9 @@ export class PortfolioController {
return performanceInformation; return performanceInformation;
} }
/**
* @deprecated
*/
@Get('position/:dataSource/:symbol') @Get('position/:dataSource/:symbol')
@UseInterceptors(RedactValuesInResponseInterceptor) @UseInterceptors(RedactValuesInResponseInterceptor)
@UseInterceptors(TransformDataSourceInRequestInterceptor) @UseInterceptors(TransformDataSourceInRequestInterceptor)
@ -592,8 +621,8 @@ export class PortfolioController {
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string, @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Param('dataSource') dataSource: DataSource, @Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string @Param('symbol') symbol: string
): Promise<PortfolioHoldingDetail> { ): Promise<PortfolioHoldingResponse> {
const holding = await this.portfolioService.getPosition( const holding = await this.portfolioService.getHolding(
dataSource, dataSource,
impersonationId, impersonationId,
symbol symbol
@ -634,7 +663,7 @@ export class PortfolioController {
} }
@HasPermission(permissions.updateOrder) @HasPermission(permissions.updateOrder)
@Put('position/:dataSource/:symbol/tags') @Put('holding/:dataSource/:symbol/tags')
@UseInterceptors(TransformDataSourceInRequestInterceptor) @UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async updateHoldingTags( public async updateHoldingTags(
@ -643,7 +672,42 @@ export class PortfolioController {
@Param('dataSource') dataSource: DataSource, @Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string @Param('symbol') symbol: string
): Promise<void> { ): Promise<void> {
const holding = await this.portfolioService.getPosition( const holding = await this.portfolioService.getHolding(
dataSource,
impersonationId,
symbol
);
if (!holding) {
throw new HttpException(
getReasonPhrase(StatusCodes.NOT_FOUND),
StatusCodes.NOT_FOUND
);
}
await this.portfolioService.updateTags({
dataSource,
impersonationId,
symbol,
tags: data.tags,
userId: this.request.user.id
});
}
/**
* @deprecated
*/
@HasPermission(permissions.updateOrder)
@Put('position/:dataSource/:symbol/tags')
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async updatePositionTags(
@Body() data: UpdateHoldingTagsDto,
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string
): Promise<void> {
const holding = await this.portfolioService.getHolding(
dataSource, dataSource,
impersonationId, impersonationId,
symbol symbol

View File

@ -35,12 +35,13 @@ import {
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { DATE_FORMAT, getSum, parseDate } from '@ghostfolio/common/helper'; import { DATE_FORMAT, getSum, parseDate } from '@ghostfolio/common/helper';
import { import {
Accounts, AccountsResponse,
EnhancedSymbolProfile, EnhancedSymbolProfile,
Filter, Filter,
HistoricalDataItem, HistoricalDataItem,
InvestmentItem, InvestmentItem,
PortfolioDetails, PortfolioDetails,
PortfolioHoldingResponse,
PortfolioInvestments, PortfolioInvestments,
PortfolioPerformanceResponse, PortfolioPerformanceResponse,
PortfolioPosition, PortfolioPosition,
@ -87,7 +88,6 @@ import { isEmpty } from 'lodash';
import { PortfolioCalculator } from './calculator/portfolio-calculator'; import { PortfolioCalculator } from './calculator/portfolio-calculator';
import { PortfolioCalculatorFactory } from './calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from './calculator/portfolio-calculator.factory';
import { PortfolioHoldingDetail } from './interfaces/portfolio-holding-detail.interface';
import { RulesService } from './rules.service'; import { RulesService } from './rules.service';
const asiaPacificMarkets = require('../../assets/countries/asia-pacific-markets.json'); const asiaPacificMarkets = require('../../assets/countries/asia-pacific-markets.json');
@ -209,7 +209,7 @@ export class PortfolioService {
filters?: Filter[]; filters?: Filter[];
userId: string; userId: string;
withExcludedAccounts?: boolean; withExcludedAccounts?: boolean;
}): Promise<Accounts> { }): Promise<AccountsResponse> {
const accounts = await this.getAccounts({ const accounts = await this.getAccounts({
filters, filters,
userId, userId,
@ -631,11 +631,11 @@ export class PortfolioService {
}; };
} }
public async getPosition( public async getHolding(
aDataSource: DataSource, aDataSource: DataSource,
aImpersonationId: string, aImpersonationId: string,
aSymbol: string aSymbol: string
): Promise<PortfolioHoldingDetail> { ): Promise<PortfolioHoldingResponse> {
const userId = await this.getUserId(aImpersonationId, this.request.user.id); const userId = await this.getUserId(aImpersonationId, this.request.user.id);
const user = await this.userService.user({ id: userId }); const user = await this.userService.user({ id: userId });
const userCurrency = this.getUserCurrency(user); const userCurrency = this.getUserCurrency(user);
@ -648,6 +648,7 @@ export class PortfolioService {
if (activities.length === 0) { if (activities.length === 0) {
return { return {
activities: [],
averagePrice: undefined, averagePrice: undefined,
dataProviderInfo: undefined, dataProviderInfo: undefined,
dividendInBaseCurrency: undefined, dividendInBaseCurrency: undefined,
@ -662,13 +663,12 @@ export class PortfolioService {
historicalData: [], historicalData: [],
investment: undefined, investment: undefined,
marketPrice: undefined, marketPrice: undefined,
maxPrice: undefined, marketPriceMax: undefined,
minPrice: undefined, marketPriceMin: undefined,
netPerformance: undefined, netPerformance: undefined,
netPerformancePercent: undefined, netPerformancePercent: undefined,
netPerformancePercentWithCurrencyEffect: undefined, netPerformancePercentWithCurrencyEffect: undefined,
netPerformanceWithCurrencyEffect: undefined, netPerformanceWithCurrencyEffect: undefined,
orders: [],
quantity: undefined, quantity: undefined,
SymbolProfile: undefined, SymbolProfile: undefined,
tags: [], tags: [],
@ -714,7 +714,7 @@ export class PortfolioService {
transactionCount transactionCount
} = position; } = position;
const activitiesOfPosition = activities.filter(({ SymbolProfile }) => { const activitiesOfHolding = activities.filter(({ SymbolProfile }) => {
return ( return (
SymbolProfile.dataSource === dataSource && SymbolProfile.dataSource === dataSource &&
SymbolProfile.symbol === symbol SymbolProfile.symbol === symbol
@ -748,12 +748,12 @@ export class PortfolioService {
); );
const historicalDataArray: HistoricalDataItem[] = []; const historicalDataArray: HistoricalDataItem[] = [];
let maxPrice = Math.max( let marketPriceMax = Math.max(
activitiesOfPosition[0].unitPriceInAssetProfileCurrency, activitiesOfHolding[0].unitPriceInAssetProfileCurrency,
marketPrice marketPrice
); );
let minPrice = Math.min( let marketPriceMin = Math.min(
activitiesOfPosition[0].unitPriceInAssetProfileCurrency, activitiesOfHolding[0].unitPriceInAssetProfileCurrency,
marketPrice marketPrice
); );
@ -793,27 +793,31 @@ export class PortfolioService {
quantity: currentQuantity quantity: currentQuantity
}); });
maxPrice = Math.max(marketPrice ?? 0, maxPrice); marketPriceMax = Math.max(marketPrice ?? 0, marketPriceMax);
minPrice = Math.min(marketPrice ?? Number.MAX_SAFE_INTEGER, minPrice); marketPriceMin = Math.min(
marketPrice ?? Number.MAX_SAFE_INTEGER,
marketPriceMin
);
} }
} else { } else {
// Add historical entry for buy date, if no historical data available // Add historical entry for buy date, if no historical data available
historicalDataArray.push({ historicalDataArray.push({
averagePrice: activitiesOfPosition[0].unitPriceInAssetProfileCurrency, averagePrice: activitiesOfHolding[0].unitPriceInAssetProfileCurrency,
date: firstBuyDate, date: firstBuyDate,
marketPrice: activitiesOfPosition[0].unitPriceInAssetProfileCurrency, marketPrice: activitiesOfHolding[0].unitPriceInAssetProfileCurrency,
quantity: activitiesOfPosition[0].quantity quantity: activitiesOfHolding[0].quantity
}); });
} }
return { return {
firstBuyDate, firstBuyDate,
marketPrice, marketPrice,
maxPrice, marketPriceMax,
minPrice, marketPriceMin,
SymbolProfile, SymbolProfile,
tags, tags,
transactionCount, transactionCount,
activities: activitiesOfHolding,
averagePrice: averagePrice.toNumber(), averagePrice: averagePrice.toNumber(),
dataProviderInfo: portfolioCalculator.getDataProviderInfos()?.[0], dataProviderInfo: portfolioCalculator.getDataProviderInfos()?.[0],
dividendInBaseCurrency: dividendInBaseCurrency.toNumber(), dividendInBaseCurrency: dividendInBaseCurrency.toNumber(),
@ -842,7 +846,6 @@ export class PortfolioService {
]?.toNumber(), ]?.toNumber(),
netPerformanceWithCurrencyEffect: netPerformanceWithCurrencyEffect:
position.netPerformanceWithCurrencyEffectMap?.['max']?.toNumber(), position.netPerformanceWithCurrencyEffectMap?.['max']?.toNumber(),
orders: activitiesOfPosition,
quantity: quantity.toNumber(), quantity: quantity.toNumber(),
value: this.exchangeRateDataService.toCurrency( value: this.exchangeRateDataService.toCurrency(
quantity.mul(marketPrice ?? 0).toNumber(), quantity.mul(marketPrice ?? 0).toNumber(),
@ -881,8 +884,8 @@ export class PortfolioService {
} }
const historicalDataArray: HistoricalDataItem[] = []; const historicalDataArray: HistoricalDataItem[] = [];
let maxPrice = marketPrice; let marketPriceMax = marketPrice;
let minPrice = marketPrice; let marketPriceMin = marketPrice;
for (const [date, { marketPrice }] of Object.entries( for (const [date, { marketPrice }] of Object.entries(
historicalData[aSymbol] historicalData[aSymbol]
@ -892,15 +895,19 @@ export class PortfolioService {
value: marketPrice value: marketPrice
}); });
maxPrice = Math.max(marketPrice ?? 0, maxPrice); marketPriceMax = Math.max(marketPrice ?? 0, marketPriceMax);
minPrice = Math.min(marketPrice ?? Number.MAX_SAFE_INTEGER, minPrice); marketPriceMin = Math.min(
marketPrice ?? Number.MAX_SAFE_INTEGER,
marketPriceMin
);
} }
return { return {
marketPrice, marketPrice,
maxPrice, marketPriceMax,
minPrice, marketPriceMin,
SymbolProfile, SymbolProfile,
activities: [],
averagePrice: 0, averagePrice: 0,
dataProviderInfo: undefined, dataProviderInfo: undefined,
dividendInBaseCurrency: 0, dividendInBaseCurrency: 0,
@ -918,7 +925,6 @@ export class PortfolioService {
netPerformancePercent: undefined, netPerformancePercent: undefined,
netPerformancePercentWithCurrencyEffect: undefined, netPerformancePercentWithCurrencyEffect: undefined,
netPerformanceWithCurrencyEffect: undefined, netPerformanceWithCurrencyEffect: undefined,
orders: [],
quantity: 0, quantity: 0,
tags: [], tags: [],
transactionCount: undefined, transactionCount: undefined,
@ -927,7 +933,7 @@ export class PortfolioService {
} }
} }
public async getPositions({ public async getHoldings({
dateRange = 'max', dateRange = 'max',
filters, filters,
impersonationId impersonationId

View File

@ -212,6 +212,18 @@ export class BenchmarkService {
}; };
} }
public getMarketCondition(
aPerformanceInPercent: number
): Benchmark['marketCondition'] {
if (aPerformanceInPercent >= 0) {
return 'ALL_TIME_HIGH';
} else if (aPerformanceInPercent <= -0.2) {
return 'BEAR_MARKET';
} else {
return 'NEUTRAL_MARKET';
}
}
private async calculateAndCacheBenchmarks({ private async calculateAndCacheBenchmarks({
enableSharing = false enableSharing = false
}): Promise<BenchmarkResponse['benchmarks']> { }): Promise<BenchmarkResponse['benchmarks']> {
@ -302,16 +314,4 @@ export class BenchmarkService {
return benchmarks; return benchmarks;
} }
private getMarketCondition(
aPerformanceInPercent: number
): Benchmark['marketCondition'] {
if (aPerformanceInPercent >= 0) {
return 'ALL_TIME_HIGH';
} else if (aPerformanceInPercent <= -0.2) {
return 'BEAR_MARKET';
} else {
return 'NEUTRAL_MARKET';
}
}
} }

View File

@ -1,4 +1,4 @@
import { DERIVED_CURRENCIES } from '@ghostfolio/common/config'; import { isDerivedCurrency } from '@ghostfolio/common/helper';
import { import {
registerDecorator, registerDecorator,
@ -28,17 +28,11 @@ export class IsExtendedCurrencyConstraint
return '$property must be a valid ISO4217 currency code'; return '$property must be a valid ISO4217 currency code';
} }
public validate(currency: any) { public validate(currency: string) {
// Return true if currency is a standard ISO 4217 code or a derived currency // Return true if currency is a derived currency or a standard ISO 4217 code
return ( return (
this.isUpperCase(currency) && isDerivedCurrency(currency) ||
(isISO4217CurrencyCode(currency) || (this.isUpperCase(currency) && isISO4217CurrencyCode(currency))
[
...DERIVED_CURRENCIES.map((derivedCurrency) => {
return derivedCurrency.currency;
}),
'USX'
].includes(currency))
); );
} }

View File

@ -105,8 +105,8 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
public investmentPrecision = 2; public investmentPrecision = 2;
public marketDataItems: MarketData[] = []; public marketDataItems: MarketData[] = [];
public marketPrice: number; public marketPrice: number;
public maxPrice: number; public marketPriceMax: number;
public minPrice: number; public marketPriceMin: number;
public netPerformance: number; public netPerformance: number;
public netPerformancePrecision = 2; public netPerformancePrecision = 2;
public netPerformancePercent: number; public netPerformancePercent: number;
@ -234,8 +234,8 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
historicalData, historicalData,
investment, investment,
marketPrice, marketPrice,
maxPrice, marketPriceMax,
minPrice, marketPriceMin,
netPerformance, netPerformance,
netPerformancePercent, netPerformancePercent,
netPerformancePercentWithCurrencyEffect, netPerformancePercentWithCurrencyEffect,
@ -297,8 +297,8 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
} }
this.marketPrice = marketPrice; this.marketPrice = marketPrice;
this.maxPrice = maxPrice; this.marketPriceMax = marketPriceMax;
this.minPrice = minPrice; this.marketPriceMin = marketPriceMin;
this.netPerformance = netPerformance; this.netPerformance = netPerformance;
if ( if (

View File

@ -106,11 +106,11 @@
[locale]="data.locale" [locale]="data.locale"
[ngClass]="{ [ngClass]="{
'text-danger': 'text-danger':
minPrice?.toFixed(2) === marketPrice?.toFixed(2) && marketPriceMin?.toFixed(2) === marketPrice?.toFixed(2) &&
maxPrice?.toFixed(2) !== minPrice?.toFixed(2) marketPriceMax?.toFixed(2) !== marketPriceMin?.toFixed(2)
}" }"
[unit]="SymbolProfile?.currency" [unit]="SymbolProfile?.currency"
[value]="minPrice" [value]="marketPriceMin"
>Minimum Price</gf-value >Minimum Price</gf-value
> >
</div> </div>
@ -122,11 +122,11 @@
[locale]="data.locale" [locale]="data.locale"
[ngClass]="{ [ngClass]="{
'text-success': 'text-success':
maxPrice?.toFixed(2) === marketPrice?.toFixed(2) && marketPriceMax?.toFixed(2) === marketPrice?.toFixed(2) &&
maxPrice?.toFixed(2) !== minPrice?.toFixed(2) marketPriceMax?.toFixed(2) !== marketPriceMin?.toFixed(2)
}" }"
[unit]="SymbolProfile?.currency" [unit]="SymbolProfile?.currency"
[value]="maxPrice" [value]="marketPriceMax"
>Maximum Price</gf-value >Maximum Price</gf-value
> >
</div> </div>

View File

@ -1,4 +1,5 @@
import { DataService } from '@ghostfolio/client/services/data.service'; 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 { UserService } from '@ghostfolio/client/services/user/user.service';
import { import {
AssetProfileIdentifier, AssetProfileIdentifier,
@ -6,6 +7,7 @@ import {
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { BenchmarkTrend } from '@ghostfolio/common/types';
import { GfBenchmarkComponent } from '@ghostfolio/ui/benchmark'; import { GfBenchmarkComponent } from '@ghostfolio/ui/benchmark';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
@ -44,6 +46,7 @@ import { CreateWatchlistItemDialogParams } from './create-watchlist-item-dialog/
}) })
export class HomeWatchlistComponent implements OnDestroy, OnInit { export class HomeWatchlistComponent implements OnDestroy, OnInit {
public deviceType: string; public deviceType: string;
public hasImpersonationId: boolean;
public hasPermissionToCreateWatchlistItem: boolean; public hasPermissionToCreateWatchlistItem: boolean;
public hasPermissionToDeleteWatchlistItem: boolean; public hasPermissionToDeleteWatchlistItem: boolean;
public user: User; public user: User;
@ -56,12 +59,20 @@ export class HomeWatchlistComponent implements OnDestroy, OnInit {
private dataService: DataService, private dataService: DataService,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private dialog: MatDialog, private dialog: MatDialog,
private impersonationStorageService: ImpersonationStorageService,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private userService: UserService private userService: UserService
) { ) {
this.deviceType = this.deviceService.getDeviceInfo().deviceType; this.deviceType = this.deviceService.getDeviceInfo().deviceType;
this.impersonationStorageService
.onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId;
});
this.route.queryParams this.route.queryParams
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((params) => { .subscribe((params) => {
@ -76,14 +87,18 @@ export class HomeWatchlistComponent implements OnDestroy, OnInit {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
this.hasPermissionToCreateWatchlistItem = hasPermission( this.hasPermissionToCreateWatchlistItem =
this.user.permissions, !this.hasImpersonationId &&
permissions.createWatchlistItem hasPermission(
); this.user.permissions,
this.hasPermissionToDeleteWatchlistItem = hasPermission( permissions.createWatchlistItem
this.user.permissions, );
permissions.deleteWatchlistItem this.hasPermissionToDeleteWatchlistItem =
); !this.hasImpersonationId &&
hasPermission(
this.user.permissions,
permissions.deleteWatchlistItem
);
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
} }
@ -118,15 +133,17 @@ export class HomeWatchlistComponent implements OnDestroy, OnInit {
.fetchWatchlist() .fetchWatchlist()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ watchlist }) => { .subscribe(({ watchlist }) => {
this.watchlist = watchlist.map(({ dataSource, symbol }) => ({ this.watchlist = watchlist.map(
dataSource, ({ dataSource, marketCondition, name, performances, symbol }) => ({
symbol, dataSource,
marketCondition: null, marketCondition,
name: symbol, name,
performances: null, performances,
trend50d: 'UNKNOWN', symbol,
trend200d: 'UNKNOWN' trend50d: 'UNKNOWN' as BenchmarkTrend,
})); trend200d: 'UNKNOWN' as BenchmarkTrend
})
);
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });

View File

@ -7,7 +7,7 @@
} }
</span> </span>
</h1> </h1>
<div class="mb-3 row"> <div class="row">
<div class="col-xs-12 col-md-8 offset-md-2"> <div class="col-xs-12 col-md-8 offset-md-2">
<gf-benchmark <gf-benchmark
[benchmarks]="watchlist" [benchmarks]="watchlist"
@ -20,7 +20,7 @@
</div> </div>
</div> </div>
</div> </div>
@if (hasPermissionToCreateWatchlistItem) { @if (!hasImpersonationId && hasPermissionToCreateWatchlistItem) {
<div class="fab-container"> <div class="fab-container">
<a <a
class="align-items-center d-flex justify-content-center" class="align-items-center d-flex justify-content-center"

View File

@ -13,7 +13,6 @@ import {
Activity Activity
} from '@ghostfolio/api/app/order/interfaces/activities.interface'; } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto'; import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
import { PortfolioHoldingDetail } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-holding-detail.interface';
import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface'; import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface';
import { DeleteOwnUserDto } from '@ghostfolio/api/app/user/delete-own-user.dto'; import { DeleteOwnUserDto } from '@ghostfolio/api/app/user/delete-own-user.dto';
import { UserItem } from '@ghostfolio/api/app/user/interfaces/user-item.interface'; import { UserItem } from '@ghostfolio/api/app/user/interfaces/user-item.interface';
@ -25,7 +24,7 @@ import {
Access, Access,
AccessTokenResponse, AccessTokenResponse,
AccountBalancesResponse, AccountBalancesResponse,
Accounts, AccountsResponse,
AiPromptResponse, AiPromptResponse,
ApiKeyResponse, ApiKeyResponse,
AssetProfileIdentifier, AssetProfileIdentifier,
@ -40,6 +39,7 @@ import {
OAuthResponse, OAuthResponse,
PortfolioDetails, PortfolioDetails,
PortfolioDividends, PortfolioDividends,
PortfolioHoldingResponse,
PortfolioHoldingsResponse, PortfolioHoldingsResponse,
PortfolioInvestments, PortfolioInvestments,
PortfolioPerformanceResponse, PortfolioPerformanceResponse,
@ -191,7 +191,7 @@ export class DataService {
public fetchAccounts({ filters }: { filters?: Filter[] } = {}) { public fetchAccounts({ filters }: { filters?: Filter[] } = {}) {
const params = this.buildFiltersAsQueryParams({ filters }); const params = this.buildFiltersAsQueryParams({ filters });
return this.http.get<Accounts>('/api/v1/account', { params }); return this.http.get<AccountsResponse>('/api/v1/account', { params });
} }
public fetchActivities({ public fetchActivities({
@ -406,13 +406,13 @@ export class DataService {
symbol: string; symbol: string;
}) { }) {
return this.http return this.http
.get<PortfolioHoldingDetail>( .get<PortfolioHoldingResponse>(
`/api/v1/portfolio/position/${dataSource}/${symbol}` `/api/v1/portfolio/holding/${dataSource}/${symbol}`
) )
.pipe( .pipe(
map((data) => { map((data) => {
if (data.orders) { if (data.activities) {
for (const order of data.orders) { for (const order of data.activities) {
order.createdAt = parseISO(order.createdAt as unknown as string); order.createdAt = parseISO(order.createdAt as unknown as string);
order.date = parseISO(order.date as unknown as string); order.date = parseISO(order.date as unknown as string);
} }
@ -776,7 +776,7 @@ export class DataService {
tags tags
}: { tags: Tag[] } & AssetProfileIdentifier) { }: { tags: Tag[] } & AssetProfileIdentifier) {
return this.http.put<void>( return this.http.put<void>(
`/api/v1/portfolio/position/${dataSource}/${symbol}/tags`, `/api/v1/portfolio/holding/${dataSource}/${symbol}/tags`,
{ tags } { tags }
); );
} }

View File

@ -2831,7 +2831,7 @@
</trans-unit> </trans-unit>
<trans-unit id="a4a68fbb5bf56e4bccaf5e73ba2d9567f754e7ca" datatype="html"> <trans-unit id="a4a68fbb5bf56e4bccaf5e73ba2d9567f754e7ca" datatype="html">
<source> Hello, <x id="INTERPOLATION" equiv-text="{{ publicPortfolioDetails?.alias ?? defaultAlias }}"/> has shared a <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="&lt;strong&gt;"/>Portfolio<x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="&lt;/strong&gt;"/> with you! </source> <source> Hello, <x id="INTERPOLATION" equiv-text="{{ publicPortfolioDetails?.alias ?? defaultAlias }}"/> has shared a <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="&lt;strong&gt;"/>Portfolio<x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="&lt;/strong&gt;"/> with you! </source>
<target state="new"> Bonjour, <x id="INTERPOLATION" equiv-text="{{ portfolioPublicDetails?.alias ?? defaultAlias }}"/> a partagé un <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="&lt;strong&gt;"/>Portefeuille<x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="&lt;/strong&gt;"/> avec vous ! </target> <target state="translated"> Bonjour, <x id="INTERPOLATION" equiv-text="{{ portfolioPublicDetails?.alias ?? defaultAlias }}"/> a partagé un <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="&lt;strong&gt;"/>Portefeuille<x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="&lt;/strong&gt;"/> avec vous ! </target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
<context context-type="linenumber">4</context> <context context-type="linenumber">4</context>
@ -7931,7 +7931,7 @@
</trans-unit> </trans-unit>
<trans-unit id="1efa64b89c9852e7099159ab06af9dcf49870438" datatype="html"> <trans-unit id="1efa64b89c9852e7099159ab06af9dcf49870438" datatype="html">
<source>Add asset to watchlist</source> <source>Add asset to watchlist</source>
<target state="new">Add asset to watchlist</target> <target state="translated">Ajouter un actif à la liste de suivi</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/home-watchlist/create-watchlist-item-dialog/create-watchlist-item-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/components/home-watchlist/create-watchlist-item-dialog/create-watchlist-item-dialog.html</context>
<context context-type="linenumber">7</context> <context context-type="linenumber">7</context>
@ -7939,7 +7939,7 @@
</trans-unit> </trans-unit>
<trans-unit id="7a6d28bd1c36c8298c95b7965abf226b218be50d" datatype="html"> <trans-unit id="7a6d28bd1c36c8298c95b7965abf226b218be50d" datatype="html">
<source>Watchlist</source> <source>Watchlist</source>
<target state="new">Watchlist</target> <target state="translated">Liste de suivi</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/home-watchlist/home-watchlist.html</context> <context context-type="sourcefile">apps/client/src/app/components/home-watchlist/home-watchlist.html</context>
<context context-type="linenumber">4</context> <context context-type="linenumber">4</context>
@ -7947,7 +7947,7 @@
</trans-unit> </trans-unit>
<trans-unit id="4558213855845176930" datatype="html"> <trans-unit id="4558213855845176930" datatype="html">
<source>Watchlist</source> <source>Watchlist</source>
<target state="new">Watchlist</target> <target state="translated">Liste de suivi</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/home/home-page-routing.module.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/home/home-page-routing.module.ts</context>
<context context-type="linenumber">44</context> <context context-type="linenumber">44</context>
@ -7959,7 +7959,7 @@
</trans-unit> </trans-unit>
<trans-unit id="e9d59bb8bf6c08243d5411c55ddbdf925c7c799c" datatype="html"> <trans-unit id="e9d59bb8bf6c08243d5411c55ddbdf925c7c799c" datatype="html">
<source>Get Early Access</source> <source>Get Early Access</source>
<target state="new">Get Early Access</target> <target state="translated">Obtenir un accès anticipé</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html</context>
<context context-type="linenumber">29</context> <context context-type="linenumber">29</context>
@ -7967,7 +7967,7 @@
</trans-unit> </trans-unit>
<trans-unit id="627795342008207050" datatype="html"> <trans-unit id="627795342008207050" datatype="html">
<source>Do you really want to delete this item?</source> <source>Do you really want to delete this item?</source>
<target state="new">Do you really want to delete this item?</target> <target state="translated">Voulez-vous vraiment supprimer cet élément?</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/benchmark/benchmark.component.ts</context> <context context-type="sourcefile">libs/ui/src/lib/benchmark/benchmark.component.ts</context>
<context context-type="linenumber">122</context> <context context-type="linenumber">122</context>

View File

@ -349,7 +349,7 @@ export function isDerivedCurrency(aCurrency: string) {
return true; return true;
} }
return DERIVED_CURRENCIES.find(({ currency }) => { return DERIVED_CURRENCIES.some(({ currency }) => {
return currency === aCurrency; return currency === aCurrency;
}); });
} }

View File

@ -1,6 +1,5 @@
import type { Access } from './access.interface'; import type { Access } from './access.interface';
import type { AccountBalance } from './account-balance.interface'; import type { AccountBalance } from './account-balance.interface';
import type { Accounts } from './accounts.interface';
import type { AdminData } from './admin-data.interface'; import type { AdminData } from './admin-data.interface';
import type { AdminJobs } from './admin-jobs.interface'; import type { AdminJobs } from './admin-jobs.interface';
import type { AdminMarketDataDetails } from './admin-market-data-details.interface'; import type { AdminMarketDataDetails } from './admin-market-data-details.interface';
@ -40,6 +39,7 @@ import type { Position } from './position.interface';
import type { Product } from './product'; import type { Product } from './product';
import type { AccessTokenResponse } from './responses/access-token-response.interface'; import type { AccessTokenResponse } from './responses/access-token-response.interface';
import type { AccountBalancesResponse } from './responses/account-balances-response.interface'; import type { AccountBalancesResponse } from './responses/account-balances-response.interface';
import type { AccountsResponse } from './responses/accounts-response.interface';
import type { AiPromptResponse } from './responses/ai-prompt-response.interface'; import type { AiPromptResponse } from './responses/ai-prompt-response.interface';
import type { ApiKeyResponse } from './responses/api-key-response.interface'; import type { ApiKeyResponse } from './responses/api-key-response.interface';
import type { BenchmarkResponse } from './responses/benchmark-response.interface'; import type { BenchmarkResponse } from './responses/benchmark-response.interface';
@ -52,6 +52,7 @@ import type { ImportResponse } from './responses/import-response.interface';
import type { LookupResponse } from './responses/lookup-response.interface'; import type { LookupResponse } from './responses/lookup-response.interface';
import type { MarketDataDetailsResponse } from './responses/market-data-details-response.interface'; import type { MarketDataDetailsResponse } from './responses/market-data-details-response.interface';
import type { OAuthResponse } from './responses/oauth-response.interface'; import type { OAuthResponse } from './responses/oauth-response.interface';
import { PortfolioHoldingResponse } from './responses/portfolio-holding-response.interface';
import type { PortfolioHoldingsResponse } from './responses/portfolio-holdings-response.interface'; import type { PortfolioHoldingsResponse } from './responses/portfolio-holdings-response.interface';
import type { PortfolioPerformanceResponse } from './responses/portfolio-performance-response.interface'; import type { PortfolioPerformanceResponse } from './responses/portfolio-performance-response.interface';
import type { PortfolioReportResponse } from './responses/portfolio-report.interface'; import type { PortfolioReportResponse } from './responses/portfolio-report.interface';
@ -74,7 +75,7 @@ export {
AccessTokenResponse, AccessTokenResponse,
AccountBalance, AccountBalance,
AccountBalancesResponse, AccountBalancesResponse,
Accounts, AccountsResponse,
AdminData, AdminData,
AdminJobs, AdminJobs,
AdminMarketData, AdminMarketData,
@ -112,6 +113,7 @@ export {
PortfolioChart, PortfolioChart,
PortfolioDetails, PortfolioDetails,
PortfolioDividends, PortfolioDividends,
PortfolioHoldingResponse,
PortfolioHoldingsResponse, PortfolioHoldingsResponse,
PortfolioInvestments, PortfolioInvestments,
PortfolioItem, PortfolioItem,

View File

@ -1,6 +1,6 @@
import { AccountWithValue } from '@ghostfolio/common/types'; import { AccountWithValue } from '@ghostfolio/common/types';
export interface Accounts { export interface AccountsResponse {
accounts: AccountWithValue[]; accounts: AccountWithValue[];
totalBalanceInBaseCurrency: number; totalBalanceInBaseCurrency: number;
totalValueInBaseCurrency: number; totalValueInBaseCurrency: number;

View File

@ -7,7 +7,8 @@ import {
import { Tag } from '@prisma/client'; import { Tag } from '@prisma/client';
export interface PortfolioHoldingDetail { export interface PortfolioHoldingResponse {
activities: Activity[];
averagePrice: number; averagePrice: number;
dataProviderInfo: DataProviderInfo; dataProviderInfo: DataProviderInfo;
dividendInBaseCurrency: number; dividendInBaseCurrency: number;
@ -22,13 +23,12 @@ export interface PortfolioHoldingDetail {
historicalData: HistoricalDataItem[]; historicalData: HistoricalDataItem[];
investment: number; investment: number;
marketPrice: number; marketPrice: number;
maxPrice: number; marketPriceMax: number;
minPrice: number; marketPriceMin: number;
netPerformance: number; netPerformance: number;
netPerformancePercent: number; netPerformancePercent: number;
netPerformancePercentWithCurrencyEffect: number; netPerformancePercentWithCurrencyEffect: number;
netPerformanceWithCurrencyEffect: number; netPerformanceWithCurrencyEffect: number;
orders: Activity[];
quantity: number; quantity: number;
SymbolProfile: EnhancedSymbolProfile; SymbolProfile: EnhancedSymbolProfile;
tags: Tag[]; tags: Tag[];

View File

@ -1,5 +1,12 @@
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; import {
AssetProfileIdentifier,
Benchmark
} from '@ghostfolio/common/interfaces';
export interface WatchlistResponse { export interface WatchlistResponse {
watchlist: AssetProfileIdentifier[]; watchlist: (AssetProfileIdentifier & {
marketCondition: Benchmark['marketCondition'];
name: string;
performances: Benchmark['performances'];
})[];
} }

26
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.158.0", "version": "2.159.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.158.0", "version": "2.159.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
@ -47,7 +47,7 @@
"@stripe/stripe-js": "5.4.0", "@stripe/stripe-js": "5.4.0",
"alphavantage": "2.2.0", "alphavantage": "2.2.0",
"big.js": "6.2.2", "big.js": "6.2.2",
"bootstrap": "4.6.0", "bootstrap": "4.6.2",
"bull": "4.16.5", "bull": "4.16.5",
"cache-manager": "5.7.6", "cache-manager": "5.7.6",
"cache-manager-redis-yet": "5.1.4", "cache-manager-redis-yet": "5.1.4",
@ -14322,14 +14322,20 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/bootstrap": { "node_modules/bootstrap": {
"version": "4.6.0", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.0.tgz", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz",
"integrity": "sha512-Io55IuQY3kydzHtbGvQya3H+KorS/M9rSNyfCGCg9WZ4pyT/lCxIlpJgG1GXW/PswzC84Tr2fBYi+7+jFVQQBw==", "integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/twbs"
},
{
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
}
],
"license": "MIT", "license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
},
"peerDependencies": { "peerDependencies": {
"jquery": "1.9.1 - 3", "jquery": "1.9.1 - 3",
"popper.js": "^1.16.1" "popper.js": "^1.16.1"

View File

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.158.0", "version": "2.159.0",
"homepage": "https://ghostfol.io", "homepage": "https://ghostfol.io",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio", "repository": "https://github.com/ghostfolio/ghostfolio",
@ -93,7 +93,7 @@
"@stripe/stripe-js": "5.4.0", "@stripe/stripe-js": "5.4.0",
"alphavantage": "2.2.0", "alphavantage": "2.2.0",
"big.js": "6.2.2", "big.js": "6.2.2",
"bootstrap": "4.6.0", "bootstrap": "4.6.2",
"bull": "4.16.5", "bull": "4.16.5",
"cache-manager": "5.7.6", "cache-manager": "5.7.6",
"cache-manager-redis-yet": "5.1.4", "cache-manager-redis-yet": "5.1.4",