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. 30
      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. 8
      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. 22
      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. 111
      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. 76
      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
### 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
- Reused the request timeout in various functions of the data providers
- 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
apps/api/src/app/admin/queue/queue.service.ts

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

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

@ -927,13 +927,25 @@ export abstract class PortfolioCalculator {
.plus(oldAccumulatedSymbol.quantity);
if (type === 'BUY') {
investment = oldAccumulatedSymbol.investment.plus(
quantity.mul(unitPrice)
);
if (oldAccumulatedSymbol.investment.gte(0)) {
investment = oldAccumulatedSymbol.investment.plus(
quantity.mul(unitPrice)
);
} else {
investment = oldAccumulatedSymbol.investment.plus(
quantity.mul(oldAccumulatedSymbol.averagePrice)
);
}
} else if (type === 'SELL') {
investment = oldAccumulatedSymbol.investment.minus(
quantity.mul(oldAccumulatedSymbol.averagePrice)
);
if (oldAccumulatedSymbol.investment.gt(0)) {
investment = oldAccumulatedSymbol.investment.minus(
quantity.mul(oldAccumulatedSymbol.averagePrice)
);
} else {
investment = oldAccumulatedSymbol.investment.minus(
quantity.mul(unitPrice)
);
}
}
currentTransactionPointItem = {
@ -942,9 +954,9 @@ export abstract class PortfolioCalculator {
investment,
skipErrors,
symbol,
averagePrice: newQuantity.gt(0)
? investment.div(newQuantity)
: new Big(0),
averagePrice: newQuantity.eq(0)
? new Big(0)
: investment.div(newQuantity).abs(),
dividend: new Big(0),
fee: oldAccumulatedSymbol.fee.plus(fee),
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.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';
}
public async search({ query }: GetSearchParams): Promise<LookupResponse> {
public async search({
query,
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT')
}: GetSearchParams): Promise<LookupResponse> {
let items: LookupItem[] = [];
try {
const { coins } = await fetch(`${this.apiUrl}/search?query=${query}`, {
headers: this.headers,
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
signal: AbortSignal.timeout(requestTimeout)
}).then((res) => res.json());
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({
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
symbol
}: GetAssetProfileParams): Promise<Partial<SymbolProfile>> {
const [searchResult] = await this.getSearchResult(symbol);
const [searchResult] = await this.getSearchResult({
requestTimeout,
query: symbol
});
if (!searchResult) {
return undefined;
@ -304,8 +308,11 @@ export class EodHistoricalDataService implements DataProviderInterface {
return 'AAPL.US';
}
public async search({ query }: GetSearchParams): Promise<LookupResponse> {
const searchResult = await this.getSearchResult(query);
public async search({
query,
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT')
}: GetSearchParams): Promise<LookupResponse> {
const searchResult = await this.getSearchResult({ query, requestTimeout });
return {
items: searchResult
@ -394,7 +401,13 @@ export class EodHistoricalDataService implements DataProviderInterface {
return name;
}
private async getSearchResult(aQuery: string) {
private async getSearchResult({
query,
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT')
}: {
query: string;
requestTimeout?: number;
}) {
let searchResult: (LookupItem & {
assetClass: AssetClass;
assetSubClass: AssetSubClass;
@ -403,11 +416,9 @@ export class EodHistoricalDataService implements DataProviderInterface {
try {
const response = await fetch(
`${this.URL}/search/${aQuery}?api_token=${this.apiKey}`,
`${this.URL}/search/${query}?api_token=${this.apiKey}`,
{
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) => res.json());
@ -433,7 +444,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
let message = error;
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
).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(
quotes.map(({ symbol }) => {
return this.getAssetProfile({ symbol }).then(({ currency }) => {
currencyBySymbolMap[symbol] = { currency };
return this.getAssetProfile({
requestTimeout,
symbol
}).then((assetProfile) => {
if (assetProfile?.currency) {
currencyBySymbolMap[symbol] = { currency: assetProfile.currency };
}
});
})
);
@ -411,7 +416,10 @@ export class FinancialModelingPrepService implements DataProviderInterface {
return 'AAPL';
}
public async search({ query }: GetSearchParams): Promise<LookupResponse> {
public async search({
query,
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT')
}: GetSearchParams): Promise<LookupResponse> {
const assetProfileBySymbolMap: {
[symbol: string]: Partial<SymbolProfile>;
} = {};
@ -422,9 +430,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
const result = await fetch(
`${this.getUrl({ version: 'stable' })}/search-isin?isin=${query.toUpperCase()}&apikey=${this.apiKey}`,
{
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
signal: AbortSignal.timeout(requestTimeout)
}
).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({
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
query
query,
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT')
}: GetSearchParams): Promise<LookupResponse> {
let searchResult: LookupResponse = { items: [] };

3
apps/client/project.json

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

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

@ -111,9 +111,7 @@ const routes: Routes = [
{
path: publicRoutes.public.path,
loadChildren: () =>
import('./pages/public/public-page.module').then(
(m) => m.PublicPageModule
)
import('./pages/public/public-page.routes').then((m) => m.routes)
},
{
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 { 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 { UserService } from '@ghostfolio/client/services/user/user.service';
import { NUMERICAL_PRECISION_THRESHOLD_6_FIGURES } from '@ghostfolio/common/config';
@ -13,7 +16,12 @@ import {
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { internalRoutes } from '@ghostfolio/common/routes/routes';
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 {
ChangeDetectionStrategy,
ChangeDetectorRef,
@ -22,10 +30,15 @@ import {
OnDestroy,
OnInit
} 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 { MatDialogModule } from '@angular/material/dialog';
import { Sort, SortDirection } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
import { Router } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone';
import { Big } from 'big.js';
import { format, parseISO } from 'date-fns';
import { addIcons } from 'ionicons';
@ -35,20 +48,36 @@ import {
walletOutline
} from 'ionicons/icons';
import { isNumber } from 'lodash';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { forkJoin, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { AccountDetailDialogParams } from './interfaces/interfaces';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
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',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: 'account-detail-dialog.html',
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 activities: OrderWithAccount[];
public balance: number;
@ -81,7 +110,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
private changeDetectorRef: ChangeDetectorRef,
@Inject(MAT_DIALOG_DATA) public data: AccountDetailDialogParams,
private dataService: DataService,
public dialogRef: MatDialogRef<AccountDetailDialog>,
public dialogRef: MatDialogRef<GfAccountDetailDialogComponent>,
private router: Router,
private userService: UserService
) {

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

@ -1,5 +1,4 @@
<gf-dialog-header
mat-dialog-title
position="center"
[deviceType]="data.deviceType"
[title]="name"
@ -166,7 +165,6 @@
</div>
<gf-dialog-footer
mat-dialog-actions
[deviceType]="data.deviceType"
(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 { internalRoutes } from '@ghostfolio/common/routes/routes';
import { ColorScheme } from '@ghostfolio/common/types';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
@ -26,6 +28,10 @@ import {
Output,
ViewChild
} 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 {
Chart,
@ -42,15 +48,25 @@ import 'chartjs-adapter-date-fns';
import annotationPlugin from 'chartjs-plugin-annotation';
import { addIcons } from 'ionicons';
import { arrowForwardOutline } from 'ionicons/icons';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@Component({
selector: 'gf-benchmark-comparator',
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'],
standalone: false
templateUrl: './benchmark-comparator.component.html'
})
export class BenchmarkComparatorComponent implements OnChanges, OnDestroy {
export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
@Input() benchmark: Partial<SymbolProfile>;
@Input() benchmarkDataItems: LineChartItem[] = [];
@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,
imports: [CommonModule, NgxSkeletonLoaderModule],
selector: 'gf-data-provider-status',
standalone: true,
templateUrl: './data-provider-status.component.html'
})
export class GfDataProviderStatusComponent implements OnDestroy, OnInit {
@ -34,12 +33,12 @@ export class GfDataProviderStatusComponent implements OnDestroy, OnInit {
this.status$ = this.dataService
.fetchDataProviderHealth(this.dataSource)
.pipe(
catchError(() => {
return of({ isHealthy: false });
}),
map(() => {
return { isHealthy: true };
}),
catchError(() => {
return of({ isHealthy: false });
}),
takeUntil(this.unsubscribeSubject)
);
}

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

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

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

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

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

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

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

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

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

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

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

@ -7,6 +7,7 @@ import {
Output
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog';
import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons';
import { close } from 'ionicons/icons';
@ -14,7 +15,7 @@ import { close } from 'ionicons/icons';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'justify-content-center' },
imports: [CommonModule, IonIcon, MatButtonModule],
imports: [CommonModule, IonIcon, MatButtonModule, MatDialogModule],
selector: 'gf-dialog-header',
styleUrls: ['./dialog-header.component.scss'],
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
mat-dialog-title
position="center"
[deviceType]="data.deviceType"
[title]="SymbolProfile?.name ?? SymbolProfile?.symbol"
@ -459,7 +458,6 @@
</div>
<gf-dialog-footer
mat-dialog-actions
[deviceType]="data.deviceType"
(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 { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
@ -22,7 +22,7 @@ import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
imports: [GfPortfolioSummaryModule, MatCardModule],
imports: [GfPortfolioSummaryComponent, MatCardModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-home-summary',
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
mat-dialog-title
[title]="data.title"
(closeButtonClicked)="onClose()"
/>
<gf-dialog-header [title]="data.title" (closeButtonClicked)="onClose()" />
<div class="py-3" mat-dialog-content>
<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 { PortfolioSummary, User } from '@ghostfolio/common/interfaces';
import { translate } from '@ghostfolio/ui/i18n';
import { GfValueComponent } from '@ghostfolio/ui/value';
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
@ -11,6 +13,8 @@ import {
OnChanges,
Output
} from '@angular/core';
import { MatTooltipModule } from '@angular/material/tooltip';
import { IonIcon } from '@ionic/angular/standalone';
import { formatDistanceToNow } from 'date-fns';
import { addIcons } from 'ionicons';
import {
@ -19,13 +23,13 @@ import {
} from 'ionicons/icons';
@Component({
selector: 'gf-portfolio-summary',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './portfolio-summary.component.html',
imports: [CommonModule, GfValueComponent, IonIcon, MatTooltipModule],
selector: 'gf-portfolio-summary',
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() hasPermissionToUpdateUserSettings: 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,
OnDestroy
} from '@angular/core';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import svgMap from 'svgmap';
@Component({
selector: 'gf-world-map-chart',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './world-map-chart.component.html',
imports: [NgxSkeletonLoaderModule],
selector: 'gf-world-map-chart',
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() format: string;
@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 {}

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

@ -6,68 +6,91 @@
</h1>
<div class="about-container">
<p>
Ghostfolio is a lightweight wealth management application for
individuals to keep track of stocks, ETFs or cryptocurrencies and make
solid, data-driven investment decisions. The source code is fully
available as
<a
href="https://github.com/ghostfolio/ghostfolio"
title="Find Ghostfolio on GitHub"
>open source software</a
<ng-container i18n
>Ghostfolio is a lightweight wealth management application for
individuals to keep track of stocks, ETFs or cryptocurrencies and
make solid, data-driven investment decisions.</ng-container
>
(OSS) under the
<a
href="https://www.gnu.org/licenses/agpl-3.0.html"
title="GNU Affero General Public License"
>AGPL-3.0 license</a
<ng-container i18n>
The source code is fully available as
<a
href="https://github.com/ghostfolio/ghostfolio"
title="Find Ghostfolio on GitHub"
>open source software</a
>
(OSS) under the
<a
href="https://www.gnu.org/licenses/agpl-3.0.html"
title="GNU Affero General Public License"
>AGPL-3.0 license</a
></ng-container
>
@if (hasPermissionForStatistics) {
and we share aggregated
<a title="Open Startup" [routerLink]="routerLinkOpenStartup"
>key metrics</a
<ng-container i18n>
and we share aggregated
<a title="Open Startup" [routerLink]="routerLinkOpenStartup"
>key metrics</a
>
of the platform’s performance</ng-container
>
of the platform’s performance
}
. The project has been initiated by
<a href="https://dotsilver.ch" title="Website of Thomas Kaul"
>Thomas Kaul</a
>
and is driven by the efforts of its
<ng-container>. </ng-container>
<ng-container i18n>The project has been initiated by</ng-container>
<a
href="https://github.com/ghostfolio/ghostfolio/graphs/contributors"
title="Contributors to Ghostfolio"
>contributors</a
href="https://dotsilver.ch"
i18n-title
title="Website of Thomas Kaul"
>
Thomas Kaul
</a>
<ng-container i18n
>and is driven by the efforts of its
<a
href="https://github.com/ghostfolio/ghostfolio/graphs/contributors"
title="Contributors to Ghostfolio"
>contributors</a
></ng-container
>.
@if (hasPermissionForSubscription) {
Check the system status at
<a href="https://status.ghostfol.io" title="Ghostfolio Status"
<ng-container i18n>Check the system status at</ng-container>
<ng-container>&nbsp;</ng-container>
<a
href="https://status.ghostfol.io"
i18n-title
title="Ghostfolio Status"
>status.ghostfol.io</a
>.
}
</p>
<p>
If you encounter a bug or would like to suggest an improvement or a
new
<a [routerLink]="routerLinkFeatures">feature</a>, please join the
Ghostfolio
<a
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
title="Join the Ghostfolio Slack community"
>Slack</a
>
community, post to
<a
href="https://x.com/ghostfolio_"
title="Post to Ghostfolio on X (formerly Twitter)"
>&#64;ghostfolio_</a
<ng-container i18n>
If you encounter a bug, would like to suggest an improvement or a
new
<a [routerLink]="routerLinkFeatures">feature</a>, please join the
Ghostfolio
<a
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
title="Join the Ghostfolio Slack community"
>Slack</a
>
community, post to
<a
href="https://x.com/ghostfolio_"
title="Post to Ghostfolio on X (formerly Twitter)"
>&#64;ghostfolio_</a
></ng-container
>
@if (user?.subscription?.type === 'Premium') {
, send an e-mail to
<a href="mailto:hi@ghostfol.io" title="Send an e-mail"
<ng-container>, </ng-container>
<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
>
}
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
href="https://github.com/ghostfolio/ghostfolio"
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 { TransferBalanceDto } from '@ghostfolio/api/app/account/transfer-balance.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 { GfAccountDetailDialogModule } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.module';
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 { NotificationService } from '@ghostfolio/client/core/notification/notification.service';
import { DataService } from '@ghostfolio/client/services/data.service';
@ -28,12 +27,7 @@ import { GfTransferBalanceDialogComponent } from './transfer-balance/transfer-ba
@Component({
host: { class: 'has-fab page' },
imports: [
GfAccountDetailDialogModule,
GfAccountsTableComponent,
MatButtonModule,
RouterModule
],
imports: [GfAccountsTableComponent, MatButtonModule, RouterModule],
selector: 'gf-accounts-page',
styleUrls: ['./accounts-page.scss'],
templateUrl: './accounts-page.html'
@ -233,7 +227,7 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit {
}
private openAccountDetailDialog(aAccountId: string) {
const dialogRef = this.dialog.open(AccountDetailDialog, {
const dialogRef = this.dialog.open(GfAccountDetailDialogComponent, {
autoFocus: false,
data: {
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 { Statistics } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
@ -30,7 +30,7 @@ import { Subject } from 'rxjs';
GfCarouselComponent,
GfLogoComponent,
GfValueComponent,
GfWorldMapChartModule,
GfWorldMapChartComponent,
IonIcon,
MatButtonModule,
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,
user: this.user
} as ImportActivitiesDialogParams,
height: this.deviceType === 'mobile' ? '98vh' : undefined,
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
});
@ -273,6 +274,7 @@ export class GfActivitiesPageComponent implements OnDestroy, OnInit {
deviceType: this.deviceType,
user: this.user
} as ImportActivitiesDialogParams,
height: this.deviceType === 'mobile' ? '98vh' : undefined,
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({
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'd-flex flex-column h-100' },
imports: [
GfActivitiesTableComponent,
GfDialogFooterComponent,

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

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

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

@ -1,54 +1,58 @@
:host {
display: block;
a {
color: rgba(var(--palette-primary-500), 1);
}
.mat-mdc-dialog-content {
max-height: unset;
a {
color: rgba(var(--palette-primary-500), 1);
}
mat-stepper {
::ng-deep {
.mat-step-header {
&[aria-selected='false'] {
pointer-events: none;
mat-stepper {
::ng-deep {
.mat-step-header {
&[aria-selected='false'] {
pointer-events: none;
}
}
}
}
}
.mat-expansion-panel {
background: none;
box-shadow: none;
.drop-area {
background-color: rgba(var(--palette-foreground-base), 0.02);
border: 1px dashed
rgba(
var(--palette-foreground-divider),
var(--palette-foreground-divider-alpha)
);
border-radius: 0.25rem;
.mat-expansion-panel-header {
color: inherit;
&:hover {
border-color: rgba(var(--palette-primary-500), 1) !important;
color: rgba(var(--palette-primary-500), 1);
}
&[aria-disabled='true'] {
cursor: default;
.cloud-icon {
font-size: 2.5rem;
}
}
}
.mat-mdc-progress-spinner {
right: 1.5rem;
top: calc(50% - 10px);
}
.drop-area {
background-color: rgba(var(--palette-foreground-base), 0.02);
border: 1px dashed
rgba(
var(--palette-foreground-divider),
var(--palette-foreground-divider-alpha)
);
border-radius: 0.25rem;
&:hover {
border-color: rgba(var(--palette-primary-500), 1) !important;
color: rgba(var(--palette-primary-500), 1);
.mat-mdc-progress-spinner {
right: 1.5rem;
top: calc(50% - 10px);
}
.cloud-icon {
font-size: 2.5rem;
.mat-expansion-panel {
background: none;
box-shadow: none;
.mat-expansion-panel-header {
color: inherit;
&[aria-disabled='true'] {
cursor: default;
}
}
}
}
}

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 { 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 { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
@ -45,7 +45,7 @@ import { takeUntil } from 'rxjs/operators';
GfPremiumIndicatorComponent,
GfTopHoldingsComponent,
GfValueComponent,
GfWorldMapChartModule,
GfWorldMapChartComponent,
MatCardModule,
MatProgressBarModule,
NgClass
@ -558,7 +558,7 @@ export class GfAllocationsPageComponent implements OnDestroy, OnInit {
}
private openAccountDetailDialog(aAccountId: string) {
const dialogRef = this.dialog.open(AccountDetailDialog, {
const dialogRef = this.dialog.open(GfAccountDetailDialogComponent, {
autoFocus: false,
data: {
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 { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
@ -46,7 +46,7 @@ import { takeUntil } from 'rxjs/operators';
@Component({
imports: [
GfBenchmarkComparatorModule,
GfBenchmarkComparatorComponent,
GfInvestmentChartModule,
GfPremiumIndicatorComponent,
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 { UNKNOWN_KEY } from '@ghostfolio/common/config';
import { prettifySymbol } from '@ghostfolio/common/helper';
@ -6,8 +7,19 @@ import {
PublicPortfolioResponse
} from '@ghostfolio/common/interfaces';
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 { AssetClass } from '@prisma/client';
import { StatusCodes } from 'http-status-codes';
@ -18,12 +30,21 @@ import { catchError, takeUntil } from 'rxjs/operators';
@Component({
host: { class: 'page' },
imports: [
CommonModule,
GfHoldingsTableComponent,
GfPortfolioProportionChartComponent,
GfValueComponent,
GfWorldMapChartComponent,
MatButtonModule,
MatCardModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-public-page',
styleUrls: ['./public-page.scss'],
templateUrl: './public-page.html',
standalone: false
templateUrl: './public-page.html'
})
export class PublicPageComponent implements OnInit {
export class GfPublicPageComponent implements OnInit {
public continents: {
[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
mat-dialog-title
position="center"
[deviceType]="data.deviceType"
[title]="assetProfile?.name ?? assetProfile?.symbol"
@ -35,7 +34,6 @@
</div>
<gf-dialog-footer
mat-dialog-actions
[deviceType]="data.deviceType"
(closeButtonClicked)="onClose()"
/>

55
package-lock.json

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

14
package.json

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