diff --git a/CHANGELOG.md b/CHANGELOG.md
index 713cf131..b4cb86aa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,30 @@ 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
+## 2.134.0 - 2025-01-15
+
+### Added
+
+- Set up the language localization for Українська (`uk`)
+
+### Changed
+
+- Extended the health check endpoint to include database and cache operations (experimental)
+- Refactored various `lodash` functions with native JavaScript equivalents
+- Improved the language localization for German (`de`)
+- Upgraded `prisma` from version `6.1.0` to `6.2.1`
+
+### Fixed
+
+- Fixed an issue with the import of activities with type `FEE` (where unit price is `0`)
+- Fixed an issue with the renaming of activities with type `FEE`, `INTEREST`, `ITEM` or `LIABILITY`
+- Handled an exception in the scraper configuration introduced by the migration from `got` to `fetch`
+
+## 2.133.1 - 2025-01-09
+
+### Added
+
+- Added a _Copy AI prompt to clipboard_ action to the analysis page (experimental)
### Changed
diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts
index 7ac2c591..6d097aef 100644
--- a/apps/api/src/app/app.module.ts
+++ b/apps/api/src/app/app.module.ts
@@ -31,6 +31,7 @@ import { AuthDeviceModule } from './auth-device/auth-device.module';
import { AuthModule } from './auth/auth.module';
import { BenchmarkModule } from './benchmark/benchmark.module';
import { CacheModule } from './cache/cache.module';
+import { AiModule } from './endpoints/ai/ai.module';
import { ApiKeysModule } from './endpoints/api-keys/api-keys.module';
import { GhostfolioModule } from './endpoints/data-providers/ghostfolio/ghostfolio.module';
import { MarketDataModule } from './endpoints/market-data/market-data.module';
@@ -57,6 +58,7 @@ import { UserModule } from './user/user.module';
AdminModule,
AccessModule,
AccountModule,
+ AiModule,
ApiKeysModule,
AssetModule,
AuthDeviceModule,
diff --git a/apps/api/src/app/benchmark/benchmark.service.ts b/apps/api/src/app/benchmark/benchmark.service.ts
index a659281d..4e466668 100644
--- a/apps/api/src/app/benchmark/benchmark.service.ts
+++ b/apps/api/src/app/benchmark/benchmark.service.ts
@@ -38,7 +38,7 @@ import {
isSameDay,
subDays
} from 'date-fns';
-import { isNumber, last, uniqBy } from 'lodash';
+import { isNumber, uniqBy } from 'lodash';
import ms from 'ms';
import { BenchmarkValue } from './interfaces/benchmark-value.interface';
@@ -258,7 +258,7 @@ export class BenchmarkService {
}
const includesEndDate = isSameDay(
- parseDate(last(marketData).date),
+ parseDate(marketData.at(-1).date),
endDate
);
diff --git a/apps/api/src/app/endpoints/ai/ai.controller.ts b/apps/api/src/app/endpoints/ai/ai.controller.ts
new file mode 100644
index 00000000..981b26aa
--- /dev/null
+++ b/apps/api/src/app/endpoints/ai/ai.controller.ts
@@ -0,0 +1,39 @@
+import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
+import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
+import {
+ DEFAULT_CURRENCY,
+ DEFAULT_LANGUAGE_CODE
+} from '@ghostfolio/common/config';
+import { AiPromptResponse } from '@ghostfolio/common/interfaces';
+import { permissions } from '@ghostfolio/common/permissions';
+import type { RequestWithUser } from '@ghostfolio/common/types';
+
+import { Controller, Get, Inject, UseGuards } from '@nestjs/common';
+import { REQUEST } from '@nestjs/core';
+import { AuthGuard } from '@nestjs/passport';
+
+import { AiService } from './ai.service';
+
+@Controller('ai')
+export class AiController {
+ public constructor(
+ private readonly aiService: AiService,
+ @Inject(REQUEST) private readonly request: RequestWithUser
+ ) {}
+
+ @Get('prompt')
+ @HasPermission(permissions.readAiPrompt)
+ @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
+ public async getPrompt(): Promise {
+ const prompt = await this.aiService.getPrompt({
+ impersonationId: undefined,
+ languageCode:
+ this.request.user.Settings.settings.language ?? DEFAULT_LANGUAGE_CODE,
+ userCurrency:
+ this.request.user.Settings.settings.baseCurrency ?? DEFAULT_CURRENCY,
+ userId: this.request.user.id
+ });
+
+ return { prompt };
+ }
+}
diff --git a/apps/api/src/app/endpoints/ai/ai.module.ts b/apps/api/src/app/endpoints/ai/ai.module.ts
new file mode 100644
index 00000000..5a30f326
--- /dev/null
+++ b/apps/api/src/app/endpoints/ai/ai.module.ts
@@ -0,0 +1,51 @@
+import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
+import { AccountService } from '@ghostfolio/api/app/account/account.service';
+import { OrderModule } from '@ghostfolio/api/app/order/order.module';
+import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
+import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
+import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
+import { RulesService } from '@ghostfolio/api/app/portfolio/rules.service';
+import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
+import { UserModule } from '@ghostfolio/api/app/user/user.module';
+import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
+import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
+import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
+import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module';
+import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
+import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
+import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
+import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module';
+import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
+
+import { Module } from '@nestjs/common';
+
+import { AiController } from './ai.controller';
+import { AiService } from './ai.service';
+
+@Module({
+ controllers: [AiController],
+ imports: [
+ ConfigurationModule,
+ DataProviderModule,
+ ExchangeRateDataModule,
+ ImpersonationModule,
+ MarketDataModule,
+ OrderModule,
+ PortfolioSnapshotQueueModule,
+ PrismaModule,
+ RedisCacheModule,
+ SymbolProfileModule,
+ UserModule
+ ],
+ providers: [
+ AccountBalanceService,
+ AccountService,
+ AiService,
+ CurrentRateService,
+ MarketDataService,
+ PortfolioCalculatorFactory,
+ PortfolioService,
+ RulesService
+ ]
+})
+export class AiModule {}
diff --git a/apps/api/src/app/endpoints/ai/ai.service.ts b/apps/api/src/app/endpoints/ai/ai.service.ts
new file mode 100644
index 00000000..59dec6ad
--- /dev/null
+++ b/apps/api/src/app/endpoints/ai/ai.service.ts
@@ -0,0 +1,60 @@
+import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
+
+import { Injectable } from '@nestjs/common';
+
+@Injectable()
+export class AiService {
+ public constructor(private readonly portfolioService: PortfolioService) {}
+
+ public async getPrompt({
+ impersonationId,
+ languageCode,
+ userCurrency,
+ userId
+ }: {
+ impersonationId: string;
+ languageCode: string;
+ userCurrency: string;
+ userId: string;
+ }) {
+ const { holdings } = await this.portfolioService.getDetails({
+ impersonationId,
+ userId
+ });
+
+ const holdingsTable = [
+ '| Name | Symbol | Currency | Asset Class | Asset Sub Class | Allocation in Percentage |',
+ '| --- | --- | --- | --- | --- | --- |',
+ ...Object.values(holdings)
+ .sort((a, b) => {
+ return b.allocationInPercentage - a.allocationInPercentage;
+ })
+ .map(
+ ({
+ allocationInPercentage,
+ assetClass,
+ assetSubClass,
+ currency,
+ name,
+ symbol
+ }) => {
+ return `| ${name} | ${symbol} | ${currency} | ${assetClass} | ${assetSubClass} | ${(allocationInPercentage * 100).toFixed(3)}% |`;
+ }
+ )
+ ];
+
+ return [
+ `You are a neutral financial assistant. Please analyze the following investment portfolio (base currency being ${userCurrency}) in simple words.`,
+ ...holdingsTable,
+ 'Structure your answer with these sections:',
+ 'Overview: Briefly summarize the portfolio’s composition and allocation rationale.',
+ 'Risk Assessment: Identify potential risks, including market volatility, concentration, and sectoral imbalances.',
+ 'Advantages: Highlight strengths, focusing on growth potential, diversification, or other benefits.',
+ 'Disadvantages: Point out weaknesses, such as overexposure or lack of defensive assets.',
+ 'Target Group: Discuss who this portfolio might suit (e.g., risk tolerance, investment goals, life stages, and experience levels).',
+ 'Optimization Ideas: Offer ideas to complement the portfolio, ensuring they are constructive and neutral in tone.',
+ 'Conclusion: Provide a concise summary highlighting key insights.',
+ `Provide your answer in the following language: ${languageCode}.`
+ ].join('\n');
+ }
+}
diff --git a/apps/api/src/app/health/health.controller.ts b/apps/api/src/app/health/health.controller.ts
index 62ee2041..6ff09825 100644
--- a/apps/api/src/app/health/health.controller.ts
+++ b/apps/api/src/app/health/health.controller.ts
@@ -3,13 +3,14 @@ import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interce
import {
Controller,
Get,
- HttpCode,
HttpException,
HttpStatus,
Param,
+ Res,
UseInterceptors
} from '@nestjs/common';
import { DataSource } from '@prisma/client';
+import { Response } from 'express';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { HealthService } from './health.service';
@@ -19,9 +20,20 @@ export class HealthController {
public constructor(private readonly healthService: HealthService) {}
@Get()
- @HttpCode(HttpStatus.OK)
- public getHealth() {
- return { status: getReasonPhrase(StatusCodes.OK) };
+ public async getHealth(@Res() response: Response) {
+ const databaseServiceHealthy = await this.healthService.isDatabaseHealthy();
+ const redisCacheServiceHealthy =
+ await this.healthService.isRedisCacheHealthy();
+
+ if (databaseServiceHealthy && redisCacheServiceHealthy) {
+ return response
+ .status(HttpStatus.OK)
+ .json({ status: getReasonPhrase(StatusCodes.OK) });
+ } else {
+ return response
+ .status(HttpStatus.SERVICE_UNAVAILABLE)
+ .json({ status: getReasonPhrase(StatusCodes.SERVICE_UNAVAILABLE) });
+ }
}
@Get('data-enhancer/:name')
diff --git a/apps/api/src/app/health/health.module.ts b/apps/api/src/app/health/health.module.ts
index 6ed46440..b8c4d581 100644
--- a/apps/api/src/app/health/health.module.ts
+++ b/apps/api/src/app/health/health.module.ts
@@ -1,6 +1,8 @@
+import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
import { DataEnhancerModule } from '@ghostfolio/api/services/data-provider/data-enhancer/data-enhancer.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
+import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
import { Module } from '@nestjs/common';
@@ -12,6 +14,8 @@ import { HealthService } from './health.service';
imports: [
DataEnhancerModule,
DataProviderModule,
+ PropertyModule,
+ RedisCacheModule,
TransformDataSourceInRequestModule
],
providers: [HealthService]
diff --git a/apps/api/src/app/health/health.service.ts b/apps/api/src/app/health/health.service.ts
index b0c81139..f08f33a1 100644
--- a/apps/api/src/app/health/health.service.ts
+++ b/apps/api/src/app/health/health.service.ts
@@ -1,5 +1,8 @@
+import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { DataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/data-enhancer.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
+import { PropertyService } from '@ghostfolio/api/services/property/property.service';
+import { PROPERTY_CURRENCIES } from '@ghostfolio/common/config';
import { Injectable } from '@nestjs/common';
import { DataSource } from '@prisma/client';
@@ -8,7 +11,9 @@ import { DataSource } from '@prisma/client';
export class HealthService {
public constructor(
private readonly dataEnhancerService: DataEnhancerService,
- private readonly dataProviderService: DataProviderService
+ private readonly dataProviderService: DataProviderService,
+ private readonly propertyService: PropertyService,
+ private readonly redisCacheService: RedisCacheService
) {}
public async hasResponseFromDataEnhancer(aName: string) {
@@ -18,4 +23,24 @@ export class HealthService {
public async hasResponseFromDataProvider(aDataSource: DataSource) {
return this.dataProviderService.checkQuote(aDataSource);
}
+
+ public async isDatabaseHealthy() {
+ try {
+ await this.propertyService.getByKey(PROPERTY_CURRENCIES);
+
+ return true;
+ } catch {
+ return false;
+ }
+ }
+
+ public async isRedisCacheHealthy() {
+ try {
+ const isHealthy = await this.redisCacheService.isHealthy();
+
+ return isHealthy;
+ } catch {
+ return false;
+ }
+ }
}
diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts
index 3b7290b4..eb1b841c 100644
--- a/apps/api/src/app/import/import.service.ts
+++ b/apps/api/src/app/import/import.service.ts
@@ -30,7 +30,7 @@ import { Injectable } from '@nestjs/common';
import { DataSource, Prisma, SymbolProfile } from '@prisma/client';
import { Big } from 'big.js';
import { endOfToday, format, isAfter, isSameSecond, parseISO } from 'date-fns';
-import { uniqBy } from 'lodash';
+import { isNumber, uniqBy } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
@Injectable()
@@ -328,7 +328,7 @@ export class ImportService {
date
);
- if (!unitPrice) {
+ if (!isNumber(unitPrice)) {
throw new Error(
`activities.${index} historical exchange rate at ${format(
date,
diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
index eb18b358..2f8a9f0c 100644
--- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
+++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
@@ -49,7 +49,7 @@ import {
min,
subDays
} from 'date-fns';
-import { first, isNumber, last, sortBy, sum, uniq, uniqBy } from 'lodash';
+import { isNumber, sortBy, sum, uniq, uniqBy } from 'lodash';
export abstract class PortfolioCalculator {
protected static readonly ENABLE_LOGGING = false;
@@ -167,7 +167,7 @@ export abstract class PortfolioCalculator {
@LogPerformance
public async computeSnapshot(): Promise {
- const lastTransactionPoint = last(this.transactionPoints);
+ const lastTransactionPoint = this.transactionPoints.at(-1);
const transactionPoints = this.transactionPoints?.filter(({ date }) => {
return isBefore(parseDate(date), this.endDate);
@@ -772,9 +772,7 @@ export abstract class PortfolioCalculator {
let firstActivityDate: Date;
try {
- const firstAccountBalanceDateString = first(
- this.accountBalanceItems
- )?.date;
+ const firstAccountBalanceDateString = this.accountBalanceItems[0]?.date;
firstAccountBalanceDate = firstAccountBalanceDateString
? parseDate(firstAccountBalanceDateString)
: new Date();
diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts
index 37499f0e..deb3cd72 100644
--- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts
+++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts
@@ -19,7 +19,6 @@ import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/po
import { parseDate } from '@ghostfolio/common/helper';
import { Big } from 'big.js';
-import { last } from 'lodash';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return {
@@ -201,7 +200,7 @@ describe('PortfolioCalculator', () => {
totalValuablesWithCurrencyEffect: new Big('0')
});
- expect(last(portfolioSnapshot.historicalData)).toMatchObject(
+ expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject(
expect.objectContaining({
netPerformance: -15.8,
netPerformanceInPercentage: -0.05528341497550734703,
diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts
index 23c594e5..7b4d53b2 100644
--- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts
+++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts
@@ -19,7 +19,6 @@ import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/po
import { parseDate } from '@ghostfolio/common/helper';
import { Big } from 'big.js';
-import { last } from 'lodash';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return {
@@ -186,7 +185,7 @@ describe('PortfolioCalculator', () => {
totalValuablesWithCurrencyEffect: new Big('0')
});
- expect(last(portfolioSnapshot.historicalData)).toMatchObject(
+ expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject(
expect.objectContaining({
netPerformance: -15.8,
netPerformanceInPercentage: -0.05528341497550734703,
diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts
index 90f6a59d..002cbd5e 100644
--- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts
+++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts
@@ -19,7 +19,6 @@ import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/po
import { parseDate } from '@ghostfolio/common/helper';
import { Big } from 'big.js';
-import { last } from 'lodash';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return {
@@ -177,7 +176,7 @@ describe('PortfolioCalculator', () => {
totalValuablesWithCurrencyEffect: new Big('0')
});
- expect(last(portfolioSnapshot.historicalData)).toMatchObject(
+ expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject(
expect.objectContaining({
netPerformance: 23.05,
netPerformanceInPercentage: 0.08437042459736457,
diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts
index e232b42c..640de398 100644
--- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts
+++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts
@@ -20,7 +20,6 @@ import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/po
import { parseDate } from '@ghostfolio/common/helper';
import { Big } from 'big.js';
-import { last } from 'lodash';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return {
@@ -205,7 +204,7 @@ describe('PortfolioCalculator', () => {
totalValuablesWithCurrencyEffect: new Big('0')
});
- expect(last(portfolioSnapshot.historicalData)).toMatchObject(
+ expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject(
expect.objectContaining({
netPerformance: new Big('27172.74').mul(0.97373).toNumber(),
netPerformanceInPercentage: 42.41983590271396609433,
diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-fee.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-fee.spec.ts
index fe379a92..6f030a73 100644
--- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-fee.spec.ts
+++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-fee.spec.ts
@@ -19,7 +19,6 @@ import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/po
import { parseDate } from '@ghostfolio/common/helper';
import { Big } from 'big.js';
-import { last } from 'lodash';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return {
@@ -158,7 +157,7 @@ describe('PortfolioCalculator', () => {
totalValuablesWithCurrencyEffect: new Big('0')
});
- expect(last(portfolioSnapshot.historicalData)).toMatchObject(
+ expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject(
expect.objectContaining({
netPerformance: 0,
netPerformanceInPercentage: 0,
diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts
index 60fe6dc6..4e25c17f 100644
--- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts
+++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts
@@ -20,7 +20,6 @@ import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/po
import { parseDate } from '@ghostfolio/common/helper';
import { Big } from 'big.js';
-import { last } from 'lodash';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return {
@@ -184,7 +183,7 @@ describe('PortfolioCalculator', () => {
totalValuablesWithCurrencyEffect: new Big('0')
});
- expect(last(portfolioSnapshot.historicalData)).toMatchObject(
+ expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject(
expect.objectContaining({
netPerformance: new Big('26.33').mul(0.8854).toNumber(),
netPerformanceInPercentage: 0.29544434470377019749,
diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-item.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-item.spec.ts
index 22856837..7fc5c526 100644
--- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-item.spec.ts
+++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-item.spec.ts
@@ -19,7 +19,6 @@ import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/po
import { parseDate } from '@ghostfolio/common/helper';
import { Big } from 'big.js';
-import { last } from 'lodash';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return {
@@ -158,7 +157,7 @@ describe('PortfolioCalculator', () => {
totalValuablesWithCurrencyEffect: new Big('0')
});
- expect(last(portfolioSnapshot.historicalData)).toMatchObject(
+ expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject(
expect.objectContaining({
netPerformance: 0,
netPerformanceInPercentage: 0,
diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts
index ba1cbeb3..54398542 100644
--- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts
+++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts
@@ -20,7 +20,6 @@ import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/po
import { parseDate } from '@ghostfolio/common/helper';
import { Big } from 'big.js';
-import { last } from 'lodash';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return {
@@ -190,7 +189,7 @@ describe('PortfolioCalculator', () => {
totalValuablesWithCurrencyEffect: new Big('0')
});
- expect(last(portfolioSnapshot.historicalData)).toMatchObject(
+ expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject(
expect.objectContaining({
totalInvestmentValueWithCurrencyEffect: 298.58
})
diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts
index f9f99ee4..37f22e2f 100644
--- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts
+++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts
@@ -21,7 +21,6 @@ import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/po
import { parseDate } from '@ghostfolio/common/helper';
import { Big } from 'big.js';
-import { last } from 'lodash';
import { join } from 'path';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
@@ -182,7 +181,7 @@ describe('PortfolioCalculator', () => {
totalValuablesWithCurrencyEffect: new Big('0')
});
- expect(last(portfolioSnapshot.historicalData)).toMatchObject(
+ expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject(
expect.objectContaining({
netPerformance: 17.68,
netPerformanceInPercentage: 0.12184460284330327256,
diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts
index 66cdb9e8..caf196f5 100644
--- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts
+++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts
@@ -21,7 +21,6 @@ import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/po
import { parseDate } from '@ghostfolio/common/helper';
import { Big } from 'big.js';
-import { last } from 'lodash';
import { join } from 'path';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
@@ -229,7 +228,7 @@ describe('PortfolioCalculator', () => {
totalValuablesWithCurrencyEffect: new Big('0')
});
- expect(last(portfolioSnapshot.historicalData)).toMatchObject(
+ expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject(
expect.objectContaining({
netPerformance: 19.86,
netPerformanceInPercentage: 0.13100263852242744063,
diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts
index 3f53ee04..cf808deb 100644
--- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts
+++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts
@@ -13,7 +13,7 @@ import { DateRange } from '@ghostfolio/common/types';
import { Logger } from '@nestjs/common';
import { Big } from 'big.js';
import { addMilliseconds, differenceInDays, format, isBefore } from 'date-fns';
-import { cloneDeep, first, last, sortBy } from 'lodash';
+import { cloneDeep, sortBy } from 'lodash';
export class TWRPortfolioCalculator extends PortfolioCalculator {
private chartDates: string[];
@@ -221,7 +221,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
};
}
- const dateOfFirstTransaction = new Date(first(orders).date);
+ const dateOfFirstTransaction = new Date(orders[0].date);
const endDateString = format(end, DATE_FORMAT);
const startDateString = format(start, DATE_FORMAT);
@@ -342,7 +342,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
});
}
- const lastOrder = last(orders);
+ const lastOrder = orders.at(-1);
lastUnitPrice = lastOrder.unitPriceFromMarketData ?? lastOrder.unitPrice;
}
diff --git a/apps/api/src/app/portfolio/current-rate.service.ts b/apps/api/src/app/portfolio/current-rate.service.ts
index ab7bf2eb..058bf1dd 100644
--- a/apps/api/src/app/portfolio/current-rate.service.ts
+++ b/apps/api/src/app/portfolio/current-rate.service.ts
@@ -13,7 +13,7 @@ import type { RequestWithUser } from '@ghostfolio/common/types';
import { Inject, Injectable } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { isBefore, isToday } from 'date-fns';
-import { flatten, isEmpty, uniqBy } from 'lodash';
+import { isEmpty, uniqBy } from 'lodash';
import { GetValueObject } from './interfaces/get-value-object.interface';
import { GetValuesObject } from './interfaces/get-values-object.interface';
@@ -102,7 +102,9 @@ export class CurrentRateService {
})
);
- const values = flatten(await Promise.all(promises));
+ const values = await Promise.all(promises).then((array) => {
+ return array.flat();
+ });
const response: GetValuesObject = {
dataProviderInfos,
diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts
index 400b0c3a..8b295aad 100644
--- a/apps/api/src/app/portfolio/portfolio.service.ts
+++ b/apps/api/src/app/portfolio/portfolio.service.ts
@@ -77,7 +77,7 @@ import {
parseISO,
set
} from 'date-fns';
-import { isEmpty, last, uniq } from 'lodash';
+import { isEmpty, uniq } from 'lodash';
import { PortfolioCalculator } from './calculator/portfolio-calculator';
import {
@@ -1133,18 +1133,15 @@ export class PortfolioService {
netWorth,
totalInvestment,
valueWithCurrencyEffect
- } =
- chart?.length > 0
- ? last(chart)
- : {
- netPerformance: 0,
- netPerformanceInPercentage: 0,
- netPerformanceInPercentageWithCurrencyEffect: 0,
- netPerformanceWithCurrencyEffect: 0,
- netWorth: 0,
- totalInvestment: 0,
- valueWithCurrencyEffect: 0
- };
+ } = chart?.at(-1) ?? {
+ netPerformance: 0,
+ netPerformanceInPercentage: 0,
+ netPerformanceInPercentageWithCurrencyEffect: 0,
+ netPerformanceWithCurrencyEffect: 0,
+ netWorth: 0,
+ totalInvestment: 0,
+ valueWithCurrencyEffect: 0
+ };
return {
chart,
diff --git a/apps/api/src/app/redis-cache/redis-cache.service.ts b/apps/api/src/app/redis-cache/redis-cache.service.ts
index c972c30a..51db93ec 100644
--- a/apps/api/src/app/redis-cache/redis-cache.service.ts
+++ b/apps/api/src/app/redis-cache/redis-cache.service.ts
@@ -7,6 +7,7 @@ import { Inject, Injectable, Logger } from '@nestjs/common';
import { Milliseconds } from 'cache-manager';
import { RedisCache } from 'cache-manager-redis-yet';
import { createHash } from 'crypto';
+import ms from 'ms';
@Injectable()
export class RedisCacheService {
@@ -59,6 +60,26 @@ export class RedisCacheService {
return `quote-${getAssetProfileIdentifier({ dataSource, symbol })}`;
}
+ public async isHealthy() {
+ try {
+ const client = this.cache.store.client;
+
+ const isHealthy = await Promise.race([
+ client.ping(),
+ new Promise((_, reject) =>
+ setTimeout(
+ () => reject(new Error('Redis health check timeout')),
+ ms('2 seconds')
+ )
+ )
+ ]);
+
+ return isHealthy === 'PONG';
+ } catch (error) {
+ return false;
+ }
+ }
+
public async remove(key: string) {
return this.cache.del(key);
}
diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts
index 33e9a67d..b5c71179 100644
--- a/apps/api/src/app/user/user.service.ts
+++ b/apps/api/src/app/user/user.service.ts
@@ -312,7 +312,8 @@ export class UserService {
currentPermissions = without(
currentPermissions,
permissions.accessHoldingsChart,
- permissions.createAccess
+ permissions.createAccess,
+ permissions.readAiPrompt
);
// Reset benchmark
diff --git a/apps/api/src/assets/sitemap.xml b/apps/api/src/assets/sitemap.xml
index 5a49f671..9fd989f4 100644
--- a/apps/api/src/assets/sitemap.xml
+++ b/apps/api/src/assets/sitemap.xml
@@ -583,6 +583,12 @@
${currentDate}T00:00:00+00:00
+
Dutch, French, German, Italian,
- Portuguese, Spanish and Turkish are currently supported.
+ Portuguese, Spanish and Turkish
+
+ are currently supported.
diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
index 506a46a3..271a5cd5 100644
--- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
+++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
@@ -1,7 +1,5 @@
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
-import { DataService } from '@ghostfolio/client/services/data.service';
-import { validateObjectForForm } from '@ghostfolio/client/util/form.util';
import { getDateFormatString } from '@ghostfolio/common/helper';
import { translate } from '@ghostfolio/ui/i18n';
@@ -24,6 +22,8 @@ import { isAfter, isToday } from 'date-fns';
import { EMPTY, Observable, Subject, lastValueFrom, of } from 'rxjs';
import { catchError, delay, map, startWith, takeUntil } from 'rxjs/operators';
+import { DataService } from '../../../../services/data.service';
+import { validateObjectForForm } from '../../../../util/form.util';
import { CreateOrUpdateActivityDialogParams } from './interfaces/interfaces';
@Component({
@@ -124,7 +124,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
name: [this.data.activity?.SymbolProfile?.name, Validators.required],
quantity: [this.data.activity?.quantity, Validators.required],
searchSymbol: [
- !!this.data.activity?.SymbolProfile
+ this.data.activity?.SymbolProfile
? {
dataSource: this.data.activity?.SymbolProfile?.dataSource,
symbol: this.data.activity?.SymbolProfile?.symbol
@@ -476,7 +476,11 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
fee: this.activityForm.get('fee').value,
quantity: this.activityForm.get('quantity').value,
symbol:
- this.activityForm.get('searchSymbol')?.value?.symbol ??
+ (['FEE', 'INTEREST', 'ITEM', 'LIABILITY'].includes(
+ this.activityForm.get('type').value
+ )
+ ? undefined
+ : this.activityForm.get('searchSymbol')?.value?.symbol) ??
this.activityForm.get('name')?.value,
tags: this.activityForm.get('tags').value,
type: this.activityForm.get('type').value,
@@ -485,8 +489,9 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
try {
if (this.mode === 'create') {
- (activity as CreateOrderDto).updateAccountBalance =
- this.activityForm.get('updateAccountBalance').value;
+ activity.updateAccountBalance = this.activityForm.get(
+ 'updateAccountBalance'
+ ).value;
await validateObjectForForm({
classDto: CreateOrderDto,
@@ -495,7 +500,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
object: activity
});
- this.dialogRef.close(activity as CreateOrderDto);
+ this.dialogRef.close(activity);
} else {
(activity as UpdateOrderDto).id = this.data.activity.id;
diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
index 76281d9e..acc561f1 100644
--- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
+++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
@@ -12,10 +12,13 @@ import {
ToggleOption,
User
} from '@ghostfolio/common/interfaces';
+import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { GroupBy } from '@ghostfolio/common/types';
import { translate } from '@ghostfolio/ui/i18n';
+import { Clipboard } from '@angular/cdk/clipboard';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
+import { MatSnackBar } from '@angular/material/snack-bar';
import { SymbolProfile } from '@prisma/client';
import { isNumber, sortBy } from 'lodash';
import { DeviceDetectorService } from 'ngx-device-detector';
@@ -40,6 +43,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
public dividendTimelineDataLabel = $localize`Dividend`;
public firstOrderDate: Date;
public hasImpersonationId: boolean;
+ public hasPermissionToReadAiPrompt: boolean;
public investments: InvestmentItem[];
public investmentTimelineDataLabel = $localize`Investment`;
public investmentsByGroup: InvestmentItem[];
@@ -74,9 +78,11 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
public constructor(
private changeDetectorRef: ChangeDetectorRef,
+ private clipboard: Clipboard,
private dataService: DataService,
private deviceService: DeviceDetectorService,
private impersonationStorageService: ImpersonationStorageService,
+ private snackBar: MatSnackBar,
private userService: UserService
) {
const { benchmarks } = this.dataService.fetchInfo();
@@ -133,6 +139,11 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
return id === this.user.settings?.benchmark;
});
+ this.hasPermissionToReadAiPrompt = hasPermission(
+ this.user.permissions,
+ permissions.readAiPrompt
+ );
+
this.update();
}
});
@@ -159,6 +170,20 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
this.fetchDividendsAndInvestments();
}
+ public onCopyPromptToClipboard() {
+ this.dataService.fetchPrompt().subscribe(({ prompt }) => {
+ this.clipboard.copy(prompt);
+
+ this.snackBar.open(
+ '✅ ' + $localize`AI prompt has been copied to the clipboard`,
+ undefined,
+ {
+ duration: 3000
+ }
+ );
+ });
+ }
+
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.html b/apps/client/src/app/pages/portfolio/analysis/analysis-page.html
index f5ad2538..8e7a63d8 100644
--- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.html
+++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.html
@@ -1,5 +1,37 @@
Analysis
+ @if (user?.settings?.isExperimentalFeatures) {
+
+
+
+
+
+
+
+
+
+
+ }
+ @if (label) {
+
+ {{ label }}
+
+ }
diff --git a/apps/client/src/app/pages/pricing/pricing-page.scss b/apps/client/src/app/pages/pricing/pricing-page.scss
index ad8e97b1..c92ca364 100644
--- a/apps/client/src/app/pages/pricing/pricing-page.scss
+++ b/apps/client/src/app/pages/pricing/pricing-page.scss
@@ -23,6 +23,12 @@
ion-icon[name='checkmark-circle-outline'] {
color: rgba(var(--palette-primary-500), 1);
}
+
+ .badge-container {
+ left: 0;
+ right: 0;
+ top: 0;
+ }
}
}
diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts
index 269a03e3..4a57d587 100644
--- a/apps/client/src/app/services/data.service.ts
+++ b/apps/client/src/app/services/data.service.ts
@@ -22,6 +22,7 @@ import {
Access,
AccountBalancesResponse,
Accounts,
+ AiPromptResponse,
ApiKeyResponse,
AssetProfileIdentifier,
BenchmarkMarketDataDetails,
@@ -637,6 +638,10 @@ export class DataService {
return this.http.get('/api/v1/portfolio/report');
}
+ public fetchPrompt() {
+ return this.http.get('/api/v1/ai/prompt');
+ }
+
public fetchPublicPortfolio(aAccessId: string) {
return this.http
.get(`/api/v1/public/${aAccessId}/portfolio`)
diff --git a/apps/client/src/locales/messages.ca.xlf b/apps/client/src/locales/messages.ca.xlf
index 9981f2a6..35cdf2a8 100644
--- a/apps/client/src/locales/messages.ca.xlf
+++ b/apps/client/src/locales/messages.ca.xlf
@@ -344,9 +344,13 @@
apps/client/src/app/components/user-account-settings/user-account-settings.html
114
+
+ apps/client/src/app/components/user-account-settings/user-account-settings.html
+ 119
+
apps/client/src/app/pages/features/features-page.html
- 259
+ 261
@@ -354,7 +358,7 @@
El risc d’assumir pèrdues en les inversions és substancial. No és recomanable invertir diners que pugui necessitar a curt termini.
apps/client/src/app/app.component.html
- 200
+ 205
@@ -575,7 +579,7 @@
apps/client/src/app/pages/pricing/pricing-page.component.ts
- 42
+ 43
apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
@@ -734,7 +738,7 @@
apps/client/src/app/pages/pricing/pricing-page.component.ts
- 43
+ 44
@@ -879,7 +883,7 @@
Realment vol revocar aquest accés?
apps/client/src/app/components/access-table/access-table.component.ts
- 68
+ 78
@@ -2423,7 +2427,7 @@
apps/client/src/app/pages/pricing/pricing-page.html
- 288
+ 293
@@ -2439,7 +2443,7 @@
apps/client/src/app/pages/pricing/pricing-page.html
- 294
+ 299
@@ -2511,7 +2515,7 @@
apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
- 158
+ 159
@@ -2827,7 +2831,7 @@
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 251
+ 257
apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html
@@ -3099,7 +3103,7 @@
apps/client/src/app/pages/pricing/pricing-page.html
- 213
+ 218
@@ -3119,7 +3123,7 @@
apps/client/src/app/pages/pricing/pricing-page.html
- 217
+ 222
@@ -3135,7 +3139,7 @@
apps/client/src/app/pages/pricing/pricing-page.html
- 221
+ 226
@@ -3151,7 +3155,7 @@
apps/client/src/app/pages/pricing/pricing-page.html
- 225
+ 230
@@ -3163,7 +3167,7 @@
apps/client/src/app/pages/pricing/pricing-page.html
- 240
+ 245
@@ -3179,7 +3183,7 @@
apps/client/src/app/pages/pricing/pricing-page.html
- 252
+ 257
@@ -3299,7 +3303,7 @@
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 224
+ 230
@@ -3351,7 +3355,7 @@
apps/client/src/app/pages/pricing/pricing-page.html
- 274
+ 279
@@ -3383,7 +3387,7 @@
Do you really want to close your Ghostfolio account?
apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
- 173
+ 174
@@ -3391,7 +3395,7 @@
Do you really want to remove this sign in method?
apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
- 247
+ 248
@@ -3399,7 +3403,7 @@
Oops! There was an error setting up biometric authentication.
apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
- 301
+ 302
@@ -3455,7 +3459,7 @@
Locale
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 123
+ 129
@@ -3463,7 +3467,7 @@
Date and number format
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 125
+ 131
@@ -3471,7 +3475,7 @@
Appearance
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 148
+ 154
@@ -3479,7 +3483,7 @@
Auto
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 162
+ 168
@@ -3487,7 +3491,7 @@
Light
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 163
+ 169
@@ -3495,7 +3499,7 @@
Dark
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 164
+ 170
@@ -3503,7 +3507,7 @@
Zen Mode
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 173
+ 179
apps/client/src/app/pages/features/features-page.html
@@ -3515,7 +3519,7 @@
Distraction-free experience for turbulent times
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 174
+ 180
@@ -3523,7 +3527,7 @@
Biometric Authentication
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 190
+ 196
@@ -3531,7 +3535,7 @@
Sign in with fingerprint
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 191
+ 197
@@ -3539,7 +3543,7 @@
Experimental Features
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 207
+ 213
@@ -3547,7 +3551,7 @@
Sneak peek at upcoming functionality
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 208
+ 214
@@ -3555,7 +3559,7 @@
Export Data
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 232
+ 238
@@ -3563,7 +3567,7 @@
Danger Zone
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 244
+ 250
@@ -3571,7 +3575,7 @@
Close Account
apps/client/src/app/components/user-account-settings/user-account-settings.html
- 279
+ 285
@@ -4083,7 +4087,7 @@
Open Source Software
apps/client/src/app/pages/features/features-page.html
- 278
+ 280
@@ -4091,7 +4095,7 @@
Get Started
apps/client/src/app/pages/features/features-page.html
- 303
+ 305
apps/client/src/app/pages/public/public-page.html
@@ -4211,7 +4215,7 @@
apps/client/src/app/pages/pricing/pricing-page.html
- 324
+ 329
@@ -5067,7 +5071,7 @@
Dividend
apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
- 38
+ 41
libs/ui/src/lib/i18n.ts
@@ -5079,11 +5083,11 @@
Investment
apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
- 42
+ 46
apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
- 56
+ 60
apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
@@ -5095,7 +5099,7 @@
Monthly
apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
- 50
+ 54
@@ -5103,7 +5107,7 @@
Yearly
apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
- 51
+ 55
@@ -5119,7 +5123,7 @@
Absolute Asset Performance
apps/client/src/app/pages/portfolio/analysis/analysis-page.html
- 28
+ 60
@@ -5127,7 +5131,7 @@
Asset Performance
apps/client/src/app/pages/portfolio/analysis/analysis-page.html
- 49
+ 81
@@ -5135,7 +5139,7 @@
Absolute Currency Performance
apps/client/src/app/pages/portfolio/analysis/analysis-page.html
- 71
+ 103
@@ -5143,7 +5147,7 @@
Currency Performance
apps/client/src/app/pages/portfolio/analysis/analysis-page.html
- 95
+ 127
@@ -5151,7 +5155,7 @@
Absolute Net Performance
apps/client/src/app/pages/portfolio/analysis/analysis-page.html
- 118
+ 150
@@ -5159,7 +5163,7 @@
Net Performance
apps/client/src/app/pages/portfolio/analysis/analysis-page.html
- 137
+ 169
@@ -5167,7 +5171,7 @@
Top
apps/client/src/app/pages/portfolio/analysis/analysis-page.html
- 165
+ 197
@@ -5175,7 +5179,7 @@
Bottom
apps/client/src/app/pages/portfolio/analysis/analysis-page.html
- 214
+ 246
@@ -5183,7 +5187,7 @@
Portfolio Evolution
apps/client/src/app/pages/portfolio/analysis/analysis-page.html
- 267
+ 299
@@ -5191,7 +5195,7 @@
Investment Timeline
apps/client/src/app/pages/portfolio/analysis/analysis-page.html
- 294
+ 326
@@ -5199,7 +5203,7 @@
Current Streak
apps/client/src/app/pages/portfolio/analysis/analysis-page.html
- 315
+ 347
@@ -5207,7 +5211,7 @@
Longest Streak
apps/client/src/app/pages/portfolio/analysis/analysis-page.html
- 324
+ 356
@@ -5215,7 +5219,7 @@
Dividend Timeline
apps/client/src/app/pages/portfolio/analysis/analysis-page.html
- 351
+ 383
@@ -5319,7 +5323,7 @@
apps/client/src/app/pages/pricing/pricing-page.html
- 201
+ 206
@@ -5335,7 +5339,7 @@
apps/client/src/app/pages/pricing/pricing-page.html
- 205
+ 210
@@ -5351,7 +5355,7 @@
apps/client/src/app/pages/pricing/pricing-page.html
- 209
+ 214
@@ -5367,7 +5371,7 @@
apps/client/src/app/pages/pricing/pricing-page.html
- 229
+ 234
@@ -5415,7 +5419,7 @@
apps/client/src/app/pages/pricing/pricing-page.html
- 261
+ 266
@@ -5423,7 +5427,7 @@
For ambitious investors who need the full picture of their financial assets.
apps/client/src/app/pages/pricing/pricing-page.html
- 194
+ 199
@@ -5431,7 +5435,7 @@
Email and Chat Support
apps/client/src/app/pages/pricing/pricing-page.html
- 257
+ 262
@@ -5439,7 +5443,7 @@
One-time payment, no auto-renewal.
apps/client/src/app/pages/pricing/pricing-page.html
- 298
+ 303
@@ -5447,7 +5451,7 @@
It’s free.
apps/client/src/app/pages/pricing/pricing-page.html
- 327
+ 332
@@ -7636,6 +7640,30 @@
59
+
+ AI prompt has been copied to the clipboard
+ AI prompt has been copied to the clipboard
+
+ apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
+ 149
+
+
+
+ Copy AI prompt to clipboard
+ Copy AI prompt to clipboard
+
+ apps/client/src/app/pages/portfolio/analysis/analysis-page.html
+ 27
+
+
+
+ Link has been copied to the clipboard
+ Link has been copied to the clipboard
+
+ apps/client/src/app/components/access-table/access-table.component.ts
+ 64
+
+