Browse Source

Portfolio calculation refined

pull/4597/head
csehatt741 4 months ago
committed by Attila Cseh
parent
commit
cd837afbe8
  1. 2
      apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts
  2. 2
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  3. 251
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts
  4. 248
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts
  5. 4
      apps/api/src/app/portfolio/current-rate.service.mock.ts
  6. 14
      apps/api/src/app/portfolio/portfolio.service.ts
  7. 7
      apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts
  8. 2
      test/import/ok-btceur.json
  9. 2
      test/import/ok-btcusd.json

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

@ -7,10 +7,12 @@ export const activityDummyData = {
createdAt: new Date(),
currency: undefined,
fee: undefined,
feeInAssetProfileCurrency: undefined,
id: undefined,
isDraft: false,
symbolProfileId: undefined,
unitPrice: undefined,
unitPriceInAssetProfileCurrency: undefined,
updatedAt: new Date(),
userId: undefined,
value: undefined,

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

@ -902,8 +902,8 @@ export abstract class PortfolioCalculator {
let lastTransactionPoint: TransactionPoint = null;
for (const {
fee,
date,
fee,
quantity,
SymbolProfile,
tags,

251
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts

@ -0,0 +1,251 @@
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import {
activityDummyData,
loadActivityExportFile,
symbolProfileDummyData,
userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import {
PerformanceCalculationType,
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 { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock';
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 { Big } from 'big.js';
import { join } from 'path';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
};
});
jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock;
})
};
}
);
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
};
});
jest.mock(
'@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service',
() => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
ExchangeRateDataService: jest.fn().mockImplementation(() => {
return ExchangeRateDataServiceMock;
})
};
}
);
describe('PortfolioCalculator', () => {
let activityDtos: CreateOrderDto[];
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService;
let portfolioCalculatorFactory: PortfolioCalculatorFactory;
let portfolioSnapshotService: PortfolioSnapshotService;
let redisCacheService: RedisCacheService;
beforeAll(() => {
activityDtos = loadActivityExportFile(
join(__dirname, '../../../../../../../test/import/ok-btceur.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 BTCUSD buy', async () => {
jest.useFakeTimers().setSystemTime(parseDate('2022-01-14').getTime());
const activities: Activity[] = activityDtos.map((activity) => ({
...activityDummyData,
...activity,
date: parseDate(activity.date),
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD',
dataSource: activity.dataSource,
name: 'Bitcoin',
symbol: activity.symbol
}
}));
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities,
calculationType: PerformanceCalculationType.ROAI,
currency: 'EUR',
userId: userDummyData.id
});
const portfolioSnapshot = await portfolioCalculator.computeSnapshot();
const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData,
groupBy: 'month'
});
expect(portfolioSnapshot.historicalData[0]).toEqual({
date: '2021-12-11',
investmentValueWithCurrencyEffect: 0,
netPerformance: 0,
netPerformanceInPercentage: 0,
netPerformanceInPercentageWithCurrencyEffect: 0,
netPerformanceWithCurrencyEffect: 0,
netWorth: 0,
totalAccountBalance: 0,
totalInvestment: 0,
totalInvestmentValueWithCurrencyEffect: 0,
value: 0,
valueWithCurrencyEffect: 0
});
expect(portfolioSnapshot.historicalData[1]).toEqual({
date: '2021-12-12',
investmentValueWithCurrencyEffect: 39380.731596,
netPerformance: -3.892688,
netPerformanceInPercentage: 0,
netPerformanceInPercentageWithCurrencyEffect: 0,
netPerformanceWithCurrencyEffect: -3.941748,
netWorth: 39380.731596,
totalAccountBalance: 0,
totalInvestment: 38890.588976,
totalInvestmentValueWithCurrencyEffect: 39380.731596,
value: 38890.588976,
valueWithCurrencyEffect: 39380.731596
});
expect(
portfolioSnapshot.historicalData[
portfolioSnapshot.historicalData.length - 1
]
).toEqual({
date: '2022-01-14',
investmentValueWithCurrencyEffect: 0,
netPerformance: -1277.063504,
netPerformanceInPercentage: -0.032837340282712,
netPerformanceInPercentageWithCurrencyEffect: -0.044876138974002826,
netPerformanceWithCurrencyEffect: -1767.255184,
netWorth: 37617.41816,
totalAccountBalance: 0,
totalInvestment: 38890.588976,
totalInvestmentValueWithCurrencyEffect: 39380.731596,
value: 37617.41816,
valueWithCurrencyEffect: 37617.41816
});
expect(portfolioSnapshot).toMatchObject({
currentValueInBaseCurrency: new Big('37617.41816'),
errors: [],
hasErrors: false,
positions: [
{
averagePrice: new Big('44558.42'),
currency: 'USD',
dataSource: 'YAHOO',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('4.46'),
feeInBaseCurrency: new Big('3.941748'),
firstBuyDate: '2021-12-12',
grossPerformance: new Big('-1273.170816'),
grossPerformancePercentage: new Big('-0.03273724696701543726'),
grossPerformancePercentageWithCurrencyEffect: new Big(
'-0.04477604565830626119'
),
grossPerformanceWithCurrencyEffect: new Big('-1763.313436'),
investment: new Big('38890.588976'),
investmentWithCurrencyEffect: new Big('39380.731596'),
netPerformance: new Big('-1277.063504'),
netPerformancePercentage: new Big('-0.03283734028271199921'),
netPerformancePercentageWithCurrencyEffectMap: {
max: new Big('-0.04487613897400282314')
},
netPerformanceWithCurrencyEffectMap: {
max: new Big('-1767.255184')
},
marketPrice: 43099.7,
marketPriceInBaseCurrency: 37617.41816,
quantity: new Big('1'),
symbol: 'BTCUSD',
tags: [],
timeWeightedInvestment: new Big('38890.588976'),
timeWeightedInvestmentWithCurrencyEffect: new Big('39380.731596'),
transactionCount: 1,
valueInBaseCurrency: new Big('37617.41816')
}
],
totalFeesWithCurrencyEffect: new Big('3.941748'),
totalInterestWithCurrencyEffect: new Big('0'),
totalInvestment: new Big('38890.588976'),
totalInvestmentWithCurrencyEffect: new Big('39380.731596'),
totalLiabilitiesWithCurrencyEffect: new Big('0'),
totalValuablesWithCurrencyEffect: new Big('0')
});
expect(investments).toEqual([
{ date: '2021-12-12', investment: new Big('44558.42') }
]);
expect(investmentsByMonth).toEqual([
{ date: '2021-12-01', investment: 39380.731596 },
{ date: '2022-01-01', investment: 0 }
]);
});
});
});

248
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts

@ -0,0 +1,248 @@
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import {
activityDummyData,
loadActivityExportFile,
symbolProfileDummyData,
userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import {
PerformanceCalculationType,
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 { Big } from 'big.js';
import { join } from 'path';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
};
});
jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock;
})
};
}
);
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
};
});
describe('PortfolioCalculator', () => {
let activityDtos: CreateOrderDto[];
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService;
let portfolioCalculatorFactory: PortfolioCalculatorFactory;
let portfolioSnapshotService: PortfolioSnapshotService;
let redisCacheService: RedisCacheService;
beforeAll(() => {
activityDtos = loadActivityExportFile(
join(__dirname, '../../../../../../../test/import/ok-btcusd.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 BTCUSD buy', async () => {
jest.useFakeTimers().setSystemTime(parseDate('2022-01-14').getTime());
const activities: Activity[] = activityDtos.map((activity) => ({
...activityDummyData,
...activity,
date: parseDate(activity.date),
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD',
dataSource: activity.dataSource,
name: 'Bitcoin',
symbol: activity.symbol
}
}));
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities,
calculationType: PerformanceCalculationType.ROAI,
currency: 'USD',
userId: userDummyData.id
});
const portfolioSnapshot = await portfolioCalculator.computeSnapshot();
const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData,
groupBy: 'month'
});
expect(portfolioSnapshot.historicalData[0]).toEqual({
date: '2021-12-11',
investmentValueWithCurrencyEffect: 0,
netPerformance: 0,
netPerformanceInPercentage: 0,
netPerformanceInPercentageWithCurrencyEffect: 0,
netPerformanceWithCurrencyEffect: 0,
netWorth: 0,
totalAccountBalance: 0,
totalInvestment: 0,
totalInvestmentValueWithCurrencyEffect: 0,
value: 0,
valueWithCurrencyEffect: 0
});
expect(portfolioSnapshot.historicalData[1]).toEqual({
date: '2021-12-12',
investmentValueWithCurrencyEffect: 44558.42,
netPerformance: -4.46,
netPerformanceInPercentage: 0,
netPerformanceInPercentageWithCurrencyEffect: 0,
netPerformanceWithCurrencyEffect: -4.46,
netWorth: 44558.42,
totalAccountBalance: 0,
totalInvestment: 44558.42,
totalInvestmentValueWithCurrencyEffect: 44558.42,
value: 44558.42,
valueWithCurrencyEffect: 44558.42
});
expect(
portfolioSnapshot.historicalData[
portfolioSnapshot.historicalData.length - 1
]
).toEqual({
date: '2022-01-14',
investmentValueWithCurrencyEffect: 0,
netPerformance: -1463.18,
netPerformanceInPercentage: -0.032837340282712,
netPerformanceInPercentageWithCurrencyEffect: -0.032837340282712,
netPerformanceWithCurrencyEffect: -1463.18,
netWorth: 43099.7,
totalAccountBalance: 0,
totalInvestment: 44558.42,
totalInvestmentValueWithCurrencyEffect: 44558.42,
value: 43099.7,
valueWithCurrencyEffect: 43099.7
});
expect(portfolioSnapshot).toMatchObject({
currentValueInBaseCurrency: new Big('43099.7'),
errors: [],
hasErrors: false,
positions: [
{
averagePrice: new Big('44558.42'),
currency: 'USD',
dataSource: 'YAHOO',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('4.46'),
feeInBaseCurrency: new Big('4.46'),
firstBuyDate: '2021-12-12',
grossPerformance: new Big('-1458.72'),
grossPerformancePercentage: new Big('-0.03273724696701543726'),
grossPerformancePercentageWithCurrencyEffect: new Big(
'-0.03273724696701543726'
),
grossPerformanceWithCurrencyEffect: new Big('-1458.72'),
investment: new Big('44558.42'),
investmentWithCurrencyEffect: new Big('44558.42'),
netPerformance: new Big('-1463.18'),
netPerformancePercentage: new Big('-0.03283734028271199921'),
netPerformancePercentageWithCurrencyEffectMap: {
max: new Big('-0.03283734028271199921')
},
netPerformanceWithCurrencyEffectMap: {
max: new Big('-1463.18')
},
marketPrice: 43099.7,
marketPriceInBaseCurrency: 43099.7,
quantity: new Big('1'),
symbol: 'BTCUSD',
tags: [],
timeWeightedInvestment: new Big('44558.42'),
timeWeightedInvestmentWithCurrencyEffect: new Big('44558.42'),
transactionCount: 1,
valueInBaseCurrency: new Big('43099.7')
}
],
totalFeesWithCurrencyEffect: new Big('4.46'),
totalInterestWithCurrencyEffect: new Big('0'),
totalInvestment: new Big('44558.42'),
totalInvestmentWithCurrencyEffect: new Big('44558.42'),
totalLiabilitiesWithCurrencyEffect: new Big('0'),
totalValuablesWithCurrencyEffect: new Big('0')
});
expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject(
expect.objectContaining({
netPerformance: -1463.18,
netPerformanceInPercentage: -0.032837340282712,
netPerformanceInPercentageWithCurrencyEffect: -0.032837340282712,
netPerformanceWithCurrencyEffect: -1463.18,
totalInvestmentValueWithCurrencyEffect: 44558.42
})
);
expect(investments).toEqual([
{ date: '2021-12-12', investment: new Big('44558.42') }
]);
expect(investmentsByMonth).toEqual([
{ date: '2021-12-01', investment: 44558.42 },
{ date: '2022-01-01', investment: 0 }
]);
});
});
});

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

@ -47,6 +47,10 @@ function mockGetValue(symbol: string, date: Date) {
return { marketPrice: 14156.4 };
} else if (isSameDay(parseDate('2018-01-01'), date)) {
return { marketPrice: 13657.2 };
} else if (isSameDay(parseDate('2021-12-12'), date)) {
return { marketPrice: 50098.3 };
} else if (isSameDay(parseDate('2022-01-14'), date)) {
return { marketPrice: 43099.7 };
}
return { marketPrice: 0 };

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

@ -748,8 +748,14 @@ export class PortfolioService {
);
const historicalDataArray: HistoricalDataItem[] = [];
let maxPrice = Math.max(activitiesOfPosition[0].unitPrice, marketPrice);
let minPrice = Math.min(activitiesOfPosition[0].unitPrice, marketPrice);
let maxPrice = Math.max(
activitiesOfPosition[0].unitPriceInAssetProfileCurrency,
marketPrice
);
let minPrice = Math.min(
activitiesOfPosition[0].unitPriceInAssetProfileCurrency,
marketPrice
);
if (historicalData[aSymbol]) {
let j = -1;
@ -793,9 +799,9 @@ export class PortfolioService {
} else {
// Add historical entry for buy date, if no historical data available
historicalDataArray.push({
averagePrice: activitiesOfPosition[0].unitPrice,
averagePrice: activitiesOfPosition[0].unitPriceInAssetProfileCurrency,
date: firstBuyDate,
marketPrice: activitiesOfPosition[0].unitPrice,
marketPrice: activitiesOfPosition[0].unitPriceInAssetProfileCurrency,
quantity: activitiesOfPosition[0].quantity
});
}

7
apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts

@ -25,6 +25,13 @@ export const ExchangeRateDataServiceMock = {
'2023-07-10': 1
}
});
} else if (targetCurrency === 'EUR') {
return Promise.resolve({
USDEUR: {
'2021-12-12': 0.8838,
'2022-01-14': 0.8728
}
});
}
return Promise.resolve({});

2
test/import/ok-btceur.json

@ -11,9 +11,11 @@
"accountId": null,
"comment": null,
"fee": 3.94,
"feeInAssetProfileCurrency": 4.46,
"quantity": 1,
"type": "BUY",
"unitPrice": 39378.5,
"unitPriceInAssetProfileCurrency": 44558.42,
"currency": "EUR",
"dataSource": "YAHOO",
"date": "2021-12-12T00:00:00.000Z",

2
test/import/ok-btcusd.json

@ -11,9 +11,11 @@
"accountId": null,
"comment": null,
"fee": 4.46,
"feeInAssetProfileCurrency": 4.46,
"quantity": 1,
"type": "BUY",
"unitPrice": 44558.42,
"unitPriceInAssetProfileCurrency": 44558.42,
"currency": "USD",
"dataSource": "YAHOO",
"date": "2021-12-12T00:00:00.000Z",

Loading…
Cancel
Save