Browse Source

Merge branch 'main' into task/upgrade-bull-board-to-version-9.10.1

pull/7928/head
Thomas Kaul 4 weeks ago
committed by GitHub
parent
commit
44928ad66c
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 8
      CHANGELOG.md
  2. 14
      apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts
  3. 31
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  4. 212
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts
  5. 14
      apps/api/src/app/portfolio/calculator/roi/portfolio-calculator.ts
  6. 14
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts
  7. 2
      apps/api/src/app/portfolio/types/performance-percentages.type.ts
  8. 2
      apps/api/src/app/subscription/subscription.service.ts
  9. 12
      package-lock.json
  10. 4
      package.json

8
CHANGELOG.md

@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Changed
- Upgraded `bull-board` from version `9.9.0` to `9.10.1`
## 3.72.0 - 2026-09-20
### Added
- Added a tool to get the watchlist to the server of the Model Context Protocol (MCP) (experimental)
@ -15,7 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Extended the cache key of the portfolio snapshot by the performance calculation type
- Hardened the validation of the device id in the biometric authentication
- Upgraded `bull-board` from version `9.9.0` to `9.10.1`
- Upgraded `stripe` from version `22.5.0` to `22.6.2`
### Fixed

14
apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts

@ -1,24 +1,24 @@
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
import { HoldingPerformance } from '@ghostfolio/api/app/portfolio/interfaces/holding-performance.interface';
import { NetPerformancePercentages } from '@ghostfolio/api/app/portfolio/types/net-performance-percentages.type';
import { PerformancePercentages } from '@ghostfolio/api/app/portfolio/types/performance-percentages.type';
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import { PortfolioSnapshot } from '@ghostfolio/common/models';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
export class MwrPortfolioCalculator extends PortfolioCalculator {
protected calculateNetPerformancePercentages(): {
[date: string]: NetPerformancePercentages;
} {
protected calculateOverallPerformance(): PortfolioSnapshot {
throw new Error('Method not implemented.');
}
protected calculateNetPerformancePercentagesForDateRange(): {
[date: string]: NetPerformancePercentages;
protected calculatePerformancePercentages(): {
[date: string]: PerformancePercentages;
} {
throw new Error('Method not implemented.');
}
protected calculateOverallPerformance(): PortfolioSnapshot {
protected calculatePerformancePercentagesForDateRange(): {
[date: string]: PerformancePercentages;
} {
throw new Error('Method not implemented.');
}

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

@ -10,7 +10,7 @@ import { PortfolioCalculatorActivityItem } from '@ghostfolio/api/app/portfolio/i
import { PortfolioCalculatorActivity } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator-activity.interface';
import { PortfolioCalculatorHolding } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator-holding.interface';
import { PortfolioSnapshotValue } from '@ghostfolio/api/app/portfolio/interfaces/snapshot-value.interface';
import { NetPerformancePercentages } from '@ghostfolio/api/app/portfolio/types/net-performance-percentages.type';
import { PerformancePercentages } from '@ghostfolio/api/app/portfolio/types/performance-percentages.type';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor';
@ -218,21 +218,21 @@ export abstract class PortfolioCalculator {
this.snapshotPromise.catch(() => undefined);
}
protected abstract calculateNetPerformancePercentages({
protected abstract calculateOverallPerformance(
positions: PortfolioCalculatorHolding[]
): PortfolioSnapshot;
protected abstract calculatePerformancePercentages({
accumulatedValuesByDate
}: {
accumulatedValuesByDate: { [date: string]: AccumulatedValues };
}): { [date: string]: NetPerformancePercentages };
}): { [date: string]: PerformancePercentages };
protected abstract calculateNetPerformancePercentagesForDateRange({
protected abstract calculatePerformancePercentagesForDateRange({
historicalDataItems
}: {
historicalDataItems: HistoricalDataItem[];
}): { [date: string]: NetPerformancePercentages };
protected abstract calculateOverallPerformance(
positions: PortfolioCalculatorHolding[]
): PortfolioSnapshot;
}): { [date: string]: PerformancePercentages };
@LogPerformance
public async computeSnapshot(): Promise<PortfolioSnapshot> {
@ -644,8 +644,9 @@ export abstract class PortfolioCalculator {
}
}
const netPerformancePercentagesByDate =
this.calculateNetPerformancePercentages({ accumulatedValuesByDate });
const performancePercentagesByDate = this.calculatePerformancePercentages({
accumulatedValuesByDate
});
const historicalData: HistoricalDataItem[] = Object.entries(
accumulatedValuesByDate
@ -663,7 +664,7 @@ export abstract class PortfolioCalculator {
} = values;
return {
...netPerformancePercentagesByDate[date],
...performancePercentagesByDate[date],
date,
investmentValueWithCurrencyEffect:
investmentValueWithCurrencyEffect.toNumber(),
@ -1369,15 +1370,15 @@ export abstract class PortfolioCalculator {
}
}
const netPerformancePercentagesByDate =
this.calculateNetPerformancePercentagesForDateRange({
const performancePercentagesByDate =
this.calculatePerformancePercentagesForDateRange({
historicalDataItems: historicalDataItemsOfDateRange
});
const chart = historicalDataItemsOfDateRange.map((historicalDataItem) => {
return {
...historicalDataItem,
...netPerformancePercentagesByDate[historicalDataItem.date]
...performancePercentagesByDate[historicalDataItem.date]
};
});

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

@ -3,7 +3,7 @@ import { AccumulatedValues } from '@ghostfolio/api/app/portfolio/interfaces/accu
import { HoldingPerformance } from '@ghostfolio/api/app/portfolio/interfaces/holding-performance.interface';
import { PortfolioCalculatorActivityItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator-activity-item.interface';
import { PortfolioCalculatorHolding } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator-holding.interface';
import { NetPerformancePercentages } from '@ghostfolio/api/app/portfolio/types/net-performance-percentages.type';
import { PerformancePercentages } from '@ghostfolio/api/app/portfolio/types/performance-percentages.type';
import {
getAnnualizedPerformancePercent,
getIntervalFromDateRange
@ -32,111 +32,6 @@ import {
import { sum } from 'lodash';
export class RoaiPortfolioCalculator extends PortfolioCalculator {
protected calculateNetPerformancePercentages({
accumulatedValuesByDate
}: {
accumulatedValuesByDate: { [date: string]: AccumulatedValues };
}): { [date: string]: NetPerformancePercentages } {
const netPerformancePercentagesByDate: {
[date: string]: NetPerformancePercentages;
} = {};
for (const [
date,
{
totalAverageInvestmentValue,
totalAverageInvestmentValueWithCurrencyEffect,
totalNetPerformanceValue,
totalNetPerformanceValueWithCurrencyEffect
}
] of Object.entries(accumulatedValuesByDate)) {
netPerformancePercentagesByDate[date] = {
netPerformanceInPercentage: totalAverageInvestmentValue.eq(0)
? 0
: totalNetPerformanceValue
.div(totalAverageInvestmentValue)
.toNumber(),
netPerformanceInPercentageWithCurrencyEffect:
totalAverageInvestmentValueWithCurrencyEffect.eq(0)
? 0
: totalNetPerformanceValueWithCurrencyEffect
.div(totalAverageInvestmentValueWithCurrencyEffect)
.toNumber()
};
}
return netPerformancePercentagesByDate;
}
protected calculateNetPerformancePercentagesForDateRange({
historicalDataItems
}: {
historicalDataItems: HistoricalDataItem[];
}): { [date: string]: NetPerformancePercentages } {
const averageInvestmentValues: number[] = [];
const averageInvestmentValuesWithCurrencyEffect: number[] = [];
let grossPerformanceAtStartDate: number;
let grossPerformanceWithCurrencyEffectAtStartDate: number;
const netPerformancePercentagesByDate: {
[date: string]: NetPerformancePercentages;
} = {};
for (const [index, historicalDataItem] of historicalDataItems.entries()) {
// Take the values at the start date from the first day of the date
// range
if (index === 0) {
grossPerformanceAtStartDate =
historicalDataItem.value - historicalDataItem.totalInvestment;
grossPerformanceWithCurrencyEffectAtStartDate =
historicalDataItem.valueWithCurrencyEffect -
historicalDataItem.totalInvestmentValueWithCurrencyEffect;
}
// Add the gross performance at the start date of the range to the
// investment of each day. Thus the range starts with the value of its
// first day, and subsequent buy and sell activities stay included.
if (historicalDataItem.totalInvestment > 0) {
averageInvestmentValues.push(
historicalDataItem.totalInvestment + grossPerformanceAtStartDate
);
}
if (historicalDataItem.totalInvestmentValueWithCurrencyEffect > 0) {
averageInvestmentValuesWithCurrencyEffect.push(
historicalDataItem.totalInvestmentValueWithCurrencyEffect +
grossPerformanceWithCurrencyEffectAtStartDate
);
}
const averageInvestmentValue =
averageInvestmentValues.length > 0
? sum(averageInvestmentValues) / averageInvestmentValues.length
: 0;
const averageInvestmentValueWithCurrencyEffect =
averageInvestmentValuesWithCurrencyEffect.length > 0
? sum(averageInvestmentValuesWithCurrencyEffect) /
averageInvestmentValuesWithCurrencyEffect.length
: 0;
netPerformancePercentagesByDate[historicalDataItem.date] = {
netPerformanceInPercentage:
averageInvestmentValue > 0
? historicalDataItem.netPerformance / averageInvestmentValue
: 0,
netPerformanceInPercentageWithCurrencyEffect:
averageInvestmentValueWithCurrencyEffect > 0
? historicalDataItem.netPerformanceWithCurrencyEffect /
averageInvestmentValueWithCurrencyEffect
: 0
};
}
return netPerformancePercentagesByDate;
}
protected calculateOverallPerformance(
positions: PortfolioCalculatorHolding[]
): PortfolioSnapshot {
@ -269,6 +164,111 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
};
}
protected calculatePerformancePercentages({
accumulatedValuesByDate
}: {
accumulatedValuesByDate: { [date: string]: AccumulatedValues };
}): { [date: string]: PerformancePercentages } {
const performancePercentagesByDate: {
[date: string]: PerformancePercentages;
} = {};
for (const [
date,
{
totalAverageInvestmentValue,
totalAverageInvestmentValueWithCurrencyEffect,
totalNetPerformanceValue,
totalNetPerformanceValueWithCurrencyEffect
}
] of Object.entries(accumulatedValuesByDate)) {
performancePercentagesByDate[date] = {
netPerformanceInPercentage: totalAverageInvestmentValue.eq(0)
? 0
: totalNetPerformanceValue
.div(totalAverageInvestmentValue)
.toNumber(),
netPerformanceInPercentageWithCurrencyEffect:
totalAverageInvestmentValueWithCurrencyEffect.eq(0)
? 0
: totalNetPerformanceValueWithCurrencyEffect
.div(totalAverageInvestmentValueWithCurrencyEffect)
.toNumber()
};
}
return performancePercentagesByDate;
}
protected calculatePerformancePercentagesForDateRange({
historicalDataItems
}: {
historicalDataItems: HistoricalDataItem[];
}): { [date: string]: PerformancePercentages } {
const averageInvestmentValues: number[] = [];
const averageInvestmentValuesWithCurrencyEffect: number[] = [];
let grossPerformanceAtStartDate: number;
let grossPerformanceWithCurrencyEffectAtStartDate: number;
const performancePercentagesByDate: {
[date: string]: PerformancePercentages;
} = {};
for (const [index, historicalDataItem] of historicalDataItems.entries()) {
// Take the values at the start date from the first day of the date
// range
if (index === 0) {
grossPerformanceAtStartDate =
historicalDataItem.value - historicalDataItem.totalInvestment;
grossPerformanceWithCurrencyEffectAtStartDate =
historicalDataItem.valueWithCurrencyEffect -
historicalDataItem.totalInvestmentValueWithCurrencyEffect;
}
// Add the gross performance at the start date of the range to the
// investment of each day. Thus the range starts with the value of its
// first day, and subsequent buy and sell activities stay included.
if (historicalDataItem.totalInvestment > 0) {
averageInvestmentValues.push(
historicalDataItem.totalInvestment + grossPerformanceAtStartDate
);
}
if (historicalDataItem.totalInvestmentValueWithCurrencyEffect > 0) {
averageInvestmentValuesWithCurrencyEffect.push(
historicalDataItem.totalInvestmentValueWithCurrencyEffect +
grossPerformanceWithCurrencyEffectAtStartDate
);
}
const averageInvestmentValue =
averageInvestmentValues.length > 0
? sum(averageInvestmentValues) / averageInvestmentValues.length
: 0;
const averageInvestmentValueWithCurrencyEffect =
averageInvestmentValuesWithCurrencyEffect.length > 0
? sum(averageInvestmentValuesWithCurrencyEffect) /
averageInvestmentValuesWithCurrencyEffect.length
: 0;
performancePercentagesByDate[historicalDataItem.date] = {
netPerformanceInPercentage:
averageInvestmentValue > 0
? historicalDataItem.netPerformance / averageInvestmentValue
: 0,
netPerformanceInPercentageWithCurrencyEffect:
averageInvestmentValueWithCurrencyEffect > 0
? historicalDataItem.netPerformanceWithCurrencyEffect /
averageInvestmentValueWithCurrencyEffect
: 0
};
}
return performancePercentagesByDate;
}
protected getHoldingPerformance({
chartDates,
dataSource,

14
apps/api/src/app/portfolio/calculator/roi/portfolio-calculator.ts

@ -1,24 +1,24 @@
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
import { HoldingPerformance } from '@ghostfolio/api/app/portfolio/interfaces/holding-performance.interface';
import { NetPerformancePercentages } from '@ghostfolio/api/app/portfolio/types/net-performance-percentages.type';
import { PerformancePercentages } from '@ghostfolio/api/app/portfolio/types/performance-percentages.type';
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import { PortfolioSnapshot } from '@ghostfolio/common/models';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
export class RoiPortfolioCalculator extends PortfolioCalculator {
protected calculateNetPerformancePercentages(): {
[date: string]: NetPerformancePercentages;
} {
protected calculateOverallPerformance(): PortfolioSnapshot {
throw new Error('Method not implemented.');
}
protected calculateNetPerformancePercentagesForDateRange(): {
[date: string]: NetPerformancePercentages;
protected calculatePerformancePercentages(): {
[date: string]: PerformancePercentages;
} {
throw new Error('Method not implemented.');
}
protected calculateOverallPerformance(): PortfolioSnapshot {
protected calculatePerformancePercentagesForDateRange(): {
[date: string]: PerformancePercentages;
} {
throw new Error('Method not implemented.');
}

14
apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts

@ -1,24 +1,24 @@
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
import { HoldingPerformance } from '@ghostfolio/api/app/portfolio/interfaces/holding-performance.interface';
import { NetPerformancePercentages } from '@ghostfolio/api/app/portfolio/types/net-performance-percentages.type';
import { PerformancePercentages } from '@ghostfolio/api/app/portfolio/types/performance-percentages.type';
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import { PortfolioSnapshot } from '@ghostfolio/common/models';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
export class TwrPortfolioCalculator extends PortfolioCalculator {
protected calculateNetPerformancePercentages(): {
[date: string]: NetPerformancePercentages;
} {
protected calculateOverallPerformance(): PortfolioSnapshot {
throw new Error('Method not implemented.');
}
protected calculateNetPerformancePercentagesForDateRange(): {
[date: string]: NetPerformancePercentages;
protected calculatePerformancePercentages(): {
[date: string]: PerformancePercentages;
} {
throw new Error('Method not implemented.');
}
protected calculateOverallPerformance(): PortfolioSnapshot {
protected calculatePerformancePercentagesForDateRange(): {
[date: string]: PerformancePercentages;
} {
throw new Error('Method not implemented.');
}

2
apps/api/src/app/portfolio/types/net-performance-percentages.type.ts → apps/api/src/app/portfolio/types/performance-percentages.type.ts

@ -1,6 +1,6 @@
import { HistoricalDataItem } from '@ghostfolio/common/interfaces';
export type NetPerformancePercentages = Required<
export type PerformancePercentages = Required<
Pick<
HistoricalDataItem,
| 'netPerformanceInPercentage'

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

@ -38,7 +38,7 @@ export class SubscriptionService {
this.stripe = new Stripe(
this.configurationService.get('STRIPE_SECRET_KEY'),
{
apiVersion: '2026-07-29.dahlia'
apiVersion: '2026-08-26.dahlia'
}
);
}

12
package-lock.json

@ -1,12 +1,12 @@
{
"name": "ghostfolio",
"version": "3.71.0",
"version": "3.72.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ghostfolio",
"version": "3.71.0",
"version": "3.72.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
@ -100,7 +100,7 @@
"passport-openidconnect": "0.1.2",
"reflect-metadata": "0.2.2",
"rxjs": "7.8.1",
"stripe": "22.5.0",
"stripe": "22.6.2",
"svgmap": "2.21.0",
"tablemark": "4.1.0",
"twitter-api-v2": "1.29.1",
@ -33626,9 +33626,9 @@
}
},
"node_modules/stripe": {
"version": "22.5.0",
"resolved": "https://registry.npmjs.org/stripe/-/stripe-22.5.0.tgz",
"integrity": "sha512-QVwMwriC0bbySx6R4dpsvJ0W//GojC1kwWVS6rPSoVqDUIZX4Hy3TaUrd2AZeXEAaKbfWIjQjvo3vKAReHZ0vQ==",
"version": "22.6.2",
"resolved": "https://registry.npmjs.org/stripe/-/stripe-22.6.2.tgz",
"integrity": "sha512-PwRE2scocvXqDKPiskMa5vRJEAEkq46W69XyUYmAwrNSLr675lQnfqLwnsndAWizwgihOWw/irLmoFRo3ez7ew==",
"license": "MIT",
"engines": {
"node": ">=18"

4
package.json

@ -1,6 +1,6 @@
{
"name": "ghostfolio",
"version": "3.71.0",
"version": "3.72.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",
@ -144,7 +144,7 @@
"passport-openidconnect": "0.1.2",
"reflect-metadata": "0.2.2",
"rxjs": "7.8.1",
"stripe": "22.5.0",
"stripe": "22.6.2",
"svgmap": "2.21.0",
"tablemark": "4.1.0",
"twitter-api-v2": "1.29.1",

Loading…
Cancel
Save