Browse Source

Merge branch 'main' into feature/Add-Gather-Missing-data-only

pull/5027/head
Dan 9 months ago
parent
commit
ec4ace58d1
  1. 15
      CHANGELOG.md
  2. 6
      apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts
  3. 48
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts
  4. 4
      apps/api/src/services/configuration/configuration.service.ts
  5. 14
      apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.module.ts
  6. 11
      apps/client/src/app/components/header/header.component.ts
  7. 1
      libs/common/src/lib/config.ts
  8. 14
      libs/ui/src/lib/treemap-chart/treemap-chart.component.ts
  9. 4
      test/import/ok-novn-buy-and-sell.json

15
CHANGELOG.md

@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Added
- Added the name to the tooltip of the chart of the holdings tab on the home page (experimental)
### Changed
- Exposed the timeout of the portfolio snapshot computation as an environment variable (`PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT`)
- Improved the portfolio unit tests to work with exported activity files
### Fixed
- Considered the language of the user settings on login with _Security Token_
## 2.114.0 - 2024-10-10 ## 2.114.0 - 2024-10-10
### Added ### Added

6
apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts

@ -1,3 +1,5 @@
import { readFileSync } from 'fs';
export const activityDummyData = { export const activityDummyData = {
accountId: undefined, accountId: undefined,
accountUserId: undefined, accountUserId: undefined,
@ -29,3 +31,7 @@ export const symbolProfileDummyData = {
export const userDummyData = { export const userDummyData = {
id: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' id: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
}; };
export function loadActivityExportFile(filePath: string) {
return JSON.parse(readFileSync(filePath, 'utf8')).activities;
}

48
apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts

@ -1,6 +1,8 @@
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { import {
activityDummyData, activityDummyData,
loadActivityExportFile,
symbolProfileDummyData, symbolProfileDummyData,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
@ -20,6 +22,7 @@ import { parseDate } from '@ghostfolio/common/helper';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { last } from 'lodash'; import { last } from 'lodash';
import { join } from 'path';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return { return {
@ -52,6 +55,8 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
}); });
describe('PortfolioCalculator', () => { describe('PortfolioCalculator', () => {
let activityDtos: CreateOrderDto[];
let configurationService: ConfigurationService; let configurationService: ConfigurationService;
let currentRateService: CurrentRateService; let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
@ -59,6 +64,15 @@ describe('PortfolioCalculator', () => {
let portfolioSnapshotService: PortfolioSnapshotService; let portfolioSnapshotService: PortfolioSnapshotService;
let redisCacheService: RedisCacheService; let redisCacheService: RedisCacheService;
beforeAll(() => {
activityDtos = loadActivityExportFile(
join(
__dirname,
'../../../../../../../test/import/ok-novn-buy-and-sell.json'
)
);
});
beforeEach(() => { beforeEach(() => {
configurationService = new ConfigurationService(); configurationService = new ConfigurationService();
@ -89,38 +103,18 @@ describe('PortfolioCalculator', () => {
it.only('with NOVN.SW buy and sell', async () => { it.only('with NOVN.SW buy and sell', async () => {
jest.useFakeTimers().setSystemTime(parseDate('2022-04-11').getTime()); jest.useFakeTimers().setSystemTime(parseDate('2022-04-11').getTime());
const activities: Activity[] = [ const activities: Activity[] = activityDtos.map((activity) => ({
{
...activityDummyData, ...activityDummyData,
date: new Date('2022-03-07'), ...activity,
fee: 0, date: parseDate(activity.date),
quantity: 2,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,
currency: 'CHF', currency: activity.currency,
dataSource: 'YAHOO', dataSource: activity.dataSource,
name: 'Novartis AG', name: 'Novartis AG',
symbol: 'NOVN.SW' symbol: activity.symbol
},
type: 'BUY',
unitPrice: 75.8
},
{
...activityDummyData,
date: new Date('2022-04-08'),
fee: 0,
quantity: 2,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'CHF',
dataSource: 'YAHOO',
name: 'Novartis AG',
symbol: 'NOVN.SW'
},
type: 'SELL',
unitPrice: 85.73
} }
]; }));
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities, activities,

4
apps/api/src/services/configuration/configuration.service.ts

@ -4,6 +4,7 @@ import {
DEFAULT_PROCESSOR_CONCURRENCY_GATHER_ASSET_PROFILE, DEFAULT_PROCESSOR_CONCURRENCY_GATHER_ASSET_PROFILE,
DEFAULT_PROCESSOR_CONCURRENCY_GATHER_HISTORICAL_MARKET_DATA, DEFAULT_PROCESSOR_CONCURRENCY_GATHER_HISTORICAL_MARKET_DATA,
DEFAULT_PROCESSOR_CONCURRENCY_PORTFOLIO_SNAPSHOT, DEFAULT_PROCESSOR_CONCURRENCY_PORTFOLIO_SNAPSHOT,
DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT,
DEFAULT_ROOT_URL DEFAULT_ROOT_URL
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
@ -59,6 +60,9 @@ export class ConfigurationService {
PROCESSOR_CONCURRENCY_PORTFOLIO_SNAPSHOT: num({ PROCESSOR_CONCURRENCY_PORTFOLIO_SNAPSHOT: num({
default: DEFAULT_PROCESSOR_CONCURRENCY_PORTFOLIO_SNAPSHOT default: DEFAULT_PROCESSOR_CONCURRENCY_PORTFOLIO_SNAPSHOT
}), }),
PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT: num({
default: DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT
}),
REDIS_DB: num({ default: 0 }), REDIS_DB: num({ default: 0 }),
REDIS_HOST: str({ default: 'localhost' }), REDIS_HOST: str({ default: 'localhost' }),
REDIS_PASSWORD: str({ default: '' }), REDIS_PASSWORD: str({ default: '' }),

14
apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.module.ts

@ -8,7 +8,10 @@ import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module'; import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { PORTFOLIO_SNAPSHOT_QUEUE } from '@ghostfolio/common/config'; import {
DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT,
PORTFOLIO_SNAPSHOT_QUEUE
} from '@ghostfolio/common/config';
import { BullModule } from '@nestjs/bull'; import { BullModule } from '@nestjs/bull';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
@ -20,7 +23,14 @@ import { PortfolioSnapshotProcessor } from './portfolio-snapshot.processor';
imports: [ imports: [
AccountBalanceModule, AccountBalanceModule,
BullModule.registerQueue({ BullModule.registerQueue({
name: PORTFOLIO_SNAPSHOT_QUEUE name: PORTFOLIO_SNAPSHOT_QUEUE,
settings: {
lockDuration: parseInt(
process.env.PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT ??
DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT.toString(),
10
)
}
}), }),
ConfigurationModule, ConfigurationModule,
DataProviderModule, DataProviderModule,

11
apps/client/src/app/components/header/header.component.ts

@ -267,8 +267,19 @@ export class HeaderComponent implements OnChanges {
this.settingsStorageService.getSetting(KEY_STAY_SIGNED_IN) === 'true' this.settingsStorageService.getSetting(KEY_STAY_SIGNED_IN) === 'true'
); );
this.userService
.get()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((user) => {
const userLanguage = user?.settings?.language;
if (userLanguage && document.documentElement.lang !== userLanguage) {
window.location.href = `../${userLanguage}`;
} else {
this.router.navigate(['/']); this.router.navigate(['/']);
} }
});
}
public ngOnDestroy() { public ngOnDestroy() {
this.unsubscribeSubject.next(); this.unsubscribeSubject.next();

1
libs/common/src/lib/config.ts

@ -51,6 +51,7 @@ export const DEFAULT_PAGE_SIZE = 50;
export const DEFAULT_PROCESSOR_CONCURRENCY_GATHER_ASSET_PROFILE = 1; export const DEFAULT_PROCESSOR_CONCURRENCY_GATHER_ASSET_PROFILE = 1;
export const DEFAULT_PROCESSOR_CONCURRENCY_GATHER_HISTORICAL_MARKET_DATA = 1; export const DEFAULT_PROCESSOR_CONCURRENCY_GATHER_HISTORICAL_MARKET_DATA = 1;
export const DEFAULT_PROCESSOR_CONCURRENCY_PORTFOLIO_SNAPSHOT = 1; export const DEFAULT_PROCESSOR_CONCURRENCY_PORTFOLIO_SNAPSHOT = 1;
export const DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT = 30000;
export const DEFAULT_ROOT_URL = 'https://localhost:4200'; export const DEFAULT_ROOT_URL = 'https://localhost:4200';
// USX is handled separately // USX is handled separately

14
libs/ui/src/lib/treemap-chart/treemap-chart.component.ts

@ -227,16 +227,24 @@ export class GfTreemapChartComponent
}), }),
callbacks: { callbacks: {
label: (context) => { label: (context) => {
const name = context.raw._data.name;
const symbol = context.raw._data.symbol;
if (context.raw._data.valueInBaseCurrency !== null) { if (context.raw._data.valueInBaseCurrency !== null) {
const value = <number>context.raw._data.valueInBaseCurrency; const value = <number>context.raw._data.valueInBaseCurrency;
return `${value.toLocaleString(this.locale, {
return [
`${name ?? symbol}`,
`${value.toLocaleString(this.locale, {
maximumFractionDigits: 2, maximumFractionDigits: 2,
minimumFractionDigits: 2 minimumFractionDigits: 2
})} ${this.baseCurrency}`; })} ${this.baseCurrency}`
];
} else { } else {
const percentage = const percentage =
<number>context.raw._data.allocationInPercentage * 100; <number>context.raw._data.allocationInPercentage * 100;
return `${percentage.toFixed(2)}%`;
return [`${name ?? symbol}`, `${percentage.toFixed(2)}%`];
} }
}, },
title: () => { title: () => {

4
test/import/ok-novn-buy-and-sell.json

@ -11,7 +11,7 @@
"unitPrice": 85.73, "unitPrice": 85.73,
"currency": "CHF", "currency": "CHF",
"dataSource": "YAHOO", "dataSource": "YAHOO",
"date": "2022-04-07T22:00:00.000Z", "date": "2022-04-08T00:00:00.000Z",
"symbol": "NOVN.SW" "symbol": "NOVN.SW"
}, },
{ {
@ -21,7 +21,7 @@
"unitPrice": 75.8, "unitPrice": 75.8,
"currency": "CHF", "currency": "CHF",
"dataSource": "YAHOO", "dataSource": "YAHOO",
"date": "2022-03-06T23:00:00.000Z", "date": "2022-03-07T00:00:00.000Z",
"symbol": "NOVN.SW" "symbol": "NOVN.SW"
} }
] ]

Loading…
Cancel
Save