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/),
|
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
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 &&
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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))
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 (
|
||||||
|
@ -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>
|
||||||
|
@ -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();
|
||||||
});
|
});
|
||||||
|
@ -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"
|
||||||
|
@ -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 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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="<strong>"/>Portfolio<x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="</strong>"/> 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="<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-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>
|
||||||
|
@ -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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
@ -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[];
|
@ -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
26
package-lock.json
generated
@ -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"
|
||||||
|
@ -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",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user