Browse Source

Task/rename transactionPoints to holdingBalancesByDate (#7923)

Rename transactionPoints to holdingBalancesByDate
task/rename-net-performance-percentages-to-performance-percentages
Thomas Kaul 1 week ago
committed by GitHub
parent
commit
dcf622aa59
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 341
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  2. 20
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-and-sell.spec.ts
  3. 48
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-stock-split.spec.ts
  4. 3
      apps/api/src/app/portfolio/interfaces/holding-balance.interface.ts
  5. 6
      apps/api/src/app/portfolio/interfaces/holding-balances-at-date.interface.ts
  6. 11
      apps/api/src/app/portfolio/interfaces/transaction-point.interface.ts
  7. 4
      apps/api/src/app/portfolio/portfolio.service.spec.ts
  8. 22
      apps/api/src/app/portfolio/portfolio.service.ts

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

@ -1,6 +1,8 @@
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { PortfolioSnapshotComputationError } from '@ghostfolio/api/app/portfolio/errors/portfolio-snapshot-computation.error';
import { AccumulatedValues } from '@ghostfolio/api/app/portfolio/interfaces/accumulated-values.interface';
import { HoldingBalance } from '@ghostfolio/api/app/portfolio/interfaces/holding-balance.interface';
import { HoldingBalancesAtDate } from '@ghostfolio/api/app/portfolio/interfaces/holding-balances-at-date.interface';
import { HoldingPerformance } from '@ghostfolio/api/app/portfolio/interfaces/holding-performance.interface';
import { HoldingValuationItem } from '@ghostfolio/api/app/portfolio/interfaces/holding-valuation-item.interface';
import { HoldingValuation } from '@ghostfolio/api/app/portfolio/interfaces/holding-valuation.interface';
@ -8,8 +10,6 @@ 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 { TransactionPointSymbol } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point-symbol.interface';
import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface';
import { NetPerformancePercentages } from '@ghostfolio/api/app/portfolio/types/net-performance-percentages.type';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
@ -95,13 +95,13 @@ export abstract class PortfolioCalculator {
private endDate: Date;
private exchangeRateDataService: ExchangeRateDataService;
private filters: Filter[];
private holdingBalancesByDate: HoldingBalancesAtDate[];
private portfolioSnapshotService: PortfolioSnapshotService;
private redisCacheService: RedisCacheService;
private snapshot: PortfolioSnapshot;
private snapshotPromise: Promise<void>;
private startDate: Date;
private subscriptionType?: SubscriptionType;
private transactionPoints: TransactionPoint[];
private usePortfolioSnapshotCache: boolean;
private userId: string;
@ -204,7 +204,7 @@ export abstract class PortfolioCalculator {
this.endDate = endOfDay(endDate);
this.startDate = startOfDay(startDate);
this.computeTransactionPoints();
this.computeHoldingBalancesByDate();
this.snapshotPromise = this.usePortfolioSnapshotCache
? this.initialize()
@ -236,13 +236,15 @@ export abstract class PortfolioCalculator {
@LogPerformance
public async computeSnapshot(): Promise<PortfolioSnapshot> {
const lastTransactionPoint = this.transactionPoints.at(-1);
const latestHoldingBalances = this.holdingBalancesByDate.at(-1);
const transactionPoints = this.transactionPoints?.filter(({ date }) => {
return isBefore(parseDate(date), this.endDate);
});
const holdingBalancesByDate = this.holdingBalancesByDate?.filter(
({ date }) => {
return isBefore(parseDate(date), this.endDate);
}
);
if (!transactionPoints.length) {
if (!holdingBalancesByDate.length) {
return {
activitiesCount: 0,
createdAt: new Date(),
@ -265,8 +267,6 @@ export abstract class PortfolioCalculator {
const cashAssetProfileIdentifiers = new Set<string>();
const currencies: { [assetProfileIdentifier: string]: string } = {};
const dataGatheringItems: DataGatheringItem[] = [];
let firstIndex = transactionPoints.length;
let firstTransactionPoint: TransactionPoint = null;
let totalCashInBaseCurrency = new Big(0);
let totalInterestWithCurrencyEffect = new Big(0);
let totalLiabilitiesWithCurrencyEffect = new Big(0);
@ -276,7 +276,7 @@ export abstract class PortfolioCalculator {
currency,
dataSource,
symbol
} of transactionPoints[firstIndex - 1].items) {
} of holdingBalancesByDate.at(-1).holdings) {
// Gather data for all assets except CASH
if (assetSubClass !== 'CASH') {
dataGatheringItems.push({
@ -288,16 +288,6 @@ export abstract class PortfolioCalculator {
currencies[getAssetProfileIdentifier({ dataSource, symbol })] = currency;
}
for (let i = 0; i < transactionPoints.length; i++) {
if (
!isBefore(parseDate(transactionPoints[i].date), this.startDate) &&
firstTransactionPoint === null
) {
firstTransactionPoint = transactionPoints[i];
firstIndex = i;
}
}
const exchangeRatesByCurrency =
await this.exchangeRateDataService.getExchangeRatesByCurrency({
currencies: Array.from(new Set(Object.values(currencies))),
@ -362,10 +352,6 @@ export abstract class PortfolioCalculator {
return chartDate;
});
if (firstIndex > 0) {
firstIndex--;
}
const errors: ResponseError['errors'] = [];
let hasAnyHoldingPerformanceErrors = false;
@ -390,7 +376,7 @@ export abstract class PortfolioCalculator {
};
} = {};
for (const item of lastTransactionPoint.items) {
for (const item of latestHoldingBalances.holdings) {
const assetProfileIdentifier = getAssetProfileIdentifier(item);
const marketPriceInBaseCurrency = (
@ -909,6 +895,10 @@ export abstract class PortfolioCalculator {
return this.snapshot.totalFeesWithCurrencyEffect;
}
public getHoldingBalancesByDate() {
return this.holdingBalancesByDate;
}
protected abstract getHoldingPerformance({
chartDates,
dataSource,
@ -1300,16 +1290,16 @@ export abstract class PortfolioCalculator {
}
public getInvestments(): { date: string; investment: Big }[] {
if (this.transactionPoints.length === 0) {
if (this.holdingBalancesByDate.length === 0) {
return [];
}
return this.transactionPoints.map((transactionPoint) => {
return this.holdingBalancesByDate.map(({ date, holdings }) => {
return {
date: transactionPoint.date,
investment: transactionPoint.items.reduce(
(investment, transactionPointSymbol) =>
investment.plus(transactionPointSymbol.investment),
date,
investment: holdings.reduce(
(investment, { investment: investmentOfHolding }) =>
investment.plus(investmentOfHolding),
new Big(0)
)
};
@ -1417,9 +1407,9 @@ export abstract class PortfolioCalculator {
}
}
if (this.transactionPoints?.length > 0) {
if (this.holdingBalancesByDate?.length > 0) {
try {
const firstActivityDateString = this.transactionPoints[0].date;
const firstActivityDateString = this.holdingBalancesByDate[0].date;
firstActivityDate = firstActivityDateString
? parseDate(firstActivityDateString)
: new Date();
@ -1486,10 +1476,6 @@ export abstract class PortfolioCalculator {
};
}
public getTransactionPoints() {
return this.transactionPoints;
}
protected getUnitPriceAtEndDate({
activities,
dataSource,
@ -1519,101 +1505,15 @@ export abstract class PortfolioCalculator {
return marketPriceAtEndDate;
}
private getChartDateMap({
endDate,
startDate,
step
}: {
endDate: Date;
startDate: Date;
step: number;
}): { [date: string]: true } {
// Create a map of all relevant chart dates:
// 1. Add transaction point dates
const chartDateMap = this.transactionPoints.reduce((result, { date }) => {
result[date] = true;
return result;
}, {});
// 2. Add dates between transactions respecting the specified step size
for (const date of eachDayOfInterval(
{ end: endDate, start: startDate },
{ step }
)) {
chartDateMap[format(date, DATE_FORMAT)] = true;
}
if (step > 1) {
// Reduce the step size of last 90 days
for (const date of eachDayOfInterval(
{ end: endDate, start: subDays(endDate, 90) },
{ step: 3 }
)) {
chartDateMap[format(date, DATE_FORMAT)] = true;
}
// Reduce the step size of last 30 days
for (const date of eachDayOfInterval(
{ end: endDate, start: subDays(endDate, 30) },
{ step: 1 }
)) {
chartDateMap[format(date, DATE_FORMAT)] = true;
}
}
// Make sure the end date is present
chartDateMap[format(endDate, DATE_FORMAT)] = true;
// Make sure some key dates are present
for (const dateRange of ['1d', '1y', '5y', 'max', 'mtd', 'wtd', 'ytd']) {
const { endDate: dateRangeEnd, startDate: dateRangeStart } =
getIntervalFromDateRange({ dateRange });
if (
!isBefore(dateRangeStart, startDate) &&
!isAfter(dateRangeStart, endDate)
) {
chartDateMap[format(dateRangeStart, DATE_FORMAT)] = true;
}
if (
!isBefore(dateRangeEnd, startDate) &&
!isAfter(dateRangeEnd, endDate)
) {
chartDateMap[format(dateRangeEnd, DATE_FORMAT)] = true;
}
}
// Make sure the first and last date of each calendar year is present
const interval = { start: startDate, end: endDate };
for (const date of eachYearOfInterval(interval)) {
const yearStart = startOfYear(date);
const yearEnd = endOfYear(date);
if (isWithinInterval(yearStart, interval)) {
// Add start of year (YYYY-01-01)
chartDateMap[format(yearStart, DATE_FORMAT)] = true;
}
if (isWithinInterval(yearEnd, interval)) {
// Add end of year (YYYY-12-31)
chartDateMap[format(yearEnd, DATE_FORMAT)] = true;
}
}
return chartDateMap;
}
@LogPerformance
private computeTransactionPoints() {
this.transactionPoints = [];
const transactionPointSymbols: {
[assetProfileIdentifier: string]: TransactionPointSymbol;
private computeHoldingBalancesByDate() {
this.holdingBalancesByDate = [];
const holdingBalancesByAssetProfileIdentifier: {
[assetProfileIdentifier: string]: HoldingBalance;
} = {};
let lastDate: string = null;
let lastTransactionPoint: TransactionPoint = null;
let latestHoldingBalances: HoldingBalancesAtDate = null;
for (const {
assetProfile,
@ -1625,7 +1525,7 @@ export abstract class PortfolioCalculator {
type,
unitPrice
} of this.activities) {
let currentTransactionPointItem: TransactionPointSymbol;
let holdingBalance: HoldingBalance;
const assetSubClass = assetProfile.assetSubClass;
const currency = assetProfile.currency;
@ -1636,33 +1536,33 @@ export abstract class PortfolioCalculator {
const assetProfileIdentifier = getAssetProfileIdentifier(assetProfile);
const oldAccumulatedSymbol =
transactionPointSymbols[assetProfileIdentifier];
const previousHoldingBalance =
holdingBalancesByAssetProfileIdentifier[assetProfileIdentifier];
if (oldAccumulatedSymbol) {
let investment = oldAccumulatedSymbol.investment;
if (previousHoldingBalance) {
let investment = previousHoldingBalance.investment;
let newQuantity = quantity
.mul(factor)
.plus(oldAccumulatedSymbol.quantity);
.plus(previousHoldingBalance.quantity);
if (type === 'BUY') {
if (oldAccumulatedSymbol.investment.gte(0)) {
investment = oldAccumulatedSymbol.investment.plus(
if (previousHoldingBalance.investment.gte(0)) {
investment = previousHoldingBalance.investment.plus(
quantity.mul(unitPrice)
);
} else {
investment = oldAccumulatedSymbol.investment.plus(
quantity.mul(oldAccumulatedSymbol.averagePrice)
investment = previousHoldingBalance.investment.plus(
quantity.mul(previousHoldingBalance.averagePrice)
);
}
} else if (type === 'SELL') {
if (oldAccumulatedSymbol.investment.gt(0)) {
investment = oldAccumulatedSymbol.investment.minus(
quantity.mul(oldAccumulatedSymbol.averagePrice)
if (previousHoldingBalance.investment.gt(0)) {
investment = previousHoldingBalance.investment.minus(
quantity.mul(previousHoldingBalance.averagePrice)
);
} else {
investment = oldAccumulatedSymbol.investment.minus(
investment = previousHoldingBalance.investment.minus(
quantity.mul(unitPrice)
);
}
@ -1674,28 +1574,27 @@ export abstract class PortfolioCalculator {
newQuantity = new Big(0);
}
currentTransactionPointItem = {
holdingBalance = {
assetSubClass,
currency,
dataSource,
investment,
skipErrors,
symbol,
activitiesCount: oldAccumulatedSymbol.activitiesCount + 1,
activitiesCount: previousHoldingBalance.activitiesCount + 1,
averagePrice: newQuantity.eq(0)
? new Big(0)
: investment.div(newQuantity).abs(),
dateOfFirstActivity: oldAccumulatedSymbol.dateOfFirstActivity,
dividend: new Big(0),
fee: oldAccumulatedSymbol.fee.plus(fee),
dateOfFirstActivity: previousHoldingBalance.dateOfFirstActivity,
fee: previousHoldingBalance.fee.plus(fee),
feeInBaseCurrency:
oldAccumulatedSymbol.feeInBaseCurrency.plus(feeInBaseCurrency),
includeInHoldings: oldAccumulatedSymbol.includeInHoldings,
previousHoldingBalance.feeInBaseCurrency.plus(feeInBaseCurrency),
includeInHoldings: previousHoldingBalance.includeInHoldings,
quantity: newQuantity,
tags: oldAccumulatedSymbol.tags.concat(tags)
tags: previousHoldingBalance.tags.concat(tags)
};
} else {
currentTransactionPointItem = {
holdingBalance = {
assetSubClass,
currency,
dataSource,
@ -1707,75 +1606,131 @@ export abstract class PortfolioCalculator {
activitiesCount: 1,
averagePrice: unitPrice,
dateOfFirstActivity: date,
dividend: new Big(0),
includeInHoldings: INVESTMENT_ACTIVITY_TYPES.includes(type),
investment: unitPrice.mul(quantity).mul(factor),
quantity: quantity.mul(factor)
};
}
currentTransactionPointItem.tags = uniqBy(
currentTransactionPointItem.tags,
'id'
);
transactionPointSymbols[assetProfileIdentifier] =
currentTransactionPointItem;
holdingBalance.tags = uniqBy(holdingBalance.tags, 'id');
const items = lastTransactionPoint?.items ?? [];
holdingBalancesByAssetProfileIdentifier[assetProfileIdentifier] =
holdingBalance;
const newItems = items.filter((item) => {
return getAssetProfileIdentifier(item) !== assetProfileIdentifier;
});
const holdings = (latestHoldingBalances?.holdings ?? []).filter(
(holding) => {
return getAssetProfileIdentifier(holding) !== assetProfileIdentifier;
}
);
newItems.push(currentTransactionPointItem);
holdings.push(holdingBalance);
newItems.sort((a, b) => {
holdings.sort((a, b) => {
return (
a.symbol?.localeCompare(b.symbol) ||
a.dataSource?.localeCompare(b.dataSource)
);
});
let fees = new Big(0);
if (lastDate !== date || latestHoldingBalances === null) {
latestHoldingBalances = { date, holdings };
if (type === 'FEE') {
fees = fee;
this.holdingBalancesByDate.push(latestHoldingBalances);
} else {
latestHoldingBalances.holdings = holdings;
}
let interest = new Big(0);
lastDate = date;
}
}
if (type === 'INTEREST') {
interest = quantity.mul(unitPrice);
private getChartDateMap({
endDate,
startDate,
step
}: {
endDate: Date;
startDate: Date;
step: number;
}): { [date: string]: true } {
// Create a map of all relevant chart dates:
// 1. Add the dates of the holding balances
const chartDateMap = this.holdingBalancesByDate.reduce(
(result, { date }) => {
result[date] = true;
return result;
},
{}
);
// 2. Add the dates in between, with the specified step size
for (const date of eachDayOfInterval(
{ end: endDate, start: startDate },
{ step }
)) {
chartDateMap[format(date, DATE_FORMAT)] = true;
}
if (step > 1) {
// Reduce the step size of last 90 days
for (const date of eachDayOfInterval(
{ end: endDate, start: subDays(endDate, 90) },
{ step: 3 }
)) {
chartDateMap[format(date, DATE_FORMAT)] = true;
}
let liabilities = new Big(0);
// Reduce the step size of last 30 days
for (const date of eachDayOfInterval(
{ end: endDate, start: subDays(endDate, 30) },
{ step: 1 }
)) {
chartDateMap[format(date, DATE_FORMAT)] = true;
}
}
// Make sure the end date is present
chartDateMap[format(endDate, DATE_FORMAT)] = true;
// Make sure some key dates are present
for (const dateRange of ['1d', '1y', '5y', 'max', 'mtd', 'wtd', 'ytd']) {
const { endDate: dateRangeEnd, startDate: dateRangeStart } =
getIntervalFromDateRange({ dateRange });
if (type === 'LIABILITY') {
liabilities = quantity.mul(unitPrice);
if (
!isBefore(dateRangeStart, startDate) &&
!isAfter(dateRangeStart, endDate)
) {
chartDateMap[format(dateRangeStart, DATE_FORMAT)] = true;
}
if (lastDate !== date || lastTransactionPoint === null) {
lastTransactionPoint = {
date,
fees,
interest,
liabilities,
items: newItems
};
if (
!isBefore(dateRangeEnd, startDate) &&
!isAfter(dateRangeEnd, endDate)
) {
chartDateMap[format(dateRangeEnd, DATE_FORMAT)] = true;
}
}
this.transactionPoints.push(lastTransactionPoint);
} else {
lastTransactionPoint.fees = lastTransactionPoint.fees.plus(fees);
lastTransactionPoint.interest =
lastTransactionPoint.interest.plus(interest);
lastTransactionPoint.items = newItems;
lastTransactionPoint.liabilities =
lastTransactionPoint.liabilities.plus(liabilities);
// Make sure the first and last date of each calendar year is present
const interval = { start: startDate, end: endDate };
for (const date of eachYearOfInterval(interval)) {
const yearStart = startOfYear(date);
const yearEnd = endOfYear(date);
if (isWithinInterval(yearStart, interval)) {
// Add start of year (YYYY-01-01)
chartDateMap[format(yearStart, DATE_FORMAT)] = true;
}
lastDate = date;
if (isWithinInterval(yearEnd, interval)) {
// Add end of year (YYYY-12-31)
chartDateMap[format(yearEnd, DATE_FORMAT)] = true;
}
}
return chartDateMap;
}
@LogPerformance

20
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-and-sell.spec.ts

@ -75,7 +75,7 @@ describe('PortfolioCalculator', () => {
);
});
describe('get transaction point', () => {
describe('get holding balances by date', () => {
it('with MSFT buy and sell with fractional quantities (multiples of 1/3)', () => {
jest.useFakeTimers().setSystemTime(parseDate('2024-04-01').getTime());
@ -137,15 +137,19 @@ describe('PortfolioCalculator', () => {
userId: userDummyData.id
});
const transactionPoints = portfolioCalculator.getTransactionPoints();
const lastTransactionPoint =
transactionPoints[transactionPoints.length - 1];
const position = lastTransactionPoint.items.find(
(item) => item.symbol === 'MSFT'
const holdingBalancesByDate =
portfolioCalculator.getHoldingBalancesByDate();
const latestHoldingBalances = holdingBalancesByDate.at(-1);
const { investment, quantity } = latestHoldingBalances.holdings.find(
({ symbol }) => {
return symbol === 'MSFT';
}
);
expect(position.investment.toNumber()).toBe(0);
expect(position.quantity.toNumber()).toBe(0);
expect(investment.toNumber()).toBe(0);
expect(quantity.toNumber()).toBe(0);
});
});
});

48
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-stock-split.spec.ts

@ -85,9 +85,11 @@ describe('RoaiPortfolioCalculator stock splits', () => {
[createSplit({ denominator: 1, numerator: 2 })]
);
const position = getLastPosition(portfolioCalculatorFactory, [activity]);
const holdingBalance = getLatestHoldingBalance(portfolioCalculatorFactory, [
activity
]);
expect(position).toMatchObject({
expect(holdingBalance).toMatchObject({
averagePrice: new Big(50),
investment: new Big(1000),
quantity: new Big(20)
@ -100,9 +102,11 @@ describe('RoaiPortfolioCalculator stock splits', () => {
[createSplit({ denominator: 10, numerator: 1 })]
);
const position = getLastPosition(portfolioCalculatorFactory, [activity]);
const holdingBalance = getLatestHoldingBalance(portfolioCalculatorFactory, [
activity
]);
expect(position).toMatchObject({
expect(holdingBalance).toMatchObject({
averagePrice: new Big(1000),
investment: new Big(1000),
quantity: new Big(1)
@ -121,9 +125,12 @@ describe('RoaiPortfolioCalculator stock splits', () => {
unitPrice: 60
});
const position = getLastPosition(portfolioCalculatorFactory, [buy, sell]);
const holdingBalance = getLatestHoldingBalance(portfolioCalculatorFactory, [
buy,
sell
]);
expect(position).toMatchObject({
expect(holdingBalance).toMatchObject({
averagePrice: new Big(50),
investment: new Big(750),
quantity: new Big(15)
@ -139,11 +146,15 @@ describe('RoaiPortfolioCalculator stock splits', () => {
]
);
const position = getLastPosition(portfolioCalculatorFactory, [activity]);
const holdingBalance = getLatestHoldingBalance(portfolioCalculatorFactory, [
activity
]);
expect(position.averagePrice).toEqual(new Big(150));
expect(position.quantity.toFixed(15)).toBe(new Big(20).div(3).toFixed(15));
expect(position.investment.toNumber()).toBeCloseTo(1000, 12);
expect(holdingBalance.averagePrice).toEqual(new Big(150));
expect(holdingBalance.quantity.toFixed(15)).toBe(
new Big(20).div(3).toFixed(15)
);
expect(holdingBalance.investment.toNumber()).toBeCloseTo(1000, 12);
});
it('resets quantity and investment when the adjusted position is closed', () => {
@ -158,18 +169,21 @@ describe('RoaiPortfolioCalculator stock splits', () => {
unitPrice: 60
});
const position = getLastPosition(portfolioCalculatorFactory, [buy, sell]);
const holdingBalance = getLatestHoldingBalance(portfolioCalculatorFactory, [
buy,
sell
]);
expect(position.quantity).toEqual(new Big(0));
expect(position.investment).toEqual(new Big(0));
expect(holdingBalance.quantity).toEqual(new Big(0));
expect(holdingBalance.investment).toEqual(new Big(0));
});
it('preserves existing behavior when no splits exist', () => {
const position = getLastPosition(portfolioCalculatorFactory, [
const holdingBalance = getLatestHoldingBalance(portfolioCalculatorFactory, [
createActivity({ unitPrice: 100 })
]);
expect(position).toMatchObject({
expect(holdingBalance).toMatchObject({
averagePrice: new Big(100),
investment: new Big(1000),
quantity: new Big(10)
@ -208,7 +222,7 @@ describe('RoaiPortfolioCalculator stock splits', () => {
});
});
function getLastPosition(
function getLatestHoldingBalance(
portfolioCalculatorFactory: PortfolioCalculatorFactory,
activities: Activity[]
) {
@ -219,7 +233,7 @@ function getLastPosition(
userId: userDummyData.id
});
return calculator.getTransactionPoints().at(-1).items[0];
return calculator.getHoldingBalancesByDate().at(-1).holdings[0];
}
function createActivity({

3
apps/api/src/app/portfolio/interfaces/transaction-point-symbol.interface.ts → apps/api/src/app/portfolio/interfaces/holding-balance.interface.ts

@ -1,14 +1,13 @@
import { AssetSubClass, DataSource, Tag } from '@prisma/client';
import { Big } from 'big.js';
export interface TransactionPointSymbol {
export interface HoldingBalance {
activitiesCount: number;
assetSubClass: AssetSubClass;
averagePrice: Big;
currency: string;
dataSource: DataSource;
dateOfFirstActivity: string;
dividend: Big;
fee: Big;
feeInBaseCurrency: Big;
includeInHoldings: boolean;

6
apps/api/src/app/portfolio/interfaces/holding-balances-at-date.interface.ts

@ -0,0 +1,6 @@
import { HoldingBalance } from './holding-balance.interface';
export interface HoldingBalancesAtDate {
date: string;
holdings: HoldingBalance[];
}

11
apps/api/src/app/portfolio/interfaces/transaction-point.interface.ts

@ -1,11 +0,0 @@
import { Big } from 'big.js';
import { TransactionPointSymbol } from './transaction-point-symbol.interface';
export interface TransactionPoint {
date: string;
fees: Big;
interest: Big;
items: TransactionPointSymbol[];
liabilities: Big;
}

4
apps/api/src/app/portfolio/portfolio.service.spec.ts

@ -646,8 +646,8 @@ describe('PortfolioService', () => {
jest
.spyOn(portfolioCalculatorFactory, 'createCalculator')
.mockReturnValue({
getSnapshot: jest.fn().mockResolvedValue({ positions: [] }),
getTransactionPoints: jest.fn().mockReturnValue([])
getHoldingBalancesByDate: jest.fn().mockReturnValue([]),
getSnapshot: jest.fn().mockResolvedValue({ positions: [] })
} as unknown as PortfolioCalculator);
});

22
apps/api/src/app/portfolio/portfolio.service.ts

@ -970,7 +970,8 @@ export class PortfolioService {
subscriptionType: user.subscription?.type
});
const transactionPoints = portfolioCalculator.getTransactionPoints();
const holdingBalancesByDate =
portfolioCalculator.getHoldingBalancesByDate();
const { positions } = await portfolioCalculator.getSnapshot();
@ -1071,8 +1072,11 @@ export class PortfolioService {
historicalDataItems
)) {
while (
j + 1 < transactionPoints.length &&
!isAfter(parseDate(transactionPoints[j + 1].date), parseDate(date))
j + 1 < holdingBalancesByDate.length &&
!isAfter(
parseDate(holdingBalancesByDate[j + 1].date),
parseDate(date)
)
) {
j++;
}
@ -1080,15 +1084,15 @@ export class PortfolioService {
let currentAveragePrice = 0;
let currentQuantity = 0;
const currentSymbol = transactionPoints[j]?.items.find(
(transactionPointSymbol) => {
return transactionPointSymbol.symbol === symbol;
const holdingBalance = holdingBalancesByDate[j]?.holdings.find(
({ symbol: holdingBalanceSymbol }) => {
return holdingBalanceSymbol === symbol;
}
);
if (currentSymbol) {
currentAveragePrice = currentSymbol.averagePrice.toNumber();
currentQuantity = currentSymbol.quantity.toNumber();
if (holdingBalance) {
currentAveragePrice = holdingBalance.averagePrice.toNumber();
currentQuantity = holdingBalance.quantity.toNumber();
}
historicalDataArray.push({

Loading…
Cancel
Save