Merge branch 'main' of github.com:ghostfolio/ghostfolio
All checks were successful
Docker image CD / build_and_push (push) Successful in 21m26s
All checks were successful
Docker image CD / build_and_push (push) Successful in 21m26s
This commit is contained in:
commit
e4073608e5
23
CHANGELOG.md
23
CHANGELOG.md
@ -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/),
|
||||
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
|
||||
|
||||
### Added
|
||||
|
@ -9,7 +9,7 @@ import { ImpersonationService } from '@ghostfolio/api/services/impersonation/imp
|
||||
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
|
||||
import {
|
||||
AccountBalancesResponse,
|
||||
Accounts
|
||||
AccountsResponse
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { permissions } from '@ghostfolio/common/permissions';
|
||||
import type {
|
||||
@ -90,7 +90,7 @@ export class AccountController {
|
||||
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId,
|
||||
@Query('dataSource') filterByDataSource?: string,
|
||||
@Query('symbol') filterBySymbol?: string
|
||||
): Promise<Accounts> {
|
||||
): Promise<AccountsResponse> {
|
||||
const impersonationUserId =
|
||||
await this.impersonationService.validateImpersonationId(impersonationId);
|
||||
|
||||
|
@ -2,6 +2,8 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorat
|
||||
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 { 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 { permissions } from '@ghostfolio/common/permissions';
|
||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
||||
@ -11,6 +13,7 @@ import {
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Headers,
|
||||
HttpException,
|
||||
Inject,
|
||||
Param,
|
||||
@ -29,6 +32,7 @@ import { WatchlistService } from './watchlist.service';
|
||||
@Controller('watchlist')
|
||||
export class WatchlistController {
|
||||
public constructor(
|
||||
private readonly impersonationService: ImpersonationService,
|
||||
@Inject(REQUEST) private readonly request: RequestWithUser,
|
||||
private readonly watchlistService: WatchlistService
|
||||
) {}
|
||||
@ -79,9 +83,14 @@ export class WatchlistController {
|
||||
@HasPermission(permissions.readWatchlist)
|
||||
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
||||
@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(
|
||||
this.request.user.id
|
||||
impersonationUserId || this.request.user.id
|
||||
);
|
||||
|
||||
return {
|
||||
|
@ -1,6 +1,9 @@
|
||||
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 { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.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 { DataGatheringModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module';
|
||||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
||||
@ -13,8 +16,11 @@ import { WatchlistService } from './watchlist.service';
|
||||
@Module({
|
||||
controllers: [WatchlistController],
|
||||
imports: [
|
||||
BenchmarkModule,
|
||||
DataGatheringModule,
|
||||
DataProviderModule,
|
||||
ImpersonationModule,
|
||||
MarketDataModule,
|
||||
PrismaModule,
|
||||
SymbolProfileModule,
|
||||
TransformDataSourceInRequestModule,
|
||||
|
@ -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 { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.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 { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
|
||||
import { WatchlistResponse } from '@ghostfolio/common/interfaces';
|
||||
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { DataSource, Prisma } from '@prisma/client';
|
||||
@ -10,8 +12,10 @@ import { DataSource, Prisma } from '@prisma/client';
|
||||
@Injectable()
|
||||
export class WatchlistService {
|
||||
public constructor(
|
||||
private readonly benchmarkService: BenchmarkService,
|
||||
private readonly dataGatheringService: DataGatheringService,
|
||||
private readonly dataProviderService: DataProviderService,
|
||||
private readonly marketDataService: MarketDataService,
|
||||
private readonly prismaService: PrismaService,
|
||||
private readonly symbolProfileService: SymbolProfileService
|
||||
) {}
|
||||
@ -87,7 +91,7 @@ export class WatchlistService {
|
||||
|
||||
public async getWatchlistItems(
|
||||
userId: string
|
||||
): Promise<AssetProfileIdentifier[]> {
|
||||
): Promise<WatchlistResponse['watchlist']> {
|
||||
const user = await this.prismaService.user.findUnique({
|
||||
select: {
|
||||
watchlist: {
|
||||
@ -97,6 +101,50 @@ export class WatchlistService {
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -49,8 +49,8 @@ export class ImportService {
|
||||
symbol
|
||||
}: AssetProfileIdentifier): Promise<Activity[]> {
|
||||
try {
|
||||
const { firstBuyDate, historicalData, orders } =
|
||||
await this.portfolioService.getPosition(dataSource, undefined, symbol);
|
||||
const { activities, firstBuyDate, historicalData } =
|
||||
await this.portfolioService.getHolding(dataSource, undefined, symbol);
|
||||
|
||||
const [[assetProfile], dividends] = await Promise.all([
|
||||
this.symbolProfileService.getSymbolProfiles([
|
||||
@ -68,7 +68,7 @@ export class ImportService {
|
||||
})
|
||||
]);
|
||||
|
||||
const accounts = orders
|
||||
const accounts = activities
|
||||
.filter(({ Account }) => {
|
||||
return !!Account;
|
||||
})
|
||||
@ -88,7 +88,7 @@ export class ImportService {
|
||||
const value = new Big(quantity).mul(marketPrice).toNumber();
|
||||
|
||||
const date = parseDate(dateString);
|
||||
const isDuplicate = orders.some((activity) => {
|
||||
const isDuplicate = activities.some((activity) => {
|
||||
return (
|
||||
activity.accountId === Account?.id &&
|
||||
activity.SymbolProfile.currency === assetProfile.currency &&
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
import {
|
||||
PortfolioDetails,
|
||||
PortfolioDividends,
|
||||
PortfolioHoldingResponse,
|
||||
PortfolioHoldingsResponse,
|
||||
PortfolioInvestments,
|
||||
PortfolioPerformanceResponse,
|
||||
@ -56,7 +57,6 @@ import { AssetClass, AssetSubClass, DataSource } from '@prisma/client';
|
||||
import { Big } from 'big.js';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
|
||||
import { PortfolioHoldingDetail } from './interfaces/portfolio-holding-detail.interface';
|
||||
import { PortfolioService } from './portfolio.service';
|
||||
import { UpdateHoldingTagsDto } from './update-holding-tags.dto';
|
||||
|
||||
@ -365,6 +365,32 @@ export class PortfolioController {
|
||||
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')
|
||||
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
||||
@UseInterceptors(RedactValuesInResponseInterceptor)
|
||||
@ -583,6 +609,9 @@ export class PortfolioController {
|
||||
return performanceInformation;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@Get('position/:dataSource/:symbol')
|
||||
@UseInterceptors(RedactValuesInResponseInterceptor)
|
||||
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||
@ -592,8 +621,8 @@ export class PortfolioController {
|
||||
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
|
||||
@Param('dataSource') dataSource: DataSource,
|
||||
@Param('symbol') symbol: string
|
||||
): Promise<PortfolioHoldingDetail> {
|
||||
const holding = await this.portfolioService.getPosition(
|
||||
): Promise<PortfolioHoldingResponse> {
|
||||
const holding = await this.portfolioService.getHolding(
|
||||
dataSource,
|
||||
impersonationId,
|
||||
symbol
|
||||
@ -634,7 +663,7 @@ export class PortfolioController {
|
||||
}
|
||||
|
||||
@HasPermission(permissions.updateOrder)
|
||||
@Put('position/:dataSource/:symbol/tags')
|
||||
@Put('holding/:dataSource/:symbol/tags')
|
||||
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
||||
public async updateHoldingTags(
|
||||
@ -643,7 +672,42 @@ export class PortfolioController {
|
||||
@Param('dataSource') dataSource: DataSource,
|
||||
@Param('symbol') symbol: string
|
||||
): 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,
|
||||
impersonationId,
|
||||
symbol
|
||||
|
@ -35,12 +35,13 @@ import {
|
||||
} from '@ghostfolio/common/config';
|
||||
import { DATE_FORMAT, getSum, parseDate } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
Accounts,
|
||||
AccountsResponse,
|
||||
EnhancedSymbolProfile,
|
||||
Filter,
|
||||
HistoricalDataItem,
|
||||
InvestmentItem,
|
||||
PortfolioDetails,
|
||||
PortfolioHoldingResponse,
|
||||
PortfolioInvestments,
|
||||
PortfolioPerformanceResponse,
|
||||
PortfolioPosition,
|
||||
@ -87,7 +88,6 @@ import { isEmpty } from 'lodash';
|
||||
|
||||
import { PortfolioCalculator } from './calculator/portfolio-calculator';
|
||||
import { PortfolioCalculatorFactory } from './calculator/portfolio-calculator.factory';
|
||||
import { PortfolioHoldingDetail } from './interfaces/portfolio-holding-detail.interface';
|
||||
import { RulesService } from './rules.service';
|
||||
|
||||
const asiaPacificMarkets = require('../../assets/countries/asia-pacific-markets.json');
|
||||
@ -209,7 +209,7 @@ export class PortfolioService {
|
||||
filters?: Filter[];
|
||||
userId: string;
|
||||
withExcludedAccounts?: boolean;
|
||||
}): Promise<Accounts> {
|
||||
}): Promise<AccountsResponse> {
|
||||
const accounts = await this.getAccounts({
|
||||
filters,
|
||||
userId,
|
||||
@ -631,11 +631,11 @@ export class PortfolioService {
|
||||
};
|
||||
}
|
||||
|
||||
public async getPosition(
|
||||
public async getHolding(
|
||||
aDataSource: DataSource,
|
||||
aImpersonationId: string,
|
||||
aSymbol: string
|
||||
): Promise<PortfolioHoldingDetail> {
|
||||
): Promise<PortfolioHoldingResponse> {
|
||||
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
||||
const user = await this.userService.user({ id: userId });
|
||||
const userCurrency = this.getUserCurrency(user);
|
||||
@ -648,6 +648,7 @@ export class PortfolioService {
|
||||
|
||||
if (activities.length === 0) {
|
||||
return {
|
||||
activities: [],
|
||||
averagePrice: undefined,
|
||||
dataProviderInfo: undefined,
|
||||
dividendInBaseCurrency: undefined,
|
||||
@ -662,13 +663,12 @@ export class PortfolioService {
|
||||
historicalData: [],
|
||||
investment: undefined,
|
||||
marketPrice: undefined,
|
||||
maxPrice: undefined,
|
||||
minPrice: undefined,
|
||||
marketPriceMax: undefined,
|
||||
marketPriceMin: undefined,
|
||||
netPerformance: undefined,
|
||||
netPerformancePercent: undefined,
|
||||
netPerformancePercentWithCurrencyEffect: undefined,
|
||||
netPerformanceWithCurrencyEffect: undefined,
|
||||
orders: [],
|
||||
quantity: undefined,
|
||||
SymbolProfile: undefined,
|
||||
tags: [],
|
||||
@ -714,7 +714,7 @@ export class PortfolioService {
|
||||
transactionCount
|
||||
} = position;
|
||||
|
||||
const activitiesOfPosition = activities.filter(({ SymbolProfile }) => {
|
||||
const activitiesOfHolding = activities.filter(({ SymbolProfile }) => {
|
||||
return (
|
||||
SymbolProfile.dataSource === dataSource &&
|
||||
SymbolProfile.symbol === symbol
|
||||
@ -748,12 +748,12 @@ export class PortfolioService {
|
||||
);
|
||||
|
||||
const historicalDataArray: HistoricalDataItem[] = [];
|
||||
let maxPrice = Math.max(
|
||||
activitiesOfPosition[0].unitPriceInAssetProfileCurrency,
|
||||
let marketPriceMax = Math.max(
|
||||
activitiesOfHolding[0].unitPriceInAssetProfileCurrency,
|
||||
marketPrice
|
||||
);
|
||||
let minPrice = Math.min(
|
||||
activitiesOfPosition[0].unitPriceInAssetProfileCurrency,
|
||||
let marketPriceMin = Math.min(
|
||||
activitiesOfHolding[0].unitPriceInAssetProfileCurrency,
|
||||
marketPrice
|
||||
);
|
||||
|
||||
@ -793,27 +793,31 @@ export class PortfolioService {
|
||||
quantity: currentQuantity
|
||||
});
|
||||
|
||||
maxPrice = Math.max(marketPrice ?? 0, maxPrice);
|
||||
minPrice = Math.min(marketPrice ?? Number.MAX_SAFE_INTEGER, minPrice);
|
||||
marketPriceMax = Math.max(marketPrice ?? 0, marketPriceMax);
|
||||
marketPriceMin = Math.min(
|
||||
marketPrice ?? Number.MAX_SAFE_INTEGER,
|
||||
marketPriceMin
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Add historical entry for buy date, if no historical data available
|
||||
historicalDataArray.push({
|
||||
averagePrice: activitiesOfPosition[0].unitPriceInAssetProfileCurrency,
|
||||
averagePrice: activitiesOfHolding[0].unitPriceInAssetProfileCurrency,
|
||||
date: firstBuyDate,
|
||||
marketPrice: activitiesOfPosition[0].unitPriceInAssetProfileCurrency,
|
||||
quantity: activitiesOfPosition[0].quantity
|
||||
marketPrice: activitiesOfHolding[0].unitPriceInAssetProfileCurrency,
|
||||
quantity: activitiesOfHolding[0].quantity
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
firstBuyDate,
|
||||
marketPrice,
|
||||
maxPrice,
|
||||
minPrice,
|
||||
marketPriceMax,
|
||||
marketPriceMin,
|
||||
SymbolProfile,
|
||||
tags,
|
||||
transactionCount,
|
||||
activities: activitiesOfHolding,
|
||||
averagePrice: averagePrice.toNumber(),
|
||||
dataProviderInfo: portfolioCalculator.getDataProviderInfos()?.[0],
|
||||
dividendInBaseCurrency: dividendInBaseCurrency.toNumber(),
|
||||
@ -842,7 +846,6 @@ export class PortfolioService {
|
||||
]?.toNumber(),
|
||||
netPerformanceWithCurrencyEffect:
|
||||
position.netPerformanceWithCurrencyEffectMap?.['max']?.toNumber(),
|
||||
orders: activitiesOfPosition,
|
||||
quantity: quantity.toNumber(),
|
||||
value: this.exchangeRateDataService.toCurrency(
|
||||
quantity.mul(marketPrice ?? 0).toNumber(),
|
||||
@ -881,8 +884,8 @@ export class PortfolioService {
|
||||
}
|
||||
|
||||
const historicalDataArray: HistoricalDataItem[] = [];
|
||||
let maxPrice = marketPrice;
|
||||
let minPrice = marketPrice;
|
||||
let marketPriceMax = marketPrice;
|
||||
let marketPriceMin = marketPrice;
|
||||
|
||||
for (const [date, { marketPrice }] of Object.entries(
|
||||
historicalData[aSymbol]
|
||||
@ -892,15 +895,19 @@ export class PortfolioService {
|
||||
value: marketPrice
|
||||
});
|
||||
|
||||
maxPrice = Math.max(marketPrice ?? 0, maxPrice);
|
||||
minPrice = Math.min(marketPrice ?? Number.MAX_SAFE_INTEGER, minPrice);
|
||||
marketPriceMax = Math.max(marketPrice ?? 0, marketPriceMax);
|
||||
marketPriceMin = Math.min(
|
||||
marketPrice ?? Number.MAX_SAFE_INTEGER,
|
||||
marketPriceMin
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
marketPrice,
|
||||
maxPrice,
|
||||
minPrice,
|
||||
marketPriceMax,
|
||||
marketPriceMin,
|
||||
SymbolProfile,
|
||||
activities: [],
|
||||
averagePrice: 0,
|
||||
dataProviderInfo: undefined,
|
||||
dividendInBaseCurrency: 0,
|
||||
@ -918,7 +925,6 @@ export class PortfolioService {
|
||||
netPerformancePercent: undefined,
|
||||
netPerformancePercentWithCurrencyEffect: undefined,
|
||||
netPerformanceWithCurrencyEffect: undefined,
|
||||
orders: [],
|
||||
quantity: 0,
|
||||
tags: [],
|
||||
transactionCount: undefined,
|
||||
@ -927,7 +933,7 @@ export class PortfolioService {
|
||||
}
|
||||
}
|
||||
|
||||
public async getPositions({
|
||||
public async getHoldings({
|
||||
dateRange = 'max',
|
||||
filters,
|
||||
impersonationId
|
||||
|
@ -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({
|
||||
enableSharing = false
|
||||
}): Promise<BenchmarkResponse['benchmarks']> {
|
||||
@ -302,16 +314,4 @@ export class BenchmarkService {
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { DERIVED_CURRENCIES } from '@ghostfolio/common/config';
|
||||
import { isDerivedCurrency } from '@ghostfolio/common/helper';
|
||||
|
||||
import {
|
||||
registerDecorator,
|
||||
@ -28,17 +28,11 @@ export class IsExtendedCurrencyConstraint
|
||||
return '$property must be a valid ISO4217 currency code';
|
||||
}
|
||||
|
||||
public validate(currency: any) {
|
||||
// Return true if currency is a standard ISO 4217 code or a derived currency
|
||||
public validate(currency: string) {
|
||||
// Return true if currency is a derived currency or a standard ISO 4217 code
|
||||
return (
|
||||
this.isUpperCase(currency) &&
|
||||
(isISO4217CurrencyCode(currency) ||
|
||||
[
|
||||
...DERIVED_CURRENCIES.map((derivedCurrency) => {
|
||||
return derivedCurrency.currency;
|
||||
}),
|
||||
'USX'
|
||||
].includes(currency))
|
||||
isDerivedCurrency(currency) ||
|
||||
(this.isUpperCase(currency) && isISO4217CurrencyCode(currency))
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -105,8 +105,8 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
|
||||
public investmentPrecision = 2;
|
||||
public marketDataItems: MarketData[] = [];
|
||||
public marketPrice: number;
|
||||
public maxPrice: number;
|
||||
public minPrice: number;
|
||||
public marketPriceMax: number;
|
||||
public marketPriceMin: number;
|
||||
public netPerformance: number;
|
||||
public netPerformancePrecision = 2;
|
||||
public netPerformancePercent: number;
|
||||
@ -234,8 +234,8 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
|
||||
historicalData,
|
||||
investment,
|
||||
marketPrice,
|
||||
maxPrice,
|
||||
minPrice,
|
||||
marketPriceMax,
|
||||
marketPriceMin,
|
||||
netPerformance,
|
||||
netPerformancePercent,
|
||||
netPerformancePercentWithCurrencyEffect,
|
||||
@ -297,8 +297,8 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
this.marketPrice = marketPrice;
|
||||
this.maxPrice = maxPrice;
|
||||
this.minPrice = minPrice;
|
||||
this.marketPriceMax = marketPriceMax;
|
||||
this.marketPriceMin = marketPriceMin;
|
||||
this.netPerformance = netPerformance;
|
||||
|
||||
if (
|
||||
|
@ -106,11 +106,11 @@
|
||||
[locale]="data.locale"
|
||||
[ngClass]="{
|
||||
'text-danger':
|
||||
minPrice?.toFixed(2) === marketPrice?.toFixed(2) &&
|
||||
maxPrice?.toFixed(2) !== minPrice?.toFixed(2)
|
||||
marketPriceMin?.toFixed(2) === marketPrice?.toFixed(2) &&
|
||||
marketPriceMax?.toFixed(2) !== marketPriceMin?.toFixed(2)
|
||||
}"
|
||||
[unit]="SymbolProfile?.currency"
|
||||
[value]="minPrice"
|
||||
[value]="marketPriceMin"
|
||||
>Minimum Price</gf-value
|
||||
>
|
||||
</div>
|
||||
@ -122,11 +122,11 @@
|
||||
[locale]="data.locale"
|
||||
[ngClass]="{
|
||||
'text-success':
|
||||
maxPrice?.toFixed(2) === marketPrice?.toFixed(2) &&
|
||||
maxPrice?.toFixed(2) !== minPrice?.toFixed(2)
|
||||
marketPriceMax?.toFixed(2) === marketPrice?.toFixed(2) &&
|
||||
marketPriceMax?.toFixed(2) !== marketPriceMin?.toFixed(2)
|
||||
}"
|
||||
[unit]="SymbolProfile?.currency"
|
||||
[value]="maxPrice"
|
||||
[value]="marketPriceMax"
|
||||
>Maximum Price</gf-value
|
||||
>
|
||||
</div>
|
||||
|
@ -1,4 +1,5 @@
|
||||
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 {
|
||||
AssetProfileIdentifier,
|
||||
@ -6,6 +7,7 @@ import {
|
||||
User
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { BenchmarkTrend } from '@ghostfolio/common/types';
|
||||
import { GfBenchmarkComponent } from '@ghostfolio/ui/benchmark';
|
||||
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
|
||||
|
||||
@ -44,6 +46,7 @@ import { CreateWatchlistItemDialogParams } from './create-watchlist-item-dialog/
|
||||
})
|
||||
export class HomeWatchlistComponent implements OnDestroy, OnInit {
|
||||
public deviceType: string;
|
||||
public hasImpersonationId: boolean;
|
||||
public hasPermissionToCreateWatchlistItem: boolean;
|
||||
public hasPermissionToDeleteWatchlistItem: boolean;
|
||||
public user: User;
|
||||
@ -56,12 +59,20 @@ export class HomeWatchlistComponent implements OnDestroy, OnInit {
|
||||
private dataService: DataService,
|
||||
private deviceService: DeviceDetectorService,
|
||||
private dialog: MatDialog,
|
||||
private impersonationStorageService: ImpersonationStorageService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private userService: UserService
|
||||
) {
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
|
||||
this.impersonationStorageService
|
||||
.onChangeHasImpersonation()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((impersonationId) => {
|
||||
this.hasImpersonationId = !!impersonationId;
|
||||
});
|
||||
|
||||
this.route.queryParams
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((params) => {
|
||||
@ -76,11 +87,15 @@ export class HomeWatchlistComponent implements OnDestroy, OnInit {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
|
||||
this.hasPermissionToCreateWatchlistItem = hasPermission(
|
||||
this.hasPermissionToCreateWatchlistItem =
|
||||
!this.hasImpersonationId &&
|
||||
hasPermission(
|
||||
this.user.permissions,
|
||||
permissions.createWatchlistItem
|
||||
);
|
||||
this.hasPermissionToDeleteWatchlistItem = hasPermission(
|
||||
this.hasPermissionToDeleteWatchlistItem =
|
||||
!this.hasImpersonationId &&
|
||||
hasPermission(
|
||||
this.user.permissions,
|
||||
permissions.deleteWatchlistItem
|
||||
);
|
||||
@ -118,15 +133,17 @@ export class HomeWatchlistComponent implements OnDestroy, OnInit {
|
||||
.fetchWatchlist()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ watchlist }) => {
|
||||
this.watchlist = watchlist.map(({ dataSource, symbol }) => ({
|
||||
this.watchlist = watchlist.map(
|
||||
({ dataSource, marketCondition, name, performances, symbol }) => ({
|
||||
dataSource,
|
||||
marketCondition,
|
||||
name,
|
||||
performances,
|
||||
symbol,
|
||||
marketCondition: null,
|
||||
name: symbol,
|
||||
performances: null,
|
||||
trend50d: 'UNKNOWN',
|
||||
trend200d: 'UNKNOWN'
|
||||
}));
|
||||
trend50d: 'UNKNOWN' as BenchmarkTrend,
|
||||
trend200d: 'UNKNOWN' as BenchmarkTrend
|
||||
})
|
||||
);
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
@ -7,7 +7,7 @@
|
||||
}
|
||||
</span>
|
||||
</h1>
|
||||
<div class="mb-3 row">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md-8 offset-md-2">
|
||||
<gf-benchmark
|
||||
[benchmarks]="watchlist"
|
||||
@ -20,7 +20,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@if (hasPermissionToCreateWatchlistItem) {
|
||||
@if (!hasImpersonationId && hasPermissionToCreateWatchlistItem) {
|
||||
<div class="fab-container">
|
||||
<a
|
||||
class="align-items-center d-flex justify-content-center"
|
||||
|
@ -13,7 +13,6 @@ import {
|
||||
Activity
|
||||
} from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||
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 { DeleteOwnUserDto } from '@ghostfolio/api/app/user/delete-own-user.dto';
|
||||
import { UserItem } from '@ghostfolio/api/app/user/interfaces/user-item.interface';
|
||||
@ -25,7 +24,7 @@ import {
|
||||
Access,
|
||||
AccessTokenResponse,
|
||||
AccountBalancesResponse,
|
||||
Accounts,
|
||||
AccountsResponse,
|
||||
AiPromptResponse,
|
||||
ApiKeyResponse,
|
||||
AssetProfileIdentifier,
|
||||
@ -40,6 +39,7 @@ import {
|
||||
OAuthResponse,
|
||||
PortfolioDetails,
|
||||
PortfolioDividends,
|
||||
PortfolioHoldingResponse,
|
||||
PortfolioHoldingsResponse,
|
||||
PortfolioInvestments,
|
||||
PortfolioPerformanceResponse,
|
||||
@ -191,7 +191,7 @@ export class DataService {
|
||||
public fetchAccounts({ filters }: { filters?: Filter[] } = {}) {
|
||||
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({
|
||||
@ -406,13 +406,13 @@ export class DataService {
|
||||
symbol: string;
|
||||
}) {
|
||||
return this.http
|
||||
.get<PortfolioHoldingDetail>(
|
||||
`/api/v1/portfolio/position/${dataSource}/${symbol}`
|
||||
.get<PortfolioHoldingResponse>(
|
||||
`/api/v1/portfolio/holding/${dataSource}/${symbol}`
|
||||
)
|
||||
.pipe(
|
||||
map((data) => {
|
||||
if (data.orders) {
|
||||
for (const order of data.orders) {
|
||||
if (data.activities) {
|
||||
for (const order of data.activities) {
|
||||
order.createdAt = parseISO(order.createdAt as unknown as string);
|
||||
order.date = parseISO(order.date as unknown as string);
|
||||
}
|
||||
@ -776,7 +776,7 @@ export class DataService {
|
||||
tags
|
||||
}: { tags: Tag[] } & AssetProfileIdentifier) {
|
||||
return this.http.put<void>(
|
||||
`/api/v1/portfolio/position/${dataSource}/${symbol}/tags`,
|
||||
`/api/v1/portfolio/holding/${dataSource}/${symbol}/tags`,
|
||||
{ tags }
|
||||
);
|
||||
}
|
||||
|
@ -2831,7 +2831,7 @@
|
||||
</trans-unit>
|
||||
<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="<strong>"/>Portfolio<x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="</strong>"/> 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="<strong>"/>Portefeuille<x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="</strong>"/> 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="<strong>"/>Portefeuille<x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="</strong>"/> avec vous ! </target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
|
||||
<context context-type="linenumber">4</context>
|
||||
@ -7931,7 +7931,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="1efa64b89c9852e7099159ab06af9dcf49870438" datatype="html">
|
||||
<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 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>
|
||||
@ -7939,7 +7939,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="7a6d28bd1c36c8298c95b7965abf226b218be50d" datatype="html">
|
||||
<source>Watchlist</source>
|
||||
<target state="new">Watchlist</target>
|
||||
<target state="translated">Liste de suivi</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/components/home-watchlist/home-watchlist.html</context>
|
||||
<context context-type="linenumber">4</context>
|
||||
@ -7947,7 +7947,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="4558213855845176930" datatype="html">
|
||||
<source>Watchlist</source>
|
||||
<target state="new">Watchlist</target>
|
||||
<target state="translated">Liste de suivi</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/home/home-page-routing.module.ts</context>
|
||||
<context context-type="linenumber">44</context>
|
||||
@ -7959,7 +7959,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="e9d59bb8bf6c08243d5411c55ddbdf925c7c799c" datatype="html">
|
||||
<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 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>
|
||||
@ -7967,7 +7967,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="627795342008207050" datatype="html">
|
||||
<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 context-type="sourcefile">libs/ui/src/lib/benchmark/benchmark.component.ts</context>
|
||||
<context context-type="linenumber">122</context>
|
||||
|
@ -349,7 +349,7 @@ export function isDerivedCurrency(aCurrency: string) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return DERIVED_CURRENCIES.find(({ currency }) => {
|
||||
return DERIVED_CURRENCIES.some(({ currency }) => {
|
||||
return currency === aCurrency;
|
||||
});
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import type { Access } from './access.interface';
|
||||
import type { AccountBalance } from './account-balance.interface';
|
||||
import type { Accounts } from './accounts.interface';
|
||||
import type { AdminData } from './admin-data.interface';
|
||||
import type { AdminJobs } from './admin-jobs.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 { AccessTokenResponse } from './responses/access-token-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 { ApiKeyResponse } from './responses/api-key-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 { MarketDataDetailsResponse } from './responses/market-data-details-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 { PortfolioPerformanceResponse } from './responses/portfolio-performance-response.interface';
|
||||
import type { PortfolioReportResponse } from './responses/portfolio-report.interface';
|
||||
@ -74,7 +75,7 @@ export {
|
||||
AccessTokenResponse,
|
||||
AccountBalance,
|
||||
AccountBalancesResponse,
|
||||
Accounts,
|
||||
AccountsResponse,
|
||||
AdminData,
|
||||
AdminJobs,
|
||||
AdminMarketData,
|
||||
@ -112,6 +113,7 @@ export {
|
||||
PortfolioChart,
|
||||
PortfolioDetails,
|
||||
PortfolioDividends,
|
||||
PortfolioHoldingResponse,
|
||||
PortfolioHoldingsResponse,
|
||||
PortfolioInvestments,
|
||||
PortfolioItem,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { AccountWithValue } from '@ghostfolio/common/types';
|
||||
|
||||
export interface Accounts {
|
||||
export interface AccountsResponse {
|
||||
accounts: AccountWithValue[];
|
||||
totalBalanceInBaseCurrency: number;
|
||||
totalValueInBaseCurrency: number;
|
@ -7,7 +7,8 @@ import {
|
||||
|
||||
import { Tag } from '@prisma/client';
|
||||
|
||||
export interface PortfolioHoldingDetail {
|
||||
export interface PortfolioHoldingResponse {
|
||||
activities: Activity[];
|
||||
averagePrice: number;
|
||||
dataProviderInfo: DataProviderInfo;
|
||||
dividendInBaseCurrency: number;
|
||||
@ -22,13 +23,12 @@ export interface PortfolioHoldingDetail {
|
||||
historicalData: HistoricalDataItem[];
|
||||
investment: number;
|
||||
marketPrice: number;
|
||||
maxPrice: number;
|
||||
minPrice: number;
|
||||
marketPriceMax: number;
|
||||
marketPriceMin: number;
|
||||
netPerformance: number;
|
||||
netPerformancePercent: number;
|
||||
netPerformancePercentWithCurrencyEffect: number;
|
||||
netPerformanceWithCurrencyEffect: number;
|
||||
orders: Activity[];
|
||||
quantity: number;
|
||||
SymbolProfile: EnhancedSymbolProfile;
|
||||
tags: Tag[];
|
@ -1,5 +1,12 @@
|
||||
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
|
||||
import {
|
||||
AssetProfileIdentifier,
|
||||
Benchmark
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
|
||||
export interface WatchlistResponse {
|
||||
watchlist: AssetProfileIdentifier[];
|
||||
watchlist: (AssetProfileIdentifier & {
|
||||
marketCondition: Benchmark['marketCondition'];
|
||||
name: string;
|
||||
performances: Benchmark['performances'];
|
||||
})[];
|
||||
}
|
||||
|
24
package-lock.json
generated
24
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ghostfolio",
|
||||
"version": "2.158.0",
|
||||
"version": "2.159.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ghostfolio",
|
||||
"version": "2.158.0",
|
||||
"version": "2.159.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
@ -47,7 +47,7 @@
|
||||
"@stripe/stripe-js": "5.4.0",
|
||||
"alphavantage": "2.2.0",
|
||||
"big.js": "6.2.2",
|
||||
"bootstrap": "4.6.0",
|
||||
"bootstrap": "4.6.2",
|
||||
"bull": "4.16.5",
|
||||
"cache-manager": "5.7.6",
|
||||
"cache-manager-redis-yet": "5.1.4",
|
||||
@ -14322,14 +14322,20 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/bootstrap": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.0.tgz",
|
||||
"integrity": "sha512-Io55IuQY3kydzHtbGvQya3H+KorS/M9rSNyfCGCg9WZ4pyT/lCxIlpJgG1GXW/PswzC84Tr2fBYi+7+jFVQQBw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz",
|
||||
"integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/twbs"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/bootstrap"
|
||||
},
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"jquery": "1.9.1 - 3",
|
||||
"popper.js": "^1.16.1"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ghostfolio",
|
||||
"version": "2.158.0",
|
||||
"version": "2.159.0",
|
||||
"homepage": "https://ghostfol.io",
|
||||
"license": "AGPL-3.0",
|
||||
"repository": "https://github.com/ghostfolio/ghostfolio",
|
||||
@ -93,7 +93,7 @@
|
||||
"@stripe/stripe-js": "5.4.0",
|
||||
"alphavantage": "2.2.0",
|
||||
"big.js": "6.2.2",
|
||||
"bootstrap": "4.6.0",
|
||||
"bootstrap": "4.6.2",
|
||||
"bull": "4.16.5",
|
||||
"cache-manager": "5.7.6",
|
||||
"cache-manager-redis-yet": "5.1.4",
|
||||
|
Loading…
x
Reference in New Issue
Block a user