Browse Source

Merge branch 'main' into issue_5421

pull/5434/head
Thomas Kaul 4 months ago
committed by GitHub
parent
commit
ff38646a22
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 36
      CHANGELOG.md
  2. 2
      apps/api/src/app/admin/queue/queue.service.ts
  3. 18
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  4. 208
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts
  5. 132
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts
  6. 2
      apps/api/src/app/subscription/subscription.service.ts
  7. 9
      apps/api/src/services/data-provider/coingecko/coingecko.service.ts
  8. 29
      apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts
  9. 18
      apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts
  10. 4
      apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts
  11. 3
      apps/client/project.json
  12. 4
      apps/client/src/app/app-routing.module.ts
  13. 39
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts
  14. 2
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html
  15. 38
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.module.ts
  16. 24
      apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts
  17. 27
      apps/client/src/app/components/benchmark-comparator/benchmark-comparator.module.ts
  18. 7
      apps/client/src/app/components/data-provider-status/data-provider-status.component.ts
  19. 2
      apps/client/src/app/components/dialog-footer/dialog-footer.component.html
  20. 8
      apps/client/src/app/components/dialog-footer/dialog-footer.component.scss
  21. 3
      apps/client/src/app/components/dialog-footer/dialog-footer.component.ts
  22. 10
      apps/client/src/app/components/dialog-header/dialog-header.component.html
  23. 3
      apps/client/src/app/components/dialog-header/dialog-header.component.scss
  24. 3
      apps/client/src/app/components/dialog-header/dialog-header.component.ts
  25. 2
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
  26. 4
      apps/client/src/app/components/home-summary/home-summary.component.ts
  27. 6
      apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html
  28. 12
      apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts
  29. 15
      apps/client/src/app/components/portfolio-summary/portfolio-summary.module.ts
  30. 9
      apps/client/src/app/components/world-map-chart/world-map-chart.component.ts
  31. 12
      apps/client/src/app/components/world-map-chart/world-map-chart.module.ts
  32. 53
      apps/client/src/app/pages/about/overview/about-overview-page.html
  33. 12
      apps/client/src/app/pages/accounts/accounts-page.component.ts
  34. 4
      apps/client/src/app/pages/landing/landing-page.component.ts
  35. 2
      apps/client/src/app/pages/portfolio/activities/activities-page.component.ts
  36. 1
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
  37. 2
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html
  38. 40
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.scss
  39. 8
      apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
  40. 4
      apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
  41. 20
      apps/client/src/app/pages/public/public-page-routing.module.ts
  42. 29
      apps/client/src/app/pages/public/public-page.component.ts
  43. 28
      apps/client/src/app/pages/public/public-page.module.ts
  44. 13
      apps/client/src/app/pages/public/public-page.routes.ts
  45. 916
      apps/client/src/locales/messages.ca.xlf
  46. 970
      apps/client/src/locales/messages.de.xlf
  47. 922
      apps/client/src/locales/messages.es.xlf
  48. 970
      apps/client/src/locales/messages.fr.xlf
  49. 966
      apps/client/src/locales/messages.it.xlf
  50. 892
      apps/client/src/locales/messages.nl.xlf
  51. 932
      apps/client/src/locales/messages.pl.xlf
  52. 972
      apps/client/src/locales/messages.pt.xlf
  53. 924
      apps/client/src/locales/messages.tr.xlf
  54. 966
      apps/client/src/locales/messages.uk.xlf
  55. 709
      apps/client/src/locales/messages.xlf
  56. 844
      apps/client/src/locales/messages.zh.xlf
  57. 2
      libs/ui/src/lib/benchmark/benchmark-detail-dialog/benchmark-detail-dialog.html
  58. 55
      package-lock.json
  59. 14
      package.json
  60. 42
      test/import/ok/btcusd-short.json

36
CHANGELOG.md

@ -11,9 +11,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Enabled automatic data gathering for custom currencies added via the currency management in the admin control panel - Enabled automatic data gathering for custom currencies added via the currency management in the admin control panel
### Fixed
- Fixed an issue related to the error handling in the data provider status component
## 2.196.0 - 2025-09-04
### Changed
- Localized the content of the about page
- Refactored the public page to standalone
- Refactored the dialog footer component
- Refactored the dialog header component
- Refactored the account detail dialog component to standalone
- Refactored the benchmark comparator component to standalone
- Refactored the portfolio summary component to standalone
- Refactored the world map chart component to standalone
- Enabled the trim option in the `extract-i18n` configuration
- Improved the language localization for German (`de`)
- Upgraded the _Stripe_ dependencies
- Upgraded `ngx-device-detector` from version `10.0.2` to `10.1.0`
- Upgraded `ngx-skeleton-loader` from version `11.2.1` to `11.3.0`
- Upgraded `yahoo-finance2` from version `3.6.4` to `3.8.0`
### Fixed
- Fixed an issue in the average price calculation for buy and sell activities of short positions
- Fixed the number of attempts in the queue jobs view of the admin control panel
## 2.195.0 - 2025-08-29
### Changed ### Changed
- Reused the request timeout in various functions of the data providers
- Refactored the _ZEN_ page to standalone - Refactored the _ZEN_ page to standalone
- Upgraded `chart.js` from version `4.4.9` to `4.5.0`
### Fixed
- Handled an exception in the get quotes functionality of the _Financial Modeling Prep_ service
## 2.194.0 - 2025-08-27 ## 2.194.0 - 2025-08-27

2
apps/api/src/app/admin/queue/queue.service.ts

@ -71,7 +71,7 @@ export class QueueService {
.slice(0, limit) .slice(0, limit)
.map(async (job) => { .map(async (job) => {
return { return {
attemptsMade: job.attemptsMade + 1, attemptsMade: job.attemptsMade,
data: job.data, data: job.data,
finishedOn: job.finishedOn, finishedOn: job.finishedOn,
id: job.id, id: job.id,

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

@ -927,13 +927,25 @@ export abstract class PortfolioCalculator {
.plus(oldAccumulatedSymbol.quantity); .plus(oldAccumulatedSymbol.quantity);
if (type === 'BUY') { if (type === 'BUY') {
if (oldAccumulatedSymbol.investment.gte(0)) {
investment = oldAccumulatedSymbol.investment.plus( investment = oldAccumulatedSymbol.investment.plus(
quantity.mul(unitPrice) quantity.mul(unitPrice)
); );
} else {
investment = oldAccumulatedSymbol.investment.plus(
quantity.mul(oldAccumulatedSymbol.averagePrice)
);
}
} else if (type === 'SELL') { } else if (type === 'SELL') {
if (oldAccumulatedSymbol.investment.gt(0)) {
investment = oldAccumulatedSymbol.investment.minus( investment = oldAccumulatedSymbol.investment.minus(
quantity.mul(oldAccumulatedSymbol.averagePrice) quantity.mul(oldAccumulatedSymbol.averagePrice)
); );
} else {
investment = oldAccumulatedSymbol.investment.minus(
quantity.mul(unitPrice)
);
}
} }
currentTransactionPointItem = { currentTransactionPointItem = {
@ -942,9 +954,9 @@ export abstract class PortfolioCalculator {
investment, investment,
skipErrors, skipErrors,
symbol, symbol,
averagePrice: newQuantity.gt(0) averagePrice: newQuantity.eq(0)
? investment.div(newQuantity) ? new Big(0)
: new Big(0), : investment.div(newQuantity).abs(),
dividend: new Big(0), dividend: new Big(0),
fee: oldAccumulatedSymbol.fee.plus(fee), fee: oldAccumulatedSymbol.fee.plus(fee),
firstBuyDate: oldAccumulatedSymbol.firstBuyDate, firstBuyDate: oldAccumulatedSymbol.firstBuyDate,

208
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts

@ -0,0 +1,208 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import {
activityDummyData,
symbolProfileDummyData,
userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { parseDate } from '@ghostfolio/common/helper';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Big } from 'big.js';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
};
});
jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock;
})
};
}
);
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
};
});
describe('PortfolioCalculator', () => {
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService;
let portfolioCalculatorFactory: PortfolioCalculatorFactory;
let portfolioSnapshotService: PortfolioSnapshotService;
let redisCacheService: RedisCacheService;
beforeEach(() => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,
null,
null,
null
);
portfolioSnapshotService = new PortfolioSnapshotService(null);
redisCacheService = new RedisCacheService(null, null);
portfolioCalculatorFactory = new PortfolioCalculatorFactory(
configurationService,
currentRateService,
exchangeRateDataService,
portfolioSnapshotService,
redisCacheService
);
});
describe('get current positions', () => {
it.only('with BALN.SW buy and buy', async () => {
jest.useFakeTimers().setSystemTime(parseDate('2021-12-18').getTime());
const activities: Activity[] = [
{
...activityDummyData,
date: new Date('2021-11-22'),
feeInAssetProfileCurrency: 1.55,
quantity: 2,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'CHF',
dataSource: 'YAHOO',
name: 'Bâloise Holding AG',
symbol: 'BALN.SW'
},
type: 'BUY',
unitPriceInAssetProfileCurrency: 142.9
},
{
...activityDummyData,
date: new Date('2021-11-30'),
feeInAssetProfileCurrency: 1.65,
quantity: 2,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'CHF',
dataSource: 'YAHOO',
name: 'Bâloise Holding AG',
symbol: 'BALN.SW'
},
type: 'BUY',
unitPriceInAssetProfileCurrency: 136.6
}
];
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities,
calculationType: PerformanceCalculationType.ROAI,
currency: 'CHF',
userId: userDummyData.id
});
const portfolioSnapshot = await portfolioCalculator.computeSnapshot();
const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData,
groupBy: 'month'
});
expect(portfolioSnapshot).toMatchObject({
currentValueInBaseCurrency: new Big('595.6'),
errors: [],
hasErrors: false,
positions: [
{
averagePrice: new Big('139.75'),
currency: 'CHF',
dataSource: 'YAHOO',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('3.2'),
feeInBaseCurrency: new Big('3.2'),
firstBuyDate: '2021-11-22',
grossPerformance: new Big('36.6'),
grossPerformancePercentage: new Big('0.07706261539956593567'),
grossPerformancePercentageWithCurrencyEffect: new Big(
'0.07706261539956593567'
),
grossPerformanceWithCurrencyEffect: new Big('36.6'),
investment: new Big('559'),
investmentWithCurrencyEffect: new Big('559'),
netPerformance: new Big('33.4'),
netPerformancePercentage: new Big('0.07032490039195361342'),
netPerformancePercentageWithCurrencyEffectMap: {
max: new Big('0.06986689805847808234')
},
netPerformanceWithCurrencyEffectMap: {
max: new Big('33.4')
},
marketPrice: 148.9,
marketPriceInBaseCurrency: 148.9,
quantity: new Big('4'),
symbol: 'BALN.SW',
tags: [],
timeWeightedInvestment: new Big('474.93846153846153846154'),
timeWeightedInvestmentWithCurrencyEffect: new Big(
'474.93846153846153846154'
),
transactionCount: 2,
valueInBaseCurrency: new Big('595.6')
}
],
totalFeesWithCurrencyEffect: new Big('3.2'),
totalInterestWithCurrencyEffect: new Big('0'),
totalInvestment: new Big('559'),
totalInvestmentWithCurrencyEffect: new Big('559'),
totalLiabilitiesWithCurrencyEffect: new Big('0')
});
expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject(
expect.objectContaining({
netPerformance: 33.4,
netPerformanceInPercentage: 0.07032490039195362,
netPerformanceInPercentageWithCurrencyEffect: 0.07032490039195362,
netPerformanceWithCurrencyEffect: 33.4,
totalInvestmentValueWithCurrencyEffect: 559
})
);
expect(investments).toEqual([
{ date: '2021-11-22', investment: new Big('285.8') },
{ date: '2021-11-30', investment: new Big('559') }
]);
expect(investmentsByMonth).toEqual([
{ date: '2021-11-01', investment: 559 },
{ date: '2021-12-01', investment: 0 }
]);
});
});
});

132
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts

@ -0,0 +1,132 @@
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import {
activityDummyData,
loadActivityExportFile,
symbolProfileDummyData,
userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { parseDate } from '@ghostfolio/common/helper';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Tag } from '@prisma/client';
import { Big } from 'big.js';
import { join } from 'path';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
};
});
jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock;
})
};
}
);
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
};
});
describe('PortfolioCalculator', () => {
let activityDtos: CreateOrderDto[];
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService;
let portfolioCalculatorFactory: PortfolioCalculatorFactory;
let portfolioSnapshotService: PortfolioSnapshotService;
let redisCacheService: RedisCacheService;
beforeAll(() => {
activityDtos = loadActivityExportFile(
join(__dirname, '../../../../../../../test/import/ok/btcusd-short.json')
);
});
beforeEach(() => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,
null,
null,
null
);
portfolioSnapshotService = new PortfolioSnapshotService(null);
redisCacheService = new RedisCacheService(null, null);
portfolioCalculatorFactory = new PortfolioCalculatorFactory(
configurationService,
currentRateService,
exchangeRateDataService,
portfolioSnapshotService,
redisCacheService
);
});
describe('get current positions', () => {
it.only('with BTCUSD short sell (in USD)', async () => {
jest.useFakeTimers().setSystemTime(parseDate('2022-01-14').getTime());
const activities: Activity[] = activityDtos.map((activity) => ({
...activityDummyData,
...activity,
date: parseDate(activity.date),
feeInAssetProfileCurrency: activity.fee,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD',
dataSource: activity.dataSource,
name: 'Bitcoin',
symbol: activity.symbol
},
tags: activity.tags?.map((id) => {
return { id } as Tag;
}),
unitPriceInAssetProfileCurrency: activity.unitPrice
}));
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities,
calculationType: PerformanceCalculationType.ROAI,
currency: 'USD',
userId: userDummyData.id
});
const portfolioSnapshot = await portfolioCalculator.computeSnapshot();
expect(portfolioSnapshot.positions[0].averagePrice).toEqual(
Big(45647.95)
);
});
});
});

2
apps/api/src/app/subscription/subscription.service.ts

@ -32,7 +32,7 @@ export class SubscriptionService {
this.stripe = new Stripe( this.stripe = new Stripe(
this.configurationService.get('STRIPE_SECRET_KEY'), this.configurationService.get('STRIPE_SECRET_KEY'),
{ {
apiVersion: '2025-06-30.basil' apiVersion: '2025-08-27.basil'
} }
); );
} }

9
apps/api/src/services/data-provider/coingecko/coingecko.service.ts

@ -214,15 +214,16 @@ export class CoinGeckoService implements DataProviderInterface {
return 'bitcoin'; return 'bitcoin';
} }
public async search({ query }: GetSearchParams): Promise<LookupResponse> { public async search({
query,
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT')
}: GetSearchParams): Promise<LookupResponse> {
let items: LookupItem[] = []; let items: LookupItem[] = [];
try { try {
const { coins } = await fetch(`${this.apiUrl}/search?query=${query}`, { const { coins } = await fetch(`${this.apiUrl}/search?query=${query}`, {
headers: this.headers, headers: this.headers,
signal: AbortSignal.timeout( signal: AbortSignal.timeout(requestTimeout)
this.configurationService.get('REQUEST_TIMEOUT')
)
}).then((res) => res.json()); }).then((res) => res.json());
items = coins.map(({ id: symbol, name }) => { items = coins.map(({ id: symbol, name }) => {

29
apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts

@ -51,9 +51,13 @@ export class EodHistoricalDataService implements DataProviderInterface {
} }
public async getAssetProfile({ public async getAssetProfile({
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
symbol symbol
}: GetAssetProfileParams): Promise<Partial<SymbolProfile>> { }: GetAssetProfileParams): Promise<Partial<SymbolProfile>> {
const [searchResult] = await this.getSearchResult(symbol); const [searchResult] = await this.getSearchResult({
requestTimeout,
query: symbol
});
if (!searchResult) { if (!searchResult) {
return undefined; return undefined;
@ -304,8 +308,11 @@ export class EodHistoricalDataService implements DataProviderInterface {
return 'AAPL.US'; return 'AAPL.US';
} }
public async search({ query }: GetSearchParams): Promise<LookupResponse> { public async search({
const searchResult = await this.getSearchResult(query); query,
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT')
}: GetSearchParams): Promise<LookupResponse> {
const searchResult = await this.getSearchResult({ query, requestTimeout });
return { return {
items: searchResult items: searchResult
@ -394,7 +401,13 @@ export class EodHistoricalDataService implements DataProviderInterface {
return name; return name;
} }
private async getSearchResult(aQuery: string) { private async getSearchResult({
query,
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT')
}: {
query: string;
requestTimeout?: number;
}) {
let searchResult: (LookupItem & { let searchResult: (LookupItem & {
assetClass: AssetClass; assetClass: AssetClass;
assetSubClass: AssetSubClass; assetSubClass: AssetSubClass;
@ -403,11 +416,9 @@ export class EodHistoricalDataService implements DataProviderInterface {
try { try {
const response = await fetch( const response = await fetch(
`${this.URL}/search/${aQuery}?api_token=${this.apiKey}`, `${this.URL}/search/${query}?api_token=${this.apiKey}`,
{ {
signal: AbortSignal.timeout( signal: AbortSignal.timeout(requestTimeout)
this.configurationService.get('REQUEST_TIMEOUT')
)
} }
).then((res) => res.json()); ).then((res) => res.json());
@ -433,7 +444,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
let message = error; let message = error;
if (['AbortError', 'TimeoutError'].includes(error?.name)) { if (['AbortError', 'TimeoutError'].includes(error?.name)) {
message = `RequestError: The operation to search for ${aQuery} was aborted because the request to the data provider took more than ${( message = `RequestError: The operation to search for ${query} was aborted because the request to the data provider took more than ${(
this.configurationService.get('REQUEST_TIMEOUT') / 1000 this.configurationService.get('REQUEST_TIMEOUT') / 1000
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;
} }

18
apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts

@ -365,8 +365,13 @@ export class FinancialModelingPrepService implements DataProviderInterface {
await Promise.all( await Promise.all(
quotes.map(({ symbol }) => { quotes.map(({ symbol }) => {
return this.getAssetProfile({ symbol }).then(({ currency }) => { return this.getAssetProfile({
currencyBySymbolMap[symbol] = { currency }; requestTimeout,
symbol
}).then((assetProfile) => {
if (assetProfile?.currency) {
currencyBySymbolMap[symbol] = { currency: assetProfile.currency };
}
}); });
}) })
); );
@ -411,7 +416,10 @@ export class FinancialModelingPrepService implements DataProviderInterface {
return 'AAPL'; return 'AAPL';
} }
public async search({ query }: GetSearchParams): Promise<LookupResponse> { public async search({
query,
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT')
}: GetSearchParams): Promise<LookupResponse> {
const assetProfileBySymbolMap: { const assetProfileBySymbolMap: {
[symbol: string]: Partial<SymbolProfile>; [symbol: string]: Partial<SymbolProfile>;
} = {}; } = {};
@ -422,9 +430,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
const result = await fetch( const result = await fetch(
`${this.getUrl({ version: 'stable' })}/search-isin?isin=${query.toUpperCase()}&apikey=${this.apiKey}`, `${this.getUrl({ version: 'stable' })}/search-isin?isin=${query.toUpperCase()}&apikey=${this.apiKey}`,
{ {
signal: AbortSignal.timeout( signal: AbortSignal.timeout(requestTimeout)
this.configurationService.get('REQUEST_TIMEOUT')
)
} }
).then((res) => res.json()); ).then((res) => res.json());

4
apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts

@ -284,8 +284,8 @@ export class GhostfolioService implements DataProviderInterface {
} }
public async search({ public async search({
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'), query,
query requestTimeout = this.configurationService.get('REQUEST_TIMEOUT')
}: GetSearchParams): Promise<LookupResponse> { }: GetSearchParams): Promise<LookupResponse> {
let searchResult: LookupResponse = { items: [] }; let searchResult: LookupResponse = { items: [] };

3
apps/client/project.json

@ -271,7 +271,8 @@
"messages.tr.xlf", "messages.tr.xlf",
"messages.uk.xlf", "messages.uk.xlf",
"messages.zh.xlf" "messages.zh.xlf"
] ],
"trim": true
} }
}, },
"lint": { "lint": {

4
apps/client/src/app/app-routing.module.ts

@ -111,9 +111,7 @@ const routes: Routes = [
{ {
path: publicRoutes.public.path, path: publicRoutes.public.path,
loadChildren: () => loadChildren: () =>
import('./pages/public/public-page.module').then( import('./pages/public/public-page.routes').then((m) => m.routes)
(m) => m.PublicPageModule
)
}, },
{ {
path: publicRoutes.register.path, path: publicRoutes.register.path,

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

@ -1,5 +1,8 @@
import { CreateAccountBalanceDto } from '@ghostfolio/api/app/account-balance/create-account-balance.dto'; import { CreateAccountBalanceDto } from '@ghostfolio/api/app/account-balance/create-account-balance.dto';
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { GfDialogFooterComponent } from '@ghostfolio/client/components/dialog-footer/dialog-footer.component';
import { GfDialogHeaderComponent } from '@ghostfolio/client/components/dialog-header/dialog-header.component';
import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { NUMERICAL_PRECISION_THRESHOLD_6_FIGURES } from '@ghostfolio/common/config'; import { NUMERICAL_PRECISION_THRESHOLD_6_FIGURES } from '@ghostfolio/common/config';
@ -13,7 +16,12 @@ import {
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { OrderWithAccount } from '@ghostfolio/common/types'; import { OrderWithAccount } from '@ghostfolio/common/types';
import { GfAccountBalancesComponent } from '@ghostfolio/ui/account-balances';
import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table';
import { GfHoldingsTableComponent } from '@ghostfolio/ui/holdings-table';
import { GfValueComponent } from '@ghostfolio/ui/value';
import { CommonModule } from '@angular/common';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
@ -22,10 +30,15 @@ import {
OnDestroy, OnDestroy,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatDialogModule } from '@angular/material/dialog';
import { Sort, SortDirection } from '@angular/material/sort'; import { Sort, SortDirection } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { format, parseISO } from 'date-fns'; import { format, parseISO } from 'date-fns';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
@ -35,20 +48,36 @@ import {
walletOutline walletOutline
} from 'ionicons/icons'; } from 'ionicons/icons';
import { isNumber } from 'lodash'; import { isNumber } from 'lodash';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { forkJoin, Subject } from 'rxjs'; import { forkJoin, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { AccountDetailDialogParams } from './interfaces/interfaces'; import { AccountDetailDialogParams } from './interfaces/interfaces';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'd-flex flex-column h-100' }, host: { class: 'd-flex flex-column h-100' },
imports: [
CommonModule,
GfAccountBalancesComponent,
GfActivitiesTableComponent,
GfDialogFooterComponent,
GfDialogHeaderComponent,
GfHoldingsTableComponent,
GfInvestmentChartModule,
GfValueComponent,
IonIcon,
MatButtonModule,
MatDialogModule,
MatTabsModule,
NgxSkeletonLoaderModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-account-detail-dialog', selector: 'gf-account-detail-dialog',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: 'account-detail-dialog.html',
styleUrls: ['./account-detail-dialog.component.scss'], styleUrls: ['./account-detail-dialog.component.scss'],
standalone: false templateUrl: 'account-detail-dialog.html'
}) })
export class AccountDetailDialog implements OnDestroy, OnInit { export class GfAccountDetailDialogComponent implements OnDestroy, OnInit {
public accountBalances: AccountBalancesResponse['balances']; public accountBalances: AccountBalancesResponse['balances'];
public activities: OrderWithAccount[]; public activities: OrderWithAccount[];
public balance: number; public balance: number;
@ -81,7 +110,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
@Inject(MAT_DIALOG_DATA) public data: AccountDetailDialogParams, @Inject(MAT_DIALOG_DATA) public data: AccountDetailDialogParams,
private dataService: DataService, private dataService: DataService,
public dialogRef: MatDialogRef<AccountDetailDialog>, public dialogRef: MatDialogRef<GfAccountDetailDialogComponent>,
private router: Router, private router: Router,
private userService: UserService private userService: UserService
) { ) {

2
apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html

@ -1,5 +1,4 @@
<gf-dialog-header <gf-dialog-header
mat-dialog-title
position="center" position="center"
[deviceType]="data.deviceType" [deviceType]="data.deviceType"
[title]="name" [title]="name"
@ -166,7 +165,6 @@
</div> </div>
<gf-dialog-footer <gf-dialog-footer
mat-dialog-actions
[deviceType]="data.deviceType" [deviceType]="data.deviceType"
(closeButtonClicked)="onClose()" (closeButtonClicked)="onClose()"
/> />

38
apps/client/src/app/components/account-detail-dialog/account-detail-dialog.module.ts

@ -1,38 +0,0 @@
import { GfDialogFooterComponent } from '@ghostfolio/client/components/dialog-footer/dialog-footer.component';
import { GfDialogHeaderComponent } from '@ghostfolio/client/components/dialog-header/dialog-header.component';
import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module';
import { GfAccountBalancesComponent } from '@ghostfolio/ui/account-balances';
import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table';
import { GfHoldingsTableComponent } from '@ghostfolio/ui/holdings-table';
import { GfValueComponent } from '@ghostfolio/ui/value';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog';
import { MatTabsModule } from '@angular/material/tabs';
import { IonIcon } from '@ionic/angular/standalone';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { AccountDetailDialog } from './account-detail-dialog.component';
@NgModule({
declarations: [AccountDetailDialog],
imports: [
CommonModule,
GfAccountBalancesComponent,
GfActivitiesTableComponent,
GfDialogFooterComponent,
GfDialogHeaderComponent,
GfHoldingsTableComponent,
GfInvestmentChartModule,
GfValueComponent,
IonIcon,
MatButtonModule,
MatDialogModule,
MatTabsModule,
NgxSkeletonLoaderModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfAccountDetailDialogModule {}

24
apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts

@ -15,7 +15,9 @@ import { LineChartItem, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { ColorScheme } from '@ghostfolio/common/types'; import { ColorScheme } from '@ghostfolio/common/types';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { CommonModule } from '@angular/common';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
@ -26,6 +28,10 @@ import {
Output, Output,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatSelectModule } from '@angular/material/select';
import { RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone';
import { SymbolProfile } from '@prisma/client'; import { SymbolProfile } from '@prisma/client';
import { import {
Chart, Chart,
@ -42,15 +48,25 @@ import 'chartjs-adapter-date-fns';
import annotationPlugin from 'chartjs-plugin-annotation'; import annotationPlugin from 'chartjs-plugin-annotation';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { arrowForwardOutline } from 'ionicons/icons'; import { arrowForwardOutline } from 'ionicons/icons';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@Component({ @Component({
selector: 'gf-benchmark-comparator',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './benchmark-comparator.component.html', imports: [
CommonModule,
FormsModule,
GfPremiumIndicatorComponent,
IonIcon,
MatSelectModule,
NgxSkeletonLoaderModule,
ReactiveFormsModule,
RouterModule
],
selector: 'gf-benchmark-comparator',
styleUrls: ['./benchmark-comparator.component.scss'], styleUrls: ['./benchmark-comparator.component.scss'],
standalone: false templateUrl: './benchmark-comparator.component.html'
}) })
export class BenchmarkComparatorComponent implements OnChanges, OnDestroy { export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
@Input() benchmark: Partial<SymbolProfile>; @Input() benchmark: Partial<SymbolProfile>;
@Input() benchmarkDataItems: LineChartItem[] = []; @Input() benchmarkDataItems: LineChartItem[] = [];
@Input() benchmarks: Partial<SymbolProfile>[]; @Input() benchmarks: Partial<SymbolProfile>[];

27
apps/client/src/app/components/benchmark-comparator/benchmark-comparator.module.ts

@ -1,27 +0,0 @@
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatSelectModule } from '@angular/material/select';
import { RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { BenchmarkComparatorComponent } from './benchmark-comparator.component';
@NgModule({
declarations: [BenchmarkComparatorComponent],
exports: [BenchmarkComparatorComponent],
imports: [
CommonModule,
FormsModule,
GfPremiumIndicatorComponent,
IonIcon,
MatSelectModule,
NgxSkeletonLoaderModule,
ReactiveFormsModule,
RouterModule
]
})
export class GfBenchmarkComparatorModule {}

7
apps/client/src/app/components/data-provider-status/data-provider-status.component.ts

@ -18,7 +18,6 @@ import { DataProviderStatus } from './interfaces/interfaces';
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, NgxSkeletonLoaderModule], imports: [CommonModule, NgxSkeletonLoaderModule],
selector: 'gf-data-provider-status', selector: 'gf-data-provider-status',
standalone: true,
templateUrl: './data-provider-status.component.html' templateUrl: './data-provider-status.component.html'
}) })
export class GfDataProviderStatusComponent implements OnDestroy, OnInit { export class GfDataProviderStatusComponent implements OnDestroy, OnInit {
@ -34,12 +33,12 @@ export class GfDataProviderStatusComponent implements OnDestroy, OnInit {
this.status$ = this.dataService this.status$ = this.dataService
.fetchDataProviderHealth(this.dataSource) .fetchDataProviderHealth(this.dataSource)
.pipe( .pipe(
catchError(() => {
return of({ isHealthy: false });
}),
map(() => { map(() => {
return { isHealthy: true }; return { isHealthy: true };
}), }),
catchError(() => {
return of({ isHealthy: false });
}),
takeUntil(this.unsubscribeSubject) takeUntil(this.unsubscribeSubject)
); );
} }

2
apps/client/src/app/components/dialog-footer/dialog-footer.component.html

@ -1,5 +1,7 @@
@if (deviceType === 'mobile') { @if (deviceType === 'mobile') {
<div class="d-flex justify-content-center" mat-dialog-actions>
<button mat-button (click)="onClickCloseButton()"> <button mat-button (click)="onClickCloseButton()">
<ion-icon name="close" size="large" /> <ion-icon name="close" size="large" />
</button> </button>
</div>
} }

8
apps/client/src/app/components/dialog-footer/dialog-footer.component.scss

@ -1,9 +1,3 @@
:host { :host {
display: flex; display: block;
flex: 0 0 auto;
min-height: 0;
@media (min-width: 576px) {
padding: 0 !important;
}
} }

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

@ -6,6 +6,7 @@ import {
Output Output
} from '@angular/core'; } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog';
import { IonIcon } from '@ionic/angular/standalone'; import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { close } from 'ionicons/icons'; import { close } from 'ionicons/icons';
@ -13,7 +14,7 @@ import { close } from 'ionicons/icons';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'justify-content-center' }, host: { class: 'justify-content-center' },
imports: [IonIcon, MatButtonModule], imports: [IonIcon, MatButtonModule, MatDialogModule],
selector: 'gf-dialog-footer', selector: 'gf-dialog-footer',
styleUrls: ['./dialog-footer.component.scss'], styleUrls: ['./dialog-footer.component.scss'],
templateUrl: './dialog-footer.component.html' templateUrl: './dialog-footer.component.html'

10
apps/client/src/app/components/dialog-header/dialog-header.component.html

@ -1,10 +1,12 @@
<span <div class="d-flex" mat-dialog-title>
<span
class="flex-grow-1 text-truncate" class="flex-grow-1 text-truncate"
[ngClass]="{ 'text-center': position === 'center' }" [ngClass]="{ 'text-center': position === 'center' }"
>{{ title }}</span >{{ title }}</span
> >
@if (deviceType !== 'mobile') { @if (deviceType !== 'mobile') {
<button class="no-min-width px-0" mat-button (click)="onClickCloseButton()"> <button class="no-min-width px-0" mat-button (click)="onClickCloseButton()">
<ion-icon name="close" size="large" /> <ion-icon name="close" size="large" />
</button> </button>
} }
</div>

3
apps/client/src/app/components/dialog-header/dialog-header.component.scss

@ -1,4 +1,3 @@
:host { :host {
align-items: center; display: block;
display: flex;
} }

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

@ -7,6 +7,7 @@ import {
Output Output
} from '@angular/core'; } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog';
import { IonIcon } from '@ionic/angular/standalone'; import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { close } from 'ionicons/icons'; import { close } from 'ionicons/icons';
@ -14,7 +15,7 @@ import { close } from 'ionicons/icons';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'justify-content-center' }, host: { class: 'justify-content-center' },
imports: [CommonModule, IonIcon, MatButtonModule], imports: [CommonModule, IonIcon, MatButtonModule, MatDialogModule],
selector: 'gf-dialog-header', selector: 'gf-dialog-header',
styleUrls: ['./dialog-header.component.scss'], styleUrls: ['./dialog-header.component.scss'],
templateUrl: './dialog-header.component.html' templateUrl: './dialog-header.component.html'

2
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html

@ -1,5 +1,4 @@
<gf-dialog-header <gf-dialog-header
mat-dialog-title
position="center" position="center"
[deviceType]="data.deviceType" [deviceType]="data.deviceType"
[title]="SymbolProfile?.name ?? SymbolProfile?.symbol" [title]="SymbolProfile?.name ?? SymbolProfile?.symbol"
@ -459,7 +458,6 @@
</div> </div>
<gf-dialog-footer <gf-dialog-footer
mat-dialog-actions
[deviceType]="data.deviceType" [deviceType]="data.deviceType"
(closeButtonClicked)="onClose()" (closeButtonClicked)="onClose()"
/> />

4
apps/client/src/app/components/home-summary/home-summary.component.ts

@ -1,4 +1,4 @@
import { GfPortfolioSummaryModule } from '@ghostfolio/client/components/portfolio-summary/portfolio-summary.module'; import { GfPortfolioSummaryComponent } from '@ghostfolio/client/components/portfolio-summary/portfolio-summary.component';
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 { 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';
@ -22,7 +22,7 @@ import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
@Component({ @Component({
imports: [GfPortfolioSummaryModule, MatCardModule], imports: [GfPortfolioSummaryComponent, MatCardModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA], schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-home-summary', selector: 'gf-home-summary',
styleUrls: ['./home-summary.scss'], styleUrls: ['./home-summary.scss'],

6
apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html

@ -1,8 +1,4 @@
<gf-dialog-header <gf-dialog-header [title]="data.title" (closeButtonClicked)="onClose()" />
mat-dialog-title
[title]="data.title"
(closeButtonClicked)="onClose()"
/>
<div class="py-3" mat-dialog-content> <div class="py-3" mat-dialog-content>
<div class="align-items-center d-flex flex-column"> <div class="align-items-center d-flex flex-column">

12
apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts

@ -2,7 +2,9 @@ import { NotificationService } from '@ghostfolio/client/core/notification/notifi
import { getDateFnsLocale, getLocale } from '@ghostfolio/common/helper'; import { getDateFnsLocale, getLocale } from '@ghostfolio/common/helper';
import { PortfolioSummary, User } from '@ghostfolio/common/interfaces'; import { PortfolioSummary, User } from '@ghostfolio/common/interfaces';
import { translate } from '@ghostfolio/ui/i18n'; import { translate } from '@ghostfolio/ui/i18n';
import { GfValueComponent } from '@ghostfolio/ui/value';
import { CommonModule } from '@angular/common';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
@ -11,6 +13,8 @@ import {
OnChanges, OnChanges,
Output Output
} from '@angular/core'; } from '@angular/core';
import { MatTooltipModule } from '@angular/material/tooltip';
import { IonIcon } from '@ionic/angular/standalone';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { import {
@ -19,13 +23,13 @@ import {
} from 'ionicons/icons'; } from 'ionicons/icons';
@Component({ @Component({
selector: 'gf-portfolio-summary',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './portfolio-summary.component.html', imports: [CommonModule, GfValueComponent, IonIcon, MatTooltipModule],
selector: 'gf-portfolio-summary',
styleUrls: ['./portfolio-summary.component.scss'], styleUrls: ['./portfolio-summary.component.scss'],
standalone: false templateUrl: './portfolio-summary.component.html'
}) })
export class PortfolioSummaryComponent implements OnChanges { export class GfPortfolioSummaryComponent implements OnChanges {
@Input() baseCurrency: string; @Input() baseCurrency: string;
@Input() hasPermissionToUpdateUserSettings: boolean; @Input() hasPermissionToUpdateUserSettings: boolean;
@Input() isLoading: boolean; @Input() isLoading: boolean;

15
apps/client/src/app/components/portfolio-summary/portfolio-summary.module.ts

@ -1,15 +0,0 @@
import { GfValueComponent } from '@ghostfolio/ui/value';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatTooltipModule } from '@angular/material/tooltip';
import { PortfolioSummaryComponent } from './portfolio-summary.component';
@NgModule({
declarations: [PortfolioSummaryComponent],
exports: [PortfolioSummaryComponent],
imports: [CommonModule, GfValueComponent, MatTooltipModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfPortfolioSummaryModule {}

9
apps/client/src/app/components/world-map-chart/world-map-chart.component.ts

@ -8,16 +8,17 @@ import {
OnChanges, OnChanges,
OnDestroy OnDestroy
} from '@angular/core'; } from '@angular/core';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import svgMap from 'svgmap'; import svgMap from 'svgmap';
@Component({ @Component({
selector: 'gf-world-map-chart',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './world-map-chart.component.html', imports: [NgxSkeletonLoaderModule],
selector: 'gf-world-map-chart',
styleUrls: ['./world-map-chart.component.scss'], styleUrls: ['./world-map-chart.component.scss'],
standalone: false templateUrl: './world-map-chart.component.html'
}) })
export class WorldMapChartComponent implements OnChanges, OnDestroy { export class GfWorldMapChartComponent implements OnChanges, OnDestroy {
@Input() countries: { [code: string]: { name?: string; value: number } }; @Input() countries: { [code: string]: { name?: string; value: number } };
@Input() format: string; @Input() format: string;
@Input() isInPercent = false; @Input() isInPercent = false;

12
apps/client/src/app/components/world-map-chart/world-map-chart.module.ts

@ -1,12 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { WorldMapChartComponent } from './world-map-chart.component';
@NgModule({
declarations: [WorldMapChartComponent],
exports: [WorldMapChartComponent],
imports: [CommonModule, NgxSkeletonLoaderModule]
})
export class GfWorldMapChartModule {}

53
apps/client/src/app/pages/about/overview/about-overview-page.html

@ -6,10 +6,13 @@
</h1> </h1>
<div class="about-container"> <div class="about-container">
<p> <p>
Ghostfolio is a lightweight wealth management application for <ng-container i18n
individuals to keep track of stocks, ETFs or cryptocurrencies and make >Ghostfolio is a lightweight wealth management application for
solid, data-driven investment decisions. The source code is fully individuals to keep track of stocks, ETFs or cryptocurrencies and
available as make solid, data-driven investment decisions.</ng-container
>
<ng-container i18n>
The source code is fully available as
<a <a
href="https://github.com/ghostfolio/ghostfolio" href="https://github.com/ghostfolio/ghostfolio"
title="Find Ghostfolio on GitHub" title="Find Ghostfolio on GitHub"
@ -20,33 +23,48 @@
href="https://www.gnu.org/licenses/agpl-3.0.html" href="https://www.gnu.org/licenses/agpl-3.0.html"
title="GNU Affero General Public License" title="GNU Affero General Public License"
>AGPL-3.0 license</a >AGPL-3.0 license</a
></ng-container
> >
@if (hasPermissionForStatistics) { @if (hasPermissionForStatistics) {
<ng-container i18n>
and we share aggregated and we share aggregated
<a title="Open Startup" [routerLink]="routerLinkOpenStartup" <a title="Open Startup" [routerLink]="routerLinkOpenStartup"
>key metrics</a >key metrics</a
> >
of the platform’s performance of the platform’s performance</ng-container
>
} }
. The project has been initiated by <ng-container>. </ng-container>
<a href="https://dotsilver.ch" title="Website of Thomas Kaul" <ng-container i18n>The project has been initiated by</ng-container>
>Thomas Kaul</a <a
href="https://dotsilver.ch"
i18n-title
title="Website of Thomas Kaul"
> >
and is driven by the efforts of its Thomas Kaul
</a>
<ng-container i18n
>and is driven by the efforts of its
<a <a
href="https://github.com/ghostfolio/ghostfolio/graphs/contributors" href="https://github.com/ghostfolio/ghostfolio/graphs/contributors"
title="Contributors to Ghostfolio" title="Contributors to Ghostfolio"
>contributors</a >contributors</a
></ng-container
>. >.
@if (hasPermissionForSubscription) { @if (hasPermissionForSubscription) {
Check the system status at <ng-container i18n>Check the system status at</ng-container>
<a href="https://status.ghostfol.io" title="Ghostfolio Status" <ng-container>&nbsp;</ng-container>
<a
href="https://status.ghostfol.io"
i18n-title
title="Ghostfolio Status"
>status.ghostfol.io</a >status.ghostfol.io</a
>. >.
} }
</p> </p>
<p> <p>
If you encounter a bug or would like to suggest an improvement or a <ng-container i18n>
If you encounter a bug, would like to suggest an improvement or a
new new
<a [routerLink]="routerLinkFeatures">feature</a>, please join the <a [routerLink]="routerLinkFeatures">feature</a>, please join the
Ghostfolio Ghostfolio
@ -60,14 +78,19 @@
href="https://x.com/ghostfolio_" href="https://x.com/ghostfolio_"
title="Post to Ghostfolio on X (formerly Twitter)" title="Post to Ghostfolio on X (formerly Twitter)"
>&#64;ghostfolio_</a >&#64;ghostfolio_</a
></ng-container
> >
@if (user?.subscription?.type === 'Premium') { @if (user?.subscription?.type === 'Premium') {
, send an e-mail to <ng-container>, </ng-container>
<a href="mailto:hi@ghostfol.io" title="Send an e-mail" <ng-container i18n>send an e-mail to</ng-container>
<ng-container>&nbsp;</ng-container>
<a href="mailto:hi@ghostfol.io" i18n-title title="Send an e-mail"
>hi&#64;ghostfol.io</a >hi&#64;ghostfol.io</a
> >
} }
or start a discussion at <ng-container>&nbsp;</ng-container>
<ng-container i18n>or start a discussion at</ng-container>
<ng-container>&nbsp;</ng-container>
<a <a
href="https://github.com/ghostfolio/ghostfolio" href="https://github.com/ghostfolio/ghostfolio"
i18n-title i18n-title

12
apps/client/src/app/pages/accounts/accounts-page.component.ts

@ -1,8 +1,7 @@
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto'; import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
import { TransferBalanceDto } from '@ghostfolio/api/app/account/transfer-balance.dto'; import { TransferBalanceDto } from '@ghostfolio/api/app/account/transfer-balance.dto';
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto'; import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
import { AccountDetailDialog } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.component'; import { GfAccountDetailDialogComponent } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.component';
import { GfAccountDetailDialogModule } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.module';
import { AccountDetailDialogParams } from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces'; import { AccountDetailDialogParams } from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces';
import { NotificationService } from '@ghostfolio/client/core/notification/notification.service'; import { NotificationService } from '@ghostfolio/client/core/notification/notification.service';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
@ -28,12 +27,7 @@ import { GfTransferBalanceDialogComponent } from './transfer-balance/transfer-ba
@Component({ @Component({
host: { class: 'has-fab page' }, host: { class: 'has-fab page' },
imports: [ imports: [GfAccountsTableComponent, MatButtonModule, RouterModule],
GfAccountDetailDialogModule,
GfAccountsTableComponent,
MatButtonModule,
RouterModule
],
selector: 'gf-accounts-page', selector: 'gf-accounts-page',
styleUrls: ['./accounts-page.scss'], styleUrls: ['./accounts-page.scss'],
templateUrl: './accounts-page.html' templateUrl: './accounts-page.html'
@ -233,7 +227,7 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit {
} }
private openAccountDetailDialog(aAccountId: string) { private openAccountDetailDialog(aAccountId: string) {
const dialogRef = this.dialog.open(AccountDetailDialog, { const dialogRef = this.dialog.open(GfAccountDetailDialogComponent, {
autoFocus: false, autoFocus: false,
data: { data: {
accountId: aAccountId, accountId: aAccountId,

4
apps/client/src/app/pages/landing/landing-page.component.ts

@ -1,4 +1,4 @@
import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module'; import { GfWorldMapChartComponent } from '@ghostfolio/client/components/world-map-chart/world-map-chart.component';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { Statistics } from '@ghostfolio/common/interfaces'; import { Statistics } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
@ -30,7 +30,7 @@ import { Subject } from 'rxjs';
GfCarouselComponent, GfCarouselComponent,
GfLogoComponent, GfLogoComponent,
GfValueComponent, GfValueComponent,
GfWorldMapChartModule, GfWorldMapChartComponent,
IonIcon, IonIcon,
MatButtonModule, MatButtonModule,
MatCardModule, MatCardModule,

2
apps/client/src/app/pages/portfolio/activities/activities-page.component.ts

@ -250,6 +250,7 @@ export class GfActivitiesPageComponent implements OnDestroy, OnInit {
deviceType: this.deviceType, deviceType: this.deviceType,
user: this.user user: this.user
} as ImportActivitiesDialogParams, } as ImportActivitiesDialogParams,
height: this.deviceType === 'mobile' ? '98vh' : undefined,
width: this.deviceType === 'mobile' ? '100vw' : '50rem' width: this.deviceType === 'mobile' ? '100vw' : '50rem'
}); });
@ -273,6 +274,7 @@ export class GfActivitiesPageComponent implements OnDestroy, OnInit {
deviceType: this.deviceType, deviceType: this.deviceType,
user: this.user user: this.user
} as ImportActivitiesDialogParams, } as ImportActivitiesDialogParams,
height: this.deviceType === 'mobile' ? '98vh' : undefined,
width: this.deviceType === 'mobile' ? '100vw' : '50rem' width: this.deviceType === 'mobile' ? '100vw' : '50rem'
}); });

1
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts

@ -56,6 +56,7 @@ import { ImportActivitiesDialogParams } from './interfaces/interfaces';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'd-flex flex-column h-100' },
imports: [ imports: [
GfActivitiesTableComponent, GfActivitiesTableComponent,
GfDialogFooterComponent, GfDialogFooterComponent,

2
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html

@ -1,5 +1,4 @@
<gf-dialog-header <gf-dialog-header
mat-dialog-title
[deviceType]="data.deviceType" [deviceType]="data.deviceType"
[title]="dialogTitle" [title]="dialogTitle"
(closeButtonClicked)="onCancel()" (closeButtonClicked)="onCancel()"
@ -193,7 +192,6 @@
</div> </div>
<gf-dialog-footer <gf-dialog-footer
mat-dialog-actions
[deviceType]="data.deviceType" [deviceType]="data.deviceType"
(closeButtonClicked)="onCancel()" (closeButtonClicked)="onCancel()"
/> />

40
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.scss

@ -1,6 +1,9 @@
:host { :host {
display: block; display: block;
.mat-mdc-dialog-content {
max-height: unset;
a { a {
color: rgba(var(--palette-primary-500), 1); color: rgba(var(--palette-primary-500), 1);
} }
@ -15,24 +18,6 @@
} }
} }
.mat-expansion-panel {
background: none;
box-shadow: none;
.mat-expansion-panel-header {
color: inherit;
&[aria-disabled='true'] {
cursor: default;
}
}
}
.mat-mdc-progress-spinner {
right: 1.5rem;
top: calc(50% - 10px);
}
.drop-area { .drop-area {
background-color: rgba(var(--palette-foreground-base), 0.02); background-color: rgba(var(--palette-foreground-base), 0.02);
border: 1px dashed border: 1px dashed
@ -51,6 +36,25 @@
font-size: 2.5rem; font-size: 2.5rem;
} }
} }
.mat-mdc-progress-spinner {
right: 1.5rem;
top: calc(50% - 10px);
}
.mat-expansion-panel {
background: none;
box-shadow: none;
.mat-expansion-panel-header {
color: inherit;
&[aria-disabled='true'] {
cursor: default;
}
}
}
}
} }
:host-context(.theme-dark) { :host-context(.theme-dark) {

8
apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts

@ -1,6 +1,6 @@
import { AccountDetailDialog } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.component'; import { GfAccountDetailDialogComponent } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.component';
import { AccountDetailDialogParams } from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces'; import { AccountDetailDialogParams } from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces';
import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module'; import { GfWorldMapChartComponent } from '@ghostfolio/client/components/world-map-chart/world-map-chart.component';
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 { 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';
@ -45,7 +45,7 @@ import { takeUntil } from 'rxjs/operators';
GfPremiumIndicatorComponent, GfPremiumIndicatorComponent,
GfTopHoldingsComponent, GfTopHoldingsComponent,
GfValueComponent, GfValueComponent,
GfWorldMapChartModule, GfWorldMapChartComponent,
MatCardModule, MatCardModule,
MatProgressBarModule, MatProgressBarModule,
NgClass NgClass
@ -558,7 +558,7 @@ export class GfAllocationsPageComponent implements OnDestroy, OnInit {
} }
private openAccountDetailDialog(aAccountId: string) { private openAccountDetailDialog(aAccountId: string) {
const dialogRef = this.dialog.open(AccountDetailDialog, { const dialogRef = this.dialog.open(GfAccountDetailDialogComponent, {
autoFocus: false, autoFocus: false,
data: { data: {
accountId: aAccountId, accountId: aAccountId,

4
apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts

@ -1,4 +1,4 @@
import { GfBenchmarkComparatorModule } from '@ghostfolio/client/components/benchmark-comparator/benchmark-comparator.module'; import { GfBenchmarkComparatorComponent } from '@ghostfolio/client/components/benchmark-comparator/benchmark-comparator.component';
import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module'; import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module';
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 { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
@ -46,7 +46,7 @@ import { takeUntil } from 'rxjs/operators';
@Component({ @Component({
imports: [ imports: [
GfBenchmarkComparatorModule, GfBenchmarkComparatorComponent,
GfInvestmentChartModule, GfInvestmentChartModule,
GfPremiumIndicatorComponent, GfPremiumIndicatorComponent,
GfToggleComponent, GfToggleComponent,

20
apps/client/src/app/pages/public/public-page-routing.module.ts

@ -1,20 +0,0 @@
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { PublicPageComponent } from './public-page.component';
const routes: Routes = [
{
canActivate: [AuthGuard],
component: PublicPageComponent,
path: ':id'
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class PublicPageRoutingModule {}

29
apps/client/src/app/pages/public/public-page.component.ts

@ -1,3 +1,4 @@
import { GfWorldMapChartComponent } from '@ghostfolio/client/components/world-map-chart/world-map-chart.component';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { UNKNOWN_KEY } from '@ghostfolio/common/config'; import { UNKNOWN_KEY } from '@ghostfolio/common/config';
import { prettifySymbol } from '@ghostfolio/common/helper'; import { prettifySymbol } from '@ghostfolio/common/helper';
@ -6,8 +7,19 @@ import {
PublicPortfolioResponse PublicPortfolioResponse
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { Market } from '@ghostfolio/common/types'; import { Market } from '@ghostfolio/common/types';
import { GfHoldingsTableComponent } from '@ghostfolio/ui/holdings-table/holdings-table.component';
import { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.component';
import { GfValueComponent } from '@ghostfolio/ui/value';
import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common';
import {
ChangeDetectorRef,
Component,
CUSTOM_ELEMENTS_SCHEMA,
OnInit
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { AssetClass } from '@prisma/client'; import { AssetClass } from '@prisma/client';
import { StatusCodes } from 'http-status-codes'; import { StatusCodes } from 'http-status-codes';
@ -18,12 +30,21 @@ import { catchError, takeUntil } from 'rxjs/operators';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
imports: [
CommonModule,
GfHoldingsTableComponent,
GfPortfolioProportionChartComponent,
GfValueComponent,
GfWorldMapChartComponent,
MatButtonModule,
MatCardModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-public-page', selector: 'gf-public-page',
styleUrls: ['./public-page.scss'], styleUrls: ['./public-page.scss'],
templateUrl: './public-page.html', templateUrl: './public-page.html'
standalone: false
}) })
export class PublicPageComponent implements OnInit { export class GfPublicPageComponent implements OnInit {
public continents: { public continents: {
[code: string]: { name: string; value: number }; [code: string]: { name: string; value: number };
}; };

28
apps/client/src/app/pages/public/public-page.module.ts

@ -1,28 +0,0 @@
import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module';
import { GfHoldingsTableComponent } from '@ghostfolio/ui/holdings-table';
import { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart';
import { GfValueComponent } from '@ghostfolio/ui/value';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { PublicPageRoutingModule } from './public-page-routing.module';
import { PublicPageComponent } from './public-page.component';
@NgModule({
declarations: [PublicPageComponent],
imports: [
CommonModule,
GfHoldingsTableComponent,
GfPortfolioProportionChartComponent,
GfValueComponent,
GfWorldMapChartModule,
MatButtonModule,
MatCardModule,
PublicPageRoutingModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class PublicPageModule {}

13
apps/client/src/app/pages/public/public-page.routes.ts

@ -0,0 +1,13 @@
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { Routes } from '@angular/router';
import { GfPublicPageComponent } from './public-page.component';
export const routes: Routes = [
{
canActivate: [AuthGuard],
component: GfPublicPageComponent,
path: ':id'
}
];

916
apps/client/src/locales/messages.ca.xlf

File diff suppressed because it is too large

970
apps/client/src/locales/messages.de.xlf

File diff suppressed because it is too large

922
apps/client/src/locales/messages.es.xlf

File diff suppressed because it is too large

970
apps/client/src/locales/messages.fr.xlf

File diff suppressed because it is too large

966
apps/client/src/locales/messages.it.xlf

File diff suppressed because it is too large

892
apps/client/src/locales/messages.nl.xlf

File diff suppressed because it is too large

932
apps/client/src/locales/messages.pl.xlf

File diff suppressed because it is too large

972
apps/client/src/locales/messages.pt.xlf

File diff suppressed because it is too large

924
apps/client/src/locales/messages.tr.xlf

File diff suppressed because it is too large

966
apps/client/src/locales/messages.uk.xlf

File diff suppressed because it is too large

709
apps/client/src/locales/messages.xlf

File diff suppressed because it is too large

844
apps/client/src/locales/messages.zh.xlf

File diff suppressed because it is too large

2
libs/ui/src/lib/benchmark/benchmark-detail-dialog/benchmark-detail-dialog.html

@ -1,5 +1,4 @@
<gf-dialog-header <gf-dialog-header
mat-dialog-title
position="center" position="center"
[deviceType]="data.deviceType" [deviceType]="data.deviceType"
[title]="assetProfile?.name ?? assetProfile?.symbol" [title]="assetProfile?.name ?? assetProfile?.symbol"
@ -35,7 +34,6 @@
</div> </div>
<gf-dialog-footer <gf-dialog-footer
mat-dialog-actions
[deviceType]="data.deviceType" [deviceType]="data.deviceType"
(closeButtonClicked)="onClose()" (closeButtonClicked)="onClose()"
/> />

55
package-lock.json

@ -1,12 +1,12 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.194.0", "version": "2.196.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.194.0", "version": "2.196.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
@ -47,13 +47,13 @@
"@prisma/client": "6.14.0", "@prisma/client": "6.14.0",
"@simplewebauthn/browser": "13.1.0", "@simplewebauthn/browser": "13.1.0",
"@simplewebauthn/server": "13.1.1", "@simplewebauthn/server": "13.1.1",
"@stripe/stripe-js": "7.6.1", "@stripe/stripe-js": "7.9.0",
"ai": "4.3.16", "ai": "4.3.16",
"alphavantage": "2.2.0", "alphavantage": "2.2.0",
"big.js": "7.0.1", "big.js": "7.0.1",
"bootstrap": "4.6.2", "bootstrap": "4.6.2",
"bull": "4.16.5", "bull": "4.16.5",
"chart.js": "4.4.9", "chart.js": "4.5.0",
"chartjs-adapter-date-fns": "3.0.0", "chartjs-adapter-date-fns": "3.0.0",
"chartjs-chart-treemap": "3.1.0", "chartjs-chart-treemap": "3.1.0",
"chartjs-plugin-annotation": "3.1.0", "chartjs-plugin-annotation": "3.1.0",
@ -77,9 +77,9 @@
"marked": "15.0.4", "marked": "15.0.4",
"ms": "3.0.0-canary.1", "ms": "3.0.0-canary.1",
"ng-extract-i18n-merge": "3.0.0", "ng-extract-i18n-merge": "3.0.0",
"ngx-device-detector": "10.0.2", "ngx-device-detector": "10.1.0",
"ngx-markdown": "20.0.0", "ngx-markdown": "20.0.0",
"ngx-skeleton-loader": "11.2.1", "ngx-skeleton-loader": "11.3.0",
"ngx-stripe": "20.7.0", "ngx-stripe": "20.7.0",
"open-color": "1.9.1", "open-color": "1.9.1",
"papaparse": "5.3.1", "papaparse": "5.3.1",
@ -89,11 +89,11 @@
"passport-jwt": "4.0.1", "passport-jwt": "4.0.1",
"reflect-metadata": "0.2.2", "reflect-metadata": "0.2.2",
"rxjs": "7.8.1", "rxjs": "7.8.1",
"stripe": "18.3.0", "stripe": "18.5.0",
"svgmap": "2.12.2", "svgmap": "2.12.2",
"twitter-api-v2": "1.23.0", "twitter-api-v2": "1.23.0",
"uuid": "11.1.0", "uuid": "11.1.0",
"yahoo-finance2": "3.6.4", "yahoo-finance2": "3.8.0",
"zone.js": "0.15.1" "zone.js": "0.15.1"
}, },
"devDependencies": { "devDependencies": {
@ -11924,9 +11924,9 @@
} }
}, },
"node_modules/@stripe/stripe-js": { "node_modules/@stripe/stripe-js": {
"version": "7.6.1", "version": "7.9.0",
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-7.6.1.tgz", "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-7.9.0.tgz",
"integrity": "sha512-BUDj5gujbtx53/Cexws0+aPrEBsKAN8ExPf9UfuTCivVU6ug2PjqI0zUeL1jon3795eOLlyqvCDjp6VNknjE0A==", "integrity": "sha512-ggs5k+/0FUJcIgNY08aZTqpBTtbExkJMYMLSMwyucrhtWexVOEY1KJmhBsxf+E/Q15f5rbwBpj+t0t2AW2oCsQ==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=12.16" "node": ">=12.16"
@ -16235,9 +16235,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/chart.js": { "node_modules/chart.js": {
"version": "4.4.9", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz",
"integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==", "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@kurkle/color": "^0.3.0" "@kurkle/color": "^0.3.0"
@ -31562,9 +31562,9 @@
} }
}, },
"node_modules/ngx-device-detector": { "node_modules/ngx-device-detector": {
"version": "10.0.2", "version": "10.1.0",
"resolved": "https://registry.npmjs.org/ngx-device-detector/-/ngx-device-detector-10.0.2.tgz", "resolved": "https://registry.npmjs.org/ngx-device-detector/-/ngx-device-detector-10.1.0.tgz",
"integrity": "sha512-KLbd2hJtpUT7lRek+9pRUINvxa6yG4YDZ6RKzYmMbIbNpYEPJwXVmszG2fMPq+DarXABdqOYwp7wUQ2DQFgihw==", "integrity": "sha512-+MrJReetLq9Flp/IoncJYvmnOP8X6vEFK/qR+2PghoqUXwDlyNjh8ujX/nx2SK/5khK2Yr0bj7ICowhXjhgC/w==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"tslib": "^2.6.3" "tslib": "^2.6.3"
@ -31599,9 +31599,9 @@
} }
}, },
"node_modules/ngx-skeleton-loader": { "node_modules/ngx-skeleton-loader": {
"version": "11.2.1", "version": "11.3.0",
"resolved": "https://registry.npmjs.org/ngx-skeleton-loader/-/ngx-skeleton-loader-11.2.1.tgz", "resolved": "https://registry.npmjs.org/ngx-skeleton-loader/-/ngx-skeleton-loader-11.3.0.tgz",
"integrity": "sha512-0YWwQgK3X4trtiLvTv3/CMGxcvjPkUbtTTKJJ2EOHhFuvPf0b+XO1KwguK0Ub9BMHnsqK8xOol0cEoVXyNh64Q==", "integrity": "sha512-MLm5shgXGiCA1W5NEqct6glBFx2AEgEKbk8pDyY15BsZ2zTGUwa5jw4pe6nJdrCj6xcl/d9oFTinQHrO0q+3RA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"tslib": "^2.0.0" "tslib": "^2.0.0"
@ -37591,9 +37591,9 @@
} }
}, },
"node_modules/stripe": { "node_modules/stripe": {
"version": "18.3.0", "version": "18.5.0",
"resolved": "https://registry.npmjs.org/stripe/-/stripe-18.3.0.tgz", "resolved": "https://registry.npmjs.org/stripe/-/stripe-18.5.0.tgz",
"integrity": "sha512-FkxrTUUcWB4CVN2yzgsfF/YHD6WgYHduaa7VmokCy5TLCgl5UNJkwortxcedrxSavQ8Qfa4Ir4JxcbIYiBsyLg==", "integrity": "sha512-Hp+wFiEQtCB0LlNgcFh5uVyKznpDjzyUZ+CNVEf+I3fhlYvh7rZruIg+jOwzJRCpy0ZTPMjlzm7J2/M2N6d+DA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"qs": "^6.11.0" "qs": "^6.11.0"
@ -40823,18 +40823,19 @@
} }
}, },
"node_modules/yahoo-finance2": { "node_modules/yahoo-finance2": {
"version": "3.6.4", "version": "3.8.0",
"resolved": "https://registry.npmjs.org/yahoo-finance2/-/yahoo-finance2-3.6.4.tgz", "resolved": "https://registry.npmjs.org/yahoo-finance2/-/yahoo-finance2-3.8.0.tgz",
"integrity": "sha512-IoMU8Hb4BEaNPVnamZjRBuorTGDbaaiV/tM/m3KI8dzwrR6BGmeuT40OX+5IqRiSkMlD8g0kAwGi9E4bY3rLvg==", "integrity": "sha512-em11JOlfSg23wevm4kXs1+A/CoSWD9eg7/hKRU3zKWuPknCfE4NkIhGVb601Nokid+KPE8Q0eoXK4qgLsMIjKA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@deno/shim-deno": "~0.18.0", "@deno/shim-deno": "~0.18.0",
"fetch-mock-cache": "npm:fetch-mock-cache@^2.1.3", "fetch-mock-cache": "npm:fetch-mock-cache@^2.1.3",
"json-schema": "^0.4.0",
"tough-cookie": "npm:tough-cookie@^5.1.1", "tough-cookie": "npm:tough-cookie@^5.1.1",
"tough-cookie-file-store": "npm:tough-cookie-file-store@^2.0.3" "tough-cookie-file-store": "npm:tough-cookie-file-store@^2.0.3"
}, },
"bin": { "bin": {
"yahoo-finance": "bin/yahoo-finance.mjs" "yahoo-finance": "esm/bin/yahoo-finance.js"
}, },
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=20.0.0"

14
package.json

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.194.0", "version": "2.196.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,13 +93,13 @@
"@prisma/client": "6.14.0", "@prisma/client": "6.14.0",
"@simplewebauthn/browser": "13.1.0", "@simplewebauthn/browser": "13.1.0",
"@simplewebauthn/server": "13.1.1", "@simplewebauthn/server": "13.1.1",
"@stripe/stripe-js": "7.6.1", "@stripe/stripe-js": "7.9.0",
"ai": "4.3.16", "ai": "4.3.16",
"alphavantage": "2.2.0", "alphavantage": "2.2.0",
"big.js": "7.0.1", "big.js": "7.0.1",
"bootstrap": "4.6.2", "bootstrap": "4.6.2",
"bull": "4.16.5", "bull": "4.16.5",
"chart.js": "4.4.9", "chart.js": "4.5.0",
"chartjs-adapter-date-fns": "3.0.0", "chartjs-adapter-date-fns": "3.0.0",
"chartjs-chart-treemap": "3.1.0", "chartjs-chart-treemap": "3.1.0",
"chartjs-plugin-annotation": "3.1.0", "chartjs-plugin-annotation": "3.1.0",
@ -123,9 +123,9 @@
"marked": "15.0.4", "marked": "15.0.4",
"ms": "3.0.0-canary.1", "ms": "3.0.0-canary.1",
"ng-extract-i18n-merge": "3.0.0", "ng-extract-i18n-merge": "3.0.0",
"ngx-device-detector": "10.0.2", "ngx-device-detector": "10.1.0",
"ngx-markdown": "20.0.0", "ngx-markdown": "20.0.0",
"ngx-skeleton-loader": "11.2.1", "ngx-skeleton-loader": "11.3.0",
"ngx-stripe": "20.7.0", "ngx-stripe": "20.7.0",
"open-color": "1.9.1", "open-color": "1.9.1",
"papaparse": "5.3.1", "papaparse": "5.3.1",
@ -135,11 +135,11 @@
"passport-jwt": "4.0.1", "passport-jwt": "4.0.1",
"reflect-metadata": "0.2.2", "reflect-metadata": "0.2.2",
"rxjs": "7.8.1", "rxjs": "7.8.1",
"stripe": "18.3.0", "stripe": "18.5.0",
"svgmap": "2.12.2", "svgmap": "2.12.2",
"twitter-api-v2": "1.23.0", "twitter-api-v2": "1.23.0",
"uuid": "11.1.0", "uuid": "11.1.0",
"yahoo-finance2": "3.6.4", "yahoo-finance2": "3.8.0",
"zone.js": "0.15.1" "zone.js": "0.15.1"
}, },
"devDependencies": { "devDependencies": {

42
test/import/ok/btcusd-short.json

@ -0,0 +1,42 @@
{
"meta": {
"date": "2021-12-12T00:00:00.000Z",
"version": "dev"
},
"accounts": [],
"platforms": [],
"tags": [],
"activities": [
{
"accountId": null,
"comment": null,
"fee": 4.46,
"quantity": 1,
"type": "SELL",
"unitPrice": 44558.42,
"currency": "USD",
"dataSource": "YAHOO",
"date": "2021-12-12T00:00:00.000Z",
"symbol": "BTCUSD",
"tags": []
},
{
"accountId": null,
"comment": null,
"fee": 4.46,
"quantity": 1,
"type": "SELL",
"unitPrice": 46737.48,
"currency": "USD",
"dataSource": "YAHOO",
"date": "2021-12-13T00:00:00.000Z",
"symbol": "BTCUSD",
"tags": []
}
],
"user": {
"settings": {
"currency": "USD"
}
}
}
Loading…
Cancel
Save