Browse Source

Task/improve performance of portfolio snapshot calculation (#7656)

* Improve performance of portfolio snapshot calculation

* Update changelog
pull/7632/head
Thomas Kaul 18 hours ago
committed by GitHub
parent
commit
507e88cf9b
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 9
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  3. 61
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts

1
CHANGELOG.md

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Improved the performance of the portfolio snapshot calculation by indexing the activities
- Upgraded `angular` from version `21.2.19` to `22.1.2`
- Upgraded `ngx-device-detector` from version `11.0.0` to `12.0.0`
- Upgraded `ngx-markdown` from version `21.2.0` to `22.0.0`

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

@ -61,7 +61,7 @@ import {
startOfYear,
subDays
} from 'date-fns';
import { isNumber, sortBy, sum, uniqBy } from 'lodash';
import { groupBy, isNumber, sortBy, sum, uniqBy } from 'lodash';
export abstract class PortfolioCalculator {
protected static readonly ENABLE_LOGGING = false;
@ -72,6 +72,9 @@ export abstract class PortfolioCalculator {
protected accountBalanceItems: HistoricalDataItem[];
protected activities: PortfolioOrder[];
protected activitiesBySymbol: {
[symbol: string]: PortfolioOrder[];
};
private configurationService: ConfigurationService;
private currency: string;
@ -162,6 +165,10 @@ export abstract class PortfolioCalculator {
return a.date?.localeCompare(b.date);
});
this.activitiesBySymbol = groupBy(this.activities, ({ assetProfile }) => {
return assetProfile.symbol;
});
this.portfolioSnapshotService = portfolioSnapshotService;
this.redisCacheService = redisCacheService;
this.userId = userId;

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

@ -3,7 +3,7 @@ import { PortfolioCalculatorPosition } from '@ghostfolio/api/app/portfolio/inter
import { PortfolioOrderItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order-item.interface';
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
import {
AssetProfileIdentifier,
SymbolMetrics
@ -21,7 +21,7 @@ import {
isBefore,
isThisYear
} from 'date-fns';
import { cloneDeep, sortBy } from 'lodash';
import { sortBy } from 'lodash';
export class RoaiPortfolioCalculator extends PortfolioCalculator {
private chartDates: string[];
@ -192,12 +192,13 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
let valueAtStartDate: Big;
let valueAtStartDateWithCurrencyEffect: Big;
// Deep clone as the items are enriched below and the originals are shared
let orders: PortfolioOrderItem[] = cloneDeep(
this.activities.filter((activities) => {
return activities.assetProfile.symbol === symbol;
})
);
// Copy the items as they are enriched below. A shallow copy is sufficient
// because only top-level properties are written.
let orders: PortfolioOrderItem[] = (
this.activitiesBySymbol[symbol] ?? []
).map((activity) => {
return { ...activity };
});
const isCash = orders[0]?.assetProfile?.assetSubClass === 'CASH';
@ -268,7 +269,8 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
}
}
const dateOfFirstTransaction = new Date(orders[0].date);
const dateStringOfFirstActivity = orders[0].date;
const dateOfFirstActivity = parseDate(dateStringOfFirstActivity);
const endDateString = format(end, DATE_FORMAT);
const startDateString = format(start, DATE_FORMAT);
@ -276,7 +278,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
const unitPriceAtStartDate = marketSymbolMap[startDateString]?.[symbol];
let unitPriceAtEndDate = marketSymbolMap[endDateString]?.[symbol];
let latestActivity = orders.at(-1);
const latestActivity = orders.at(-1);
if (
dataSource === 'MANUAL' &&
@ -293,7 +295,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
if (
!unitPriceAtEndDate ||
(!unitPriceAtStartDate && isBefore(dateOfFirstTransaction, start))
(!unitPriceAtStartDate && isBefore(dateOfFirstActivity, start))
) {
// A missing market price can only affect the quantity which is held. The
// dividends, the interest and the liabilities do not hold any quantity
@ -367,7 +369,9 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
unitPrice: unitPriceAtEndDate
});
let lastUnitPrice: Big;
// Fall back to the unit price at the end date for the chart dates before
// the first known market price of the symbol
let lastUnitPrice = unitPriceAtEndDate;
const ordersByDate: { [date: string]: PortfolioOrderItem[] } = {};
@ -393,23 +397,24 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice;
}
} else {
orders.push({
assetProfile,
date: dateString,
fee: new Big(0),
feeInBaseCurrency: new Big(0),
quantity: new Big(0),
type: 'BUY',
unitPrice: marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice,
unitPriceFromMarketData:
marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice
});
}
latestActivity = orders.at(-1);
const unitPrice =
marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice;
if (dateString >= dateStringOfFirstActivity) {
orders.push({
assetProfile,
unitPrice,
date: dateString,
fee: new Big(0),
feeInBaseCurrency: new Big(0),
quantity: new Big(0),
type: 'BUY',
unitPriceFromMarketData: unitPrice
});
}
lastUnitPrice =
latestActivity.unitPriceFromMarketData ?? latestActivity.unitPrice;
lastUnitPrice = unitPrice;
}
}
// Sort orders so that the start and end placeholder order are at the correct

Loading…
Cancel
Save