Browse Source

Merge remote-tracking branch 'origin/main' into feature/enable-strict-null-checks-in-ui

pull/6264/head
KenTandrian 2 weeks ago
parent
commit
7ea822c37d
  1. 9
      CHANGELOG.md
  2. 7
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  3. 190
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-jnug-buy-and-sell-and-buy-and-sell.spec.ts
  4. 7
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts
  5. 11
      apps/api/src/app/portfolio/current-rate.service.mock.ts
  6. 4
      libs/common/src/lib/calculation-helper.ts
  7. 12
      package-lock.json
  8. 4
      package.json
  9. 58
      test/import/ok/jnug-buy-and-sell-and-buy-and-sell.json

9
CHANGELOG.md

@ -5,12 +5,19 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
## 2.238.0 - 2026-02-12
### Changed
- Upgraded `ngx-skeleton-loader` from version `11.3.0` to `12.0.0`
- Upgraded `twitter-api-v2` from version `1.27.0` to `1.29.0`
### Fixed
- Fixed a performance calculation issue by resetting tracking variables when a holding is fully closed
- Fixed an issue in the annualized performance calculation
- Fixed an issue with the exchange rate calculation by expanding the date range to cover the full day (start to end of day)
## 2.237.0 - 2026-02-08
### Changed

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

@ -53,6 +53,7 @@ import {
isBefore,
isWithinInterval,
min,
startOfDay,
startOfYear,
subDays
} from 'date-fns';
@ -162,8 +163,8 @@ export abstract class PortfolioCalculator {
subDays(dateOfFirstActivity, 1)
);
this.endDate = endDate;
this.startDate = startDate;
this.endDate = endOfDay(endDate);
this.startDate = startOfDay(startDate);
this.computeTransactionPoints();
@ -236,7 +237,7 @@ export abstract class PortfolioCalculator {
const exchangeRatesByCurrency =
await this.exchangeRateDataService.getExchangeRatesByCurrency({
currencies: Array.from(new Set(Object.values(currencies))),
endDate: endOfDay(this.endDate),
endDate: this.endDate,
startDate: this.startDate,
targetCurrency: this.currency
});

190
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-jnug-buy-and-sell-and-buy-and-sell.spec.ts

@ -0,0 +1,190 @@
import {
activityDummyData,
loadExportFile,
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 { Activity, ExportResponse } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Big } from 'big.js';
import { join } from 'node:path';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return {
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
};
});
jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => {
return {
PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock;
})
};
}
);
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
};
});
describe('PortfolioCalculator', () => {
let exportResponse: ExportResponse;
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService;
let portfolioCalculatorFactory: PortfolioCalculatorFactory;
let portfolioSnapshotService: PortfolioSnapshotService;
let redisCacheService: RedisCacheService;
beforeAll(() => {
exportResponse = loadExportFile(
join(
__dirname,
'../../../../../../../test/import/ok/jnug-buy-and-sell-and-buy-and-sell.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 JNUG buy and sell', async () => {
jest.useFakeTimers().setSystemTime(parseDate('2025-12-28').getTime());
const activities: Activity[] = exportResponse.activities.map(
(activity) => ({
...activityDummyData,
...activity,
date: parseDate(activity.date),
feeInAssetProfileCurrency: activity.fee,
feeInBaseCurrency: activity.fee,
SymbolProfile: {
...symbolProfileDummyData,
currency: activity.currency,
dataSource: activity.dataSource,
name: 'Direxion Daily Junior Gold Miners Index Bull 2X Shares',
symbol: activity.symbol
},
unitPriceInAssetProfileCurrency: activity.unitPrice
})
);
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities,
calculationType: PerformanceCalculationType.ROAI,
currency: exportResponse.user.settings.currency,
userId: userDummyData.id
});
const portfolioSnapshot = await portfolioCalculator.computeSnapshot();
const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData,
groupBy: 'month'
});
const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData,
groupBy: 'year'
});
expect(portfolioSnapshot).toMatchObject({
currentValueInBaseCurrency: new Big('0'),
errors: [],
hasErrors: false,
positions: [
{
activitiesCount: 4,
averagePrice: new Big('0'),
currency: 'USD',
dataSource: 'YAHOO',
dateOfFirstActivity: '2025-12-11',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('4'),
feeInBaseCurrency: new Big('4'),
grossPerformance: new Big('43.95'), // (1890.00 - 1885.05) + (2080.10 - 2041.10)
grossPerformanceWithCurrencyEffect: new Big('43.95'), // (1890.00 - 1885.05) + (2080.10 - 2041.10)
investment: new Big('0'),
investmentWithCurrencyEffect: new Big('0'),
netPerformance: new Big('39.95'), // (1890.00 - 1885.05) + (2080.10 - 2041.10) - 4
netPerformanceWithCurrencyEffectMap: {
max: new Big('39.95') // (1890.00 - 1885.05) + (2080.10 - 2041.10) - 4
},
marketPrice: 237.8000030517578,
marketPriceInBaseCurrency: 237.8000030517578,
quantity: new Big('0'),
symbol: 'JNUG',
tags: [],
valueInBaseCurrency: new Big('0')
}
],
totalFeesWithCurrencyEffect: new Big('4'),
totalInterestWithCurrencyEffect: new Big('0'),
totalInvestment: new Big('0'),
totalInvestmentWithCurrencyEffect: new Big('0'),
totalLiabilitiesWithCurrencyEffect: new Big('0')
});
expect(investments).toEqual([
{ date: '2025-12-11', investment: new Big('1885.05') },
{ date: '2025-12-18', investment: new Big('2041.1') },
{ date: '2025-12-28', investment: new Big('0') }
]);
expect(investmentsByMonth).toEqual([
{ date: '2025-12-01', investment: 0 }
]);
expect(investmentsByYear).toEqual([
{ date: '2025-01-01', investment: 0 }
]);
});
});
});

7
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts

@ -626,6 +626,13 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
totalQuantityFromBuyTransactions
);
if (totalUnits.eq(0)) {
// Reset tracking variables when position is fully closed
totalInvestmentFromBuyTransactions = new Big(0);
totalInvestmentFromBuyTransactionsWithCurrencyEffect = new Big(0);
totalQuantityFromBuyTransactions = new Big(0);
}
if (PortfolioCalculator.ENABLE_LOGGING) {
console.log(
'grossPerformanceFromSells',

11
apps/api/src/app/portfolio/current-rate.service.mock.ts

@ -64,6 +64,17 @@ function mockGetValue(symbol: string, date: Date) {
return { marketPrice: 0 };
case 'JNUG':
if (isSameDay(parseDate('2025-12-10'), date)) {
return { marketPrice: 204.5599975585938 };
} else if (isSameDay(parseDate('2025-12-17'), date)) {
return { marketPrice: 203.9700012207031 };
} else if (isSameDay(parseDate('2025-12-28'), date)) {
return { marketPrice: 237.8000030517578 };
}
return { marketPrice: 0 };
case 'MSFT':
if (isSameDay(parseDate('2021-09-16'), date)) {
return { marketPrice: 89.12 };

4
libs/common/src/lib/calculation-helper.ts

@ -9,7 +9,7 @@ import {
subDays,
subYears
} from 'date-fns';
import { isNumber } from 'lodash';
import { isFinite, isNumber } from 'lodash';
import { resetHours } from './helper';
import { DateRange } from './types';
@ -28,7 +28,7 @@ export function getAnnualizedPerformancePercent({
exponent
);
if (!isNaN(growthFactor)) {
if (isFinite(growthFactor)) {
return new Big(growthFactor).minus(1);
}
}

12
package-lock.json

@ -1,12 +1,12 @@
{
"name": "ghostfolio",
"version": "2.237.0",
"version": "2.238.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ghostfolio",
"version": "2.237.0",
"version": "2.238.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
@ -75,7 +75,7 @@
"ng-extract-i18n-merge": "3.2.1",
"ngx-device-detector": "11.0.0",
"ngx-markdown": "21.0.1",
"ngx-skeleton-loader": "11.3.0",
"ngx-skeleton-loader": "12.0.0",
"open-color": "1.9.1",
"papaparse": "5.3.1",
"passport": "0.7.0",
@ -26249,9 +26249,9 @@
}
},
"node_modules/ngx-skeleton-loader": {
"version": "11.3.0",
"resolved": "https://registry.npmjs.org/ngx-skeleton-loader/-/ngx-skeleton-loader-11.3.0.tgz",
"integrity": "sha512-MLm5shgXGiCA1W5NEqct6glBFx2AEgEKbk8pDyY15BsZ2zTGUwa5jw4pe6nJdrCj6xcl/d9oFTinQHrO0q+3RA==",
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/ngx-skeleton-loader/-/ngx-skeleton-loader-12.0.0.tgz",
"integrity": "sha512-vGEytpLElYKSLovFHCJkwgPZOdy0lPqyejxuhVFcZJg9dsp07o0/NeM4/Nnc2oCDE8T/wkXSPIbrpKzfTDbMCQ==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"

4
package.json

@ -1,6 +1,6 @@
{
"name": "ghostfolio",
"version": "2.237.0",
"version": "2.238.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",
@ -119,7 +119,7 @@
"ng-extract-i18n-merge": "3.2.1",
"ngx-device-detector": "11.0.0",
"ngx-markdown": "21.0.1",
"ngx-skeleton-loader": "11.3.0",
"ngx-skeleton-loader": "12.0.0",
"open-color": "1.9.1",
"papaparse": "5.3.1",
"passport": "0.7.0",

58
test/import/ok/jnug-buy-and-sell-and-buy-and-sell.json

@ -0,0 +1,58 @@
{
"meta": {
"date": "2026-02-07T02:09:15.272Z",
"version": "dev"
},
"activities": [
{
"currency": "USD",
"dataSource": "YAHOO",
"date": "2025-12-11T05:00:00.000Z",
"fee": 1,
"id": "cea33621-9f4b-4cea-9eb7-be38264888aa",
"quantity": 9,
"symbol": "JNUG",
"type": "BUY",
"unitPrice": 209.45
},
{
"currency": "USD",
"dataSource": "YAHOO",
"date": "2025-12-18T05:00:00.000Z",
"fee": 1,
"id": "53be2e35-a0af-476c-9e63-9b1a437114a4",
"quantity": 9,
"symbol": "JNUG",
"type": "SELL",
"unitPrice": 210
},
{
"currency": "USD",
"dataSource": "YAHOO",
"date": "2025-12-18T05:00:00.000Z",
"fee": 1,
"id": "6648eeeb-8ea5-46b6-9a30-f278a9ed477b",
"quantity": 10,
"symbol": "JNUG",
"type": "BUY",
"unitPrice": 204.11
},
{
"currency": "USD",
"dataSource": "YAHOO",
"date": "2025-12-28T05:00:00.000Z",
"fee": 1,
"id": "861e736d-0086-496c-8f85-31328479cf63",
"quantity": 10,
"symbol": "JNUG",
"type": "SELL",
"unitPrice": 208.01
}
],
"user": {
"settings": {
"currency": "USD",
"performanceCalculationType": "ROAI"
}
}
}
Loading…
Cancel
Save