mirror of https://github.com/ghostfolio/ghostfolio
committed by
GitHub
25 changed files with 2831 additions and 549 deletions
@ -0,0 +1,208 @@ |
|||||
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; |
||||
|
import { |
||||
|
activityDummyData, |
||||
|
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 { 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 { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; |
||||
|
|
||||
|
import { Big } from 'big.js'; |
||||
|
|
||||
|
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 configurationService: ConfigurationService; |
||||
|
let currentRateService: CurrentRateService; |
||||
|
let exchangeRateDataService: ExchangeRateDataService; |
||||
|
let portfolioCalculatorFactory: PortfolioCalculatorFactory; |
||||
|
let portfolioSnapshotService: PortfolioSnapshotService; |
||||
|
let redisCacheService: RedisCacheService; |
||||
|
|
||||
|
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, |
||||
|
null |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
describe('get current positions', () => { |
||||
|
it.only('with GOOGL buy', async () => { |
||||
|
jest.useFakeTimers().setSystemTime(parseDate('2023-07-10').getTime()); |
||||
|
|
||||
|
const activities: Activity[] = [ |
||||
|
{ |
||||
|
...activityDummyData, |
||||
|
date: new Date('2023-01-03'), |
||||
|
feeInAssetProfileCurrency: 1, |
||||
|
quantity: 1, |
||||
|
SymbolProfile: { |
||||
|
...symbolProfileDummyData, |
||||
|
currency: 'USD', |
||||
|
dataSource: 'YAHOO', |
||||
|
name: 'Alphabet Inc.', |
||||
|
symbol: 'GOOGL' |
||||
|
}, |
||||
|
type: 'BUY', |
||||
|
unitPriceInAssetProfileCurrency: 89.12 |
||||
|
} |
||||
|
]; |
||||
|
|
||||
|
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ |
||||
|
activities, |
||||
|
calculationType: PerformanceCalculationType.ROI, |
||||
|
currency: 'CHF', |
||||
|
userId: userDummyData.id |
||||
|
}); |
||||
|
|
||||
|
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); |
||||
|
|
||||
|
const investments = portfolioCalculator.getInvestments(); |
||||
|
|
||||
|
const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ |
||||
|
data: portfolioSnapshot.historicalData, |
||||
|
groupBy: 'month' |
||||
|
}); |
||||
|
|
||||
|
expect(portfolioSnapshot).toMatchObject({ |
||||
|
currentValueInBaseCurrency: new Big('103.10483'), |
||||
|
errors: [], |
||||
|
hasErrors: false, |
||||
|
positions: [ |
||||
|
{ |
||||
|
averagePrice: new Big('89.12'), |
||||
|
currency: 'USD', |
||||
|
dataSource: 'YAHOO', |
||||
|
dividend: new Big('0'), |
||||
|
dividendInBaseCurrency: new Big('0'), |
||||
|
fee: new Big('1'), |
||||
|
feeInBaseCurrency: new Big('0.9238'), |
||||
|
firstBuyDate: '2023-01-03', |
||||
|
grossPerformance: new Big('27.33').mul(0.8854), |
||||
|
grossPerformancePercentage: new Big('0.3066651705565529623'), |
||||
|
grossPerformancePercentageWithCurrencyEffect: new Big( |
||||
|
'0.25235044599563974109' |
||||
|
), |
||||
|
grossPerformanceWithCurrencyEffect: new Big('20.775774'), |
||||
|
investment: new Big('89.12').mul(0.8854), |
||||
|
investmentWithCurrencyEffect: new Big('82.329056'), |
||||
|
netPerformance: new Big('26.33').mul(0.8854), |
||||
|
netPerformancePercentage: new Big('0.29544434470377019749'), |
||||
|
netPerformancePercentageWithCurrencyEffectMap: { |
||||
|
max: new Big('0.24112962014285697628') |
||||
|
}, |
||||
|
netPerformanceWithCurrencyEffectMap: { max: new Big('19.851974') }, |
||||
|
marketPrice: 116.45, |
||||
|
marketPriceInBaseCurrency: 103.10483, |
||||
|
quantity: new Big('1'), |
||||
|
symbol: 'GOOGL', |
||||
|
tags: [], |
||||
|
timeWeightedInvestment: new Big('89.12').mul(0.8854), |
||||
|
timeWeightedInvestmentWithCurrencyEffect: new Big('82.329056'), |
||||
|
transactionCount: 1, |
||||
|
valueInBaseCurrency: new Big('103.10483') |
||||
|
} |
||||
|
], |
||||
|
totalFeesWithCurrencyEffect: new Big('0.9238'), |
||||
|
totalInterestWithCurrencyEffect: new Big('0'), |
||||
|
totalInvestment: new Big('89.12').mul(0.8854), |
||||
|
totalInvestmentWithCurrencyEffect: new Big('82.329056'), |
||||
|
totalLiabilitiesWithCurrencyEffect: new Big('0'), |
||||
|
totalValuablesWithCurrencyEffect: new Big('0') |
||||
|
}); |
||||
|
|
||||
|
expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject( |
||||
|
expect.objectContaining({ |
||||
|
netPerformance: new Big('26.33').mul(0.8854).toNumber(), |
||||
|
netPerformanceInPercentage: 0.29544434470377019749, |
||||
|
netPerformanceInPercentageWithCurrencyEffect: 0.24112962014285697628, |
||||
|
netPerformanceWithCurrencyEffect: 19.851974, |
||||
|
totalInvestmentValueWithCurrencyEffect: 82.329056 |
||||
|
}) |
||||
|
); |
||||
|
|
||||
|
expect(investments).toEqual([ |
||||
|
{ date: '2023-01-03', investment: new Big('89.12') } |
||||
|
]); |
||||
|
|
||||
|
expect(investmentsByMonth).toEqual([ |
||||
|
{ date: '2023-01-01', investment: 82.329056 }, |
||||
|
{ date: '2023-02-01', investment: 0 }, |
||||
|
{ date: '2023-03-01', investment: 0 }, |
||||
|
{ date: '2023-04-01', investment: 0 }, |
||||
|
{ date: '2023-05-01', investment: 0 }, |
||||
|
{ date: '2023-06-01', investment: 0 }, |
||||
|
{ date: '2023-07-01', investment: 0 } |
||||
|
]); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
@ -0,0 +1,39 @@ |
|||||
|
import { SymbolMetrics } from '@ghostfolio/common/interfaces'; |
||||
|
|
||||
|
import { Big } from 'big.js'; |
||||
|
|
||||
|
import { PortfolioOrderItem } from '../../interfaces/portfolio-order-item.interface'; |
||||
|
|
||||
|
export class PortfolioCalculatorSymbolMetricsHelperObject { |
||||
|
currentExchangeRate: number; |
||||
|
endDateString: string; |
||||
|
exchangeRateAtOrderDate: number; |
||||
|
fees: Big = new Big(0); |
||||
|
feesWithCurrencyEffect: Big = new Big(0); |
||||
|
feesAtStartDate: Big = new Big(0); |
||||
|
feesAtStartDateWithCurrencyEffect: Big = new Big(0); |
||||
|
grossPerformanceAtStartDate: Big = new Big(0); |
||||
|
grossPerformanceAtStartDateWithCurrencyEffect: Big = new Big(0); |
||||
|
indexOfEndOrder: number; |
||||
|
indexOfStartOrder: number; |
||||
|
initialValue: Big; |
||||
|
initialValueWithCurrencyEffect: Big; |
||||
|
investmentAtStartDate: Big; |
||||
|
investmentAtStartDateWithCurrencyEffect: Big; |
||||
|
investmentValueBeforeTransaction: Big = new Big(0); |
||||
|
investmentValueBeforeTransactionWithCurrencyEffect: Big = new Big(0); |
||||
|
ordersByDate: { [date: string]: PortfolioOrderItem[] } = {}; |
||||
|
startDateString: string; |
||||
|
symbolMetrics: SymbolMetrics; |
||||
|
totalUnits: Big = new Big(0); |
||||
|
totalInvestmentFromBuyTransactions: Big = new Big(0); |
||||
|
totalInvestmentFromBuyTransactionsWithCurrencyEffect: Big = new Big(0); |
||||
|
totalQuantityFromBuyTransactions: Big = new Big(0); |
||||
|
totalValueOfPositionsSold: Big = new Big(0); |
||||
|
totalValueOfPositionsSoldWithCurrencyEffect: Big = new Big(0); |
||||
|
unitPrice: Big; |
||||
|
unitPriceAtEndDate: Big = new Big(0); |
||||
|
unitPriceAtStartDate: Big = new Big(0); |
||||
|
valueAtStartDate: Big = new Big(0); |
||||
|
valueAtStartDateWithCurrencyEffect: Big = new Big(0); |
||||
|
} |
@ -0,0 +1,198 @@ |
|||||
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; |
||||
|
import { |
||||
|
activityDummyData, |
||||
|
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 { 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 { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; |
||||
|
|
||||
|
import { Big } from 'big.js'; |
||||
|
|
||||
|
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 configurationService: ConfigurationService; |
||||
|
let currentRateService: CurrentRateService; |
||||
|
let exchangeRateDataService: ExchangeRateDataService; |
||||
|
let portfolioCalculatorFactory: PortfolioCalculatorFactory; |
||||
|
let portfolioSnapshotService: PortfolioSnapshotService; |
||||
|
let redisCacheService: RedisCacheService; |
||||
|
|
||||
|
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, |
||||
|
null |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
describe('get current positions', () => { |
||||
|
it.only('with MSFT buy', async () => { |
||||
|
jest.useFakeTimers().setSystemTime(parseDate('2023-07-10').getTime()); |
||||
|
|
||||
|
const activities: Activity[] = [ |
||||
|
{ |
||||
|
...activityDummyData, |
||||
|
date: new Date('2021-09-16'), |
||||
|
feeInAssetProfileCurrency: 19, |
||||
|
quantity: 1, |
||||
|
SymbolProfile: { |
||||
|
...symbolProfileDummyData, |
||||
|
currency: 'USD', |
||||
|
dataSource: 'YAHOO', |
||||
|
name: 'Microsoft Inc.', |
||||
|
symbol: 'MSFT' |
||||
|
}, |
||||
|
type: 'BUY', |
||||
|
unitPriceInAssetProfileCurrency: 298.58 |
||||
|
}, |
||||
|
{ |
||||
|
...activityDummyData, |
||||
|
date: new Date('2021-11-16'), |
||||
|
feeInAssetProfileCurrency: 0, |
||||
|
quantity: 1, |
||||
|
SymbolProfile: { |
||||
|
...symbolProfileDummyData, |
||||
|
currency: 'USD', |
||||
|
dataSource: 'YAHOO', |
||||
|
name: 'Microsoft Inc.', |
||||
|
symbol: 'MSFT' |
||||
|
}, |
||||
|
type: 'DIVIDEND', |
||||
|
unitPriceInAssetProfileCurrency: 0.62 |
||||
|
} |
||||
|
]; |
||||
|
|
||||
|
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ |
||||
|
activities, |
||||
|
calculationType: PerformanceCalculationType.ROI, |
||||
|
currency: 'USD', |
||||
|
userId: userDummyData.id |
||||
|
}); |
||||
|
|
||||
|
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); |
||||
|
|
||||
|
expect(portfolioSnapshot).toMatchObject({ |
||||
|
errors: [], |
||||
|
hasErrors: false, |
||||
|
positions: [ |
||||
|
{ |
||||
|
averagePrice: new Big('298.58'), |
||||
|
currency: 'USD', |
||||
|
dataSource: 'YAHOO', |
||||
|
dividend: new Big('0.62'), |
||||
|
dividendInBaseCurrency: new Big('0.62'), |
||||
|
fee: new Big('19'), |
||||
|
firstBuyDate: '2021-09-16', |
||||
|
grossPerformance: new Big('33.87'), |
||||
|
grossPerformancePercentage: new Big('0.11343693482483756447'), |
||||
|
grossPerformancePercentageWithCurrencyEffect: new Big( |
||||
|
'0.11343693482483756447' |
||||
|
), |
||||
|
grossPerformanceWithCurrencyEffect: new Big('33.87'), |
||||
|
investment: new Big('298.58'), |
||||
|
investmentWithCurrencyEffect: new Big('298.58'), |
||||
|
marketPrice: 331.83, |
||||
|
marketPriceInBaseCurrency: 331.83, |
||||
|
netPerformance: new Big('14.87'), |
||||
|
netPerformancePercentage: new Big('0.04980239801728180052'), |
||||
|
netPerformancePercentageWithCurrencyEffectMap: { |
||||
|
max: new Big('0.04980239801728180052') |
||||
|
}, |
||||
|
netPerformanceWithCurrencyEffectMap: { |
||||
|
'1d': new Big('-5.39'), |
||||
|
'5y': new Big('14.87'), |
||||
|
max: new Big('14.87'), |
||||
|
wtd: new Big('-5.39') |
||||
|
}, |
||||
|
quantity: new Big('1'), |
||||
|
symbol: 'MSFT', |
||||
|
tags: [], |
||||
|
transactionCount: 2 |
||||
|
} |
||||
|
], |
||||
|
totalFeesWithCurrencyEffect: new Big('19'), |
||||
|
totalInterestWithCurrencyEffect: new Big('0'), |
||||
|
totalInvestment: new Big('298.58'), |
||||
|
totalInvestmentWithCurrencyEffect: new Big('298.58'), |
||||
|
totalLiabilitiesWithCurrencyEffect: new Big('0'), |
||||
|
totalValuablesWithCurrencyEffect: new Big('0') |
||||
|
}); |
||||
|
|
||||
|
expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject( |
||||
|
expect.objectContaining({ |
||||
|
totalInvestmentValueWithCurrencyEffect: 298.58 |
||||
|
}) |
||||
|
); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
@ -0,0 +1,202 @@ |
|||||
|
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 { 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 { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; |
||||
|
|
||||
|
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-novn-buy-and-sell-partially.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, |
||||
|
null |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
describe('get current positions', () => { |
||||
|
it.only('with NOVN.SW buy and sell partially', async () => { |
||||
|
jest.useFakeTimers().setSystemTime(parseDate('2022-04-11').getTime()); |
||||
|
|
||||
|
const activities: Activity[] = activityDtos.map((activity) => ({ |
||||
|
...activityDummyData, |
||||
|
...activity, |
||||
|
date: parseDate(activity.date), |
||||
|
feeInAssetProfileCurrency: activity.fee, |
||||
|
SymbolProfile: { |
||||
|
...symbolProfileDummyData, |
||||
|
currency: activity.currency, |
||||
|
dataSource: activity.dataSource, |
||||
|
name: 'Novartis AG', |
||||
|
symbol: activity.symbol |
||||
|
}, |
||||
|
unitPriceInAssetProfileCurrency: activity.unitPrice |
||||
|
})); |
||||
|
|
||||
|
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ |
||||
|
activities, |
||||
|
calculationType: PerformanceCalculationType.ROI, |
||||
|
currency: 'CHF', |
||||
|
userId: userDummyData.id |
||||
|
}); |
||||
|
|
||||
|
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); |
||||
|
|
||||
|
const investments = portfolioCalculator.getInvestments(); |
||||
|
|
||||
|
const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ |
||||
|
data: portfolioSnapshot.historicalData, |
||||
|
groupBy: 'month' |
||||
|
}); |
||||
|
|
||||
|
expect(portfolioSnapshot).toMatchObject({ |
||||
|
currentValueInBaseCurrency: new Big('87.8'), |
||||
|
errors: [], |
||||
|
hasErrors: false, |
||||
|
positions: [ |
||||
|
{ |
||||
|
averagePrice: new Big('75.80'), |
||||
|
currency: 'CHF', |
||||
|
dataSource: 'YAHOO', |
||||
|
dividend: new Big('0'), |
||||
|
dividendInBaseCurrency: new Big('0'), |
||||
|
fee: new Big('4.25'), |
||||
|
feeInBaseCurrency: new Big('4.25'), |
||||
|
firstBuyDate: '2022-03-07', |
||||
|
grossPerformance: new Big('21.93'), |
||||
|
grossPerformancePercentage: new Big('0.14465699208443271768'), |
||||
|
grossPerformancePercentageWithCurrencyEffect: new Big( |
||||
|
'0.14465699208443271768' |
||||
|
), |
||||
|
grossPerformanceWithCurrencyEffect: new Big('21.93'), |
||||
|
investment: new Big('75.80'), |
||||
|
investmentWithCurrencyEffect: new Big('75.80'), |
||||
|
netPerformance: new Big('17.68'), |
||||
|
netPerformancePercentage: new Big('0.11662269129287598945'), |
||||
|
netPerformancePercentageWithCurrencyEffectMap: { |
||||
|
max: new Big('0.11662269129287598945') |
||||
|
}, |
||||
|
netPerformanceWithCurrencyEffectMap: { max: new Big('17.68') }, |
||||
|
marketPrice: 87.8, |
||||
|
marketPriceInBaseCurrency: 87.8, |
||||
|
quantity: new Big('1'), |
||||
|
symbol: 'NOVN.SW', |
||||
|
tags: [], |
||||
|
timeWeightedInvestment: new Big('151.6'), |
||||
|
timeWeightedInvestmentWithCurrencyEffect: new Big('151.6'), |
||||
|
transactionCount: 2, |
||||
|
valueInBaseCurrency: new Big('87.8') |
||||
|
} |
||||
|
], |
||||
|
totalFeesWithCurrencyEffect: new Big('4.25'), |
||||
|
totalInterestWithCurrencyEffect: new Big('0'), |
||||
|
totalInvestment: new Big('75.80'), |
||||
|
totalInvestmentWithCurrencyEffect: new Big('75.80'), |
||||
|
totalLiabilitiesWithCurrencyEffect: new Big('0'), |
||||
|
totalValuablesWithCurrencyEffect: new Big('0') |
||||
|
}); |
||||
|
|
||||
|
expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject( |
||||
|
expect.objectContaining({ |
||||
|
netPerformance: 17.68, |
||||
|
netPerformanceInPercentage: 0.11662269129287598945, |
||||
|
netPerformanceInPercentageWithCurrencyEffect: 0.11662269129287598945, |
||||
|
netPerformanceWithCurrencyEffect: 17.68, |
||||
|
totalInvestmentValueWithCurrencyEffect: 75.8 |
||||
|
}) |
||||
|
); |
||||
|
|
||||
|
expect(investments).toEqual([ |
||||
|
{ date: '2022-03-07', investment: new Big('151.6') }, |
||||
|
{ date: '2022-04-08', investment: new Big('75.8') } |
||||
|
]); |
||||
|
|
||||
|
expect(investmentsByMonth).toEqual([ |
||||
|
{ date: '2022-03-01', investment: 151.6 }, |
||||
|
{ date: '2022-04-01', investment: -75.8 } |
||||
|
]); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
@ -0,0 +1,259 @@ |
|||||
|
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 { 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 { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; |
||||
|
|
||||
|
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-novn-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, |
||||
|
null |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
describe('get current positions', () => { |
||||
|
it.only('with NOVN.SW buy and sell', async () => { |
||||
|
jest.useFakeTimers().setSystemTime(parseDate('2022-04-11').getTime()); |
||||
|
|
||||
|
const activities: Activity[] = activityDtos.map((activity) => ({ |
||||
|
...activityDummyData, |
||||
|
...activity, |
||||
|
date: parseDate(activity.date), |
||||
|
feeInAssetProfileCurrency: activity.fee, |
||||
|
SymbolProfile: { |
||||
|
...symbolProfileDummyData, |
||||
|
currency: activity.currency, |
||||
|
dataSource: activity.dataSource, |
||||
|
name: 'Novartis AG', |
||||
|
symbol: activity.symbol |
||||
|
}, |
||||
|
unitPriceInAssetProfileCurrency: activity.unitPrice |
||||
|
})); |
||||
|
|
||||
|
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ |
||||
|
activities, |
||||
|
calculationType: PerformanceCalculationType.ROI, |
||||
|
currency: 'CHF', |
||||
|
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: '2022-03-06', |
||||
|
investmentValueWithCurrencyEffect: 0, |
||||
|
netPerformance: 0, |
||||
|
netPerformanceInPercentage: 0, |
||||
|
netPerformanceInPercentageWithCurrencyEffect: 0, |
||||
|
netPerformanceWithCurrencyEffect: 0, |
||||
|
netWorth: 0, |
||||
|
timeWeightedPerformanceInPercentage: 0, |
||||
|
timeWeightedPerformanceInPercentageWithCurrencyEffect: 0, |
||||
|
totalAccountBalance: 0, |
||||
|
totalInvestment: 0, |
||||
|
totalInvestmentValueWithCurrencyEffect: 0, |
||||
|
value: 0, |
||||
|
valueWithCurrencyEffect: 0 |
||||
|
}); |
||||
|
|
||||
|
expect(portfolioSnapshot.historicalData[1]).toEqual({ |
||||
|
date: '2022-03-07', |
||||
|
investmentValueWithCurrencyEffect: 151.6, |
||||
|
netPerformance: 0, |
||||
|
netPerformanceInPercentage: 0, |
||||
|
netPerformanceInPercentageWithCurrencyEffect: 0, |
||||
|
netPerformanceWithCurrencyEffect: 0, |
||||
|
timeWeightedPerformanceInPercentage: 0, |
||||
|
timeWeightedPerformanceInPercentageWithCurrencyEffect: 0, |
||||
|
netWorth: 151.6, |
||||
|
totalAccountBalance: 0, |
||||
|
totalInvestment: 151.6, |
||||
|
totalInvestmentValueWithCurrencyEffect: 151.6, |
||||
|
value: 151.6, |
||||
|
valueWithCurrencyEffect: 151.6 |
||||
|
}); |
||||
|
|
||||
|
expect( |
||||
|
portfolioSnapshot.historicalData[ |
||||
|
portfolioSnapshot.historicalData.length - 1 |
||||
|
] |
||||
|
).toEqual({ |
||||
|
date: '2022-04-11', |
||||
|
investmentValueWithCurrencyEffect: 0, |
||||
|
netPerformance: 19.86, |
||||
|
netPerformanceInPercentage: 0.13100263852242744, |
||||
|
netPerformanceInPercentageWithCurrencyEffect: 0.13100263852242744, |
||||
|
timeWeightedPerformanceInPercentage: 0.13100263852242744, |
||||
|
timeWeightedPerformanceInPercentageWithCurrencyEffect: 0.13100263852242744, |
||||
|
netPerformanceWithCurrencyEffect: 19.86, |
||||
|
netWorth: 0, |
||||
|
totalAccountBalance: 0, |
||||
|
totalInvestment: 0, |
||||
|
totalInvestmentValueWithCurrencyEffect: 0, |
||||
|
value: 0, |
||||
|
valueWithCurrencyEffect: 0 |
||||
|
}); |
||||
|
|
||||
|
expect(portfolioSnapshot).toMatchObject({ |
||||
|
currentValueInBaseCurrency: new Big('0'), |
||||
|
errors: [], |
||||
|
hasErrors: false, |
||||
|
positions: [ |
||||
|
{ |
||||
|
averagePrice: new Big('0'), |
||||
|
currency: 'CHF', |
||||
|
dataSource: 'YAHOO', |
||||
|
dividend: new Big('0'), |
||||
|
dividendInBaseCurrency: new Big('0'), |
||||
|
fee: new Big('0'), |
||||
|
feeInBaseCurrency: new Big('0'), |
||||
|
firstBuyDate: '2022-03-07', |
||||
|
grossPerformance: new Big('19.86'), |
||||
|
grossPerformancePercentage: new Big('0.13100263852242744063'), |
||||
|
grossPerformancePercentageWithCurrencyEffect: new Big( |
||||
|
'0.13100263852242744063' |
||||
|
), |
||||
|
grossPerformanceWithCurrencyEffect: new Big('19.86'), |
||||
|
investment: new Big('0'), |
||||
|
investmentWithCurrencyEffect: new Big('0'), |
||||
|
netPerformance: new Big('19.86'), |
||||
|
netPerformancePercentage: new Big('0.13100263852242744063'), |
||||
|
netPerformancePercentageWithCurrencyEffectMap: { |
||||
|
max: new Big('0.13100263852242744063') |
||||
|
}, |
||||
|
netPerformanceWithCurrencyEffectMap: { |
||||
|
max: new Big('19.86') |
||||
|
}, |
||||
|
marketPrice: 87.8, |
||||
|
marketPriceInBaseCurrency: 87.8, |
||||
|
quantity: new Big('0'), |
||||
|
symbol: 'NOVN.SW', |
||||
|
tags: [], |
||||
|
timeWeightedInvestment: new Big('151.6'), |
||||
|
timeWeightedInvestmentWithCurrencyEffect: new Big('151.6'), |
||||
|
transactionCount: 2, |
||||
|
valueInBaseCurrency: new Big('0') |
||||
|
} |
||||
|
], |
||||
|
totalFeesWithCurrencyEffect: new Big('0'), |
||||
|
totalInterestWithCurrencyEffect: new Big('0'), |
||||
|
totalInvestment: new Big('0'), |
||||
|
totalInvestmentWithCurrencyEffect: new Big('0'), |
||||
|
totalLiabilitiesWithCurrencyEffect: new Big('0'), |
||||
|
totalValuablesWithCurrencyEffect: new Big('0') |
||||
|
}); |
||||
|
|
||||
|
expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject( |
||||
|
expect.objectContaining({ |
||||
|
netPerformance: 19.86, |
||||
|
netPerformanceInPercentage: 0.13100263852242744063, |
||||
|
netPerformanceInPercentageWithCurrencyEffect: 0.13100263852242744063, |
||||
|
netPerformanceWithCurrencyEffect: 19.86, |
||||
|
totalInvestmentValueWithCurrencyEffect: 0 |
||||
|
}) |
||||
|
); |
||||
|
|
||||
|
expect(investments).toEqual([ |
||||
|
{ date: '2022-03-07', investment: new Big('151.6') }, |
||||
|
{ date: '2022-04-08', investment: new Big('0') } |
||||
|
]); |
||||
|
|
||||
|
expect(investmentsByMonth).toEqual([ |
||||
|
{ date: '2022-03-01', investment: 151.6 }, |
||||
|
{ date: '2022-04-01', investment: -151.6 } |
||||
|
]); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
@ -0,0 +1,868 @@ |
|||||
|
import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; |
||||
|
import { DATE_FORMAT } from '@ghostfolio/common/helper'; |
||||
|
import { SymbolMetrics } from '@ghostfolio/common/interfaces'; |
||||
|
import { DateRangeTypes } from '@ghostfolio/common/types/date-range.type'; |
||||
|
|
||||
|
import { DataSource } from '@prisma/client'; |
||||
|
import { Big } from 'big.js'; |
||||
|
import { isBefore, addMilliseconds, format } from 'date-fns'; |
||||
|
import { sortBy } from 'lodash'; |
||||
|
|
||||
|
import { getFactor } from '../../../../helper/portfolio.helper'; |
||||
|
import { PortfolioOrderItem } from '../../interfaces/portfolio-order-item.interface'; |
||||
|
import { PortfolioCalculatorSymbolMetricsHelperObject } from './portfolio-calculator-helper-object'; |
||||
|
|
||||
|
export class RoiPortfolioCalculatorSymbolMetricsHelper { |
||||
|
private ENABLE_LOGGING: boolean; |
||||
|
private baseCurrencySuffix = 'InBaseCurrency'; |
||||
|
private chartDates: string[]; |
||||
|
private marketSymbolMap: { [date: string]: { [symbol: string]: Big } }; |
||||
|
public constructor( |
||||
|
ENABLE_LOGGING: boolean, |
||||
|
marketSymbolMap: { [date: string]: { [symbol: string]: Big } }, |
||||
|
chartDates: string[] |
||||
|
) { |
||||
|
this.ENABLE_LOGGING = ENABLE_LOGGING; |
||||
|
this.marketSymbolMap = marketSymbolMap; |
||||
|
this.chartDates = chartDates; |
||||
|
} |
||||
|
|
||||
|
public calculateNetPerformanceByDateRange( |
||||
|
start: Date, |
||||
|
symbolMetricsHelper: PortfolioCalculatorSymbolMetricsHelperObject |
||||
|
) { |
||||
|
for (const dateRange of DateRangeTypes) { |
||||
|
const dateInterval = getIntervalFromDateRange(dateRange); |
||||
|
const endDate = dateInterval.endDate; |
||||
|
let startDate = dateInterval.startDate; |
||||
|
|
||||
|
if (isBefore(startDate, start)) { |
||||
|
startDate = start; |
||||
|
} |
||||
|
|
||||
|
const rangeEndDateString = format(endDate, DATE_FORMAT); |
||||
|
const rangeStartDateString = format(startDate, DATE_FORMAT); |
||||
|
|
||||
|
symbolMetricsHelper.symbolMetrics.netPerformanceWithCurrencyEffectMap[ |
||||
|
dateRange |
||||
|
] = |
||||
|
symbolMetricsHelper.symbolMetrics.netPerformanceValuesWithCurrencyEffect[ |
||||
|
rangeEndDateString |
||||
|
]?.minus( |
||||
|
// If the date range is 'max', take 0 as a start value. Otherwise,
|
||||
|
// the value of the end of the day of the start date is taken which
|
||||
|
// differs from the buying price.
|
||||
|
dateRange === 'max' |
||||
|
? new Big(0) |
||||
|
: (symbolMetricsHelper.symbolMetrics |
||||
|
.netPerformanceValuesWithCurrencyEffect[rangeStartDateString] ?? |
||||
|
new Big(0)) |
||||
|
) ?? new Big(0); |
||||
|
|
||||
|
let investmentBasis = |
||||
|
symbolMetricsHelper.symbolMetrics.currentValuesWithCurrencyEffect[ |
||||
|
rangeStartDateString |
||||
|
]; |
||||
|
|
||||
|
if ( |
||||
|
!symbolMetricsHelper.symbolMetrics.currentValuesWithCurrencyEffect[ |
||||
|
rangeStartDateString |
||||
|
]?.gt(0) |
||||
|
) { |
||||
|
investmentBasis = |
||||
|
symbolMetricsHelper.symbolMetrics |
||||
|
.timeWeightedInvestmentValuesWithCurrencyEffect[rangeEndDateString]; |
||||
|
} |
||||
|
|
||||
|
symbolMetricsHelper.symbolMetrics.netPerformancePercentageWithCurrencyEffectMap[ |
||||
|
dateRange |
||||
|
] = investmentBasis.gt(0) |
||||
|
? symbolMetricsHelper.symbolMetrics.netPerformanceWithCurrencyEffectMap[ |
||||
|
dateRange |
||||
|
].div(investmentBasis) |
||||
|
: new Big(0); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public handleOverallPerformanceCalculation( |
||||
|
symbolMetricsHelper: PortfolioCalculatorSymbolMetricsHelperObject |
||||
|
) { |
||||
|
symbolMetricsHelper.symbolMetrics.grossPerformance = |
||||
|
symbolMetricsHelper.symbolMetrics.grossPerformance.minus( |
||||
|
symbolMetricsHelper.grossPerformanceAtStartDate |
||||
|
); |
||||
|
symbolMetricsHelper.symbolMetrics.grossPerformanceWithCurrencyEffect = |
||||
|
symbolMetricsHelper.symbolMetrics.grossPerformanceWithCurrencyEffect.minus( |
||||
|
symbolMetricsHelper.grossPerformanceAtStartDateWithCurrencyEffect |
||||
|
); |
||||
|
|
||||
|
symbolMetricsHelper.symbolMetrics.netPerformance = |
||||
|
symbolMetricsHelper.symbolMetrics.grossPerformance.minus( |
||||
|
symbolMetricsHelper.fees.minus(symbolMetricsHelper.feesAtStartDate) |
||||
|
); |
||||
|
|
||||
|
symbolMetricsHelper.symbolMetrics.timeWeightedInvestment = new Big( |
||||
|
symbolMetricsHelper.totalInvestmentFromBuyTransactions |
||||
|
); |
||||
|
symbolMetricsHelper.symbolMetrics.timeWeightedInvestmentWithCurrencyEffect = |
||||
|
new Big( |
||||
|
symbolMetricsHelper.totalInvestmentFromBuyTransactionsWithCurrencyEffect |
||||
|
); |
||||
|
|
||||
|
if (symbolMetricsHelper.symbolMetrics.timeWeightedInvestment.gt(0)) { |
||||
|
symbolMetricsHelper.symbolMetrics.netPerformancePercentage = |
||||
|
symbolMetricsHelper.symbolMetrics.netPerformance.div( |
||||
|
symbolMetricsHelper.symbolMetrics.timeWeightedInvestment |
||||
|
); |
||||
|
symbolMetricsHelper.symbolMetrics.grossPerformancePercentage = |
||||
|
symbolMetricsHelper.symbolMetrics.grossPerformance.div( |
||||
|
symbolMetricsHelper.symbolMetrics.timeWeightedInvestment |
||||
|
); |
||||
|
symbolMetricsHelper.symbolMetrics.grossPerformancePercentageWithCurrencyEffect = |
||||
|
symbolMetricsHelper.symbolMetrics.grossPerformanceWithCurrencyEffect.div( |
||||
|
symbolMetricsHelper.symbolMetrics |
||||
|
.timeWeightedInvestmentWithCurrencyEffect |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public processOrderMetrics( |
||||
|
orders: PortfolioOrderItem[], |
||||
|
i: number, |
||||
|
exchangeRates: { [dateString: string]: number }, |
||||
|
symbolMetricsHelper: PortfolioCalculatorSymbolMetricsHelperObject |
||||
|
) { |
||||
|
const order = orders[i]; |
||||
|
this.writeOrderToLogIfNecessary(i, order); |
||||
|
|
||||
|
symbolMetricsHelper.exchangeRateAtOrderDate = exchangeRates[order.date]; |
||||
|
const value = order.quantity.gt(0) |
||||
|
? order.quantity.mul(order.unitPrice) |
||||
|
: new Big(0); |
||||
|
|
||||
|
this.handleNoneBuyAndSellOrders(order, value, symbolMetricsHelper); |
||||
|
this.handleStartOrder( |
||||
|
order, |
||||
|
i, |
||||
|
orders, |
||||
|
symbolMetricsHelper.unitPriceAtStartDate |
||||
|
); |
||||
|
this.handleOrderFee(order, symbolMetricsHelper); |
||||
|
symbolMetricsHelper.unitPrice = this.getUnitPriceAndFillCurrencyDeviations( |
||||
|
order, |
||||
|
symbolMetricsHelper |
||||
|
); |
||||
|
|
||||
|
if (order.unitPriceInBaseCurrency) { |
||||
|
symbolMetricsHelper.investmentValueBeforeTransaction = |
||||
|
symbolMetricsHelper.totalUnits.mul(order.unitPriceInBaseCurrency); |
||||
|
symbolMetricsHelper.investmentValueBeforeTransactionWithCurrencyEffect = |
||||
|
symbolMetricsHelper.totalUnits.mul( |
||||
|
order.unitPriceInBaseCurrencyWithCurrencyEffect |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
this.handleInitialInvestmentValues(symbolMetricsHelper, i, order); |
||||
|
|
||||
|
const { transactionInvestment, transactionInvestmentWithCurrencyEffect } = |
||||
|
this.handleBuyAndSellTranscation(order, symbolMetricsHelper); |
||||
|
|
||||
|
this.logTransactionValuesIfRequested( |
||||
|
order, |
||||
|
transactionInvestment, |
||||
|
transactionInvestmentWithCurrencyEffect |
||||
|
); |
||||
|
|
||||
|
this.updateTotalInvestments( |
||||
|
symbolMetricsHelper, |
||||
|
transactionInvestment, |
||||
|
transactionInvestmentWithCurrencyEffect |
||||
|
); |
||||
|
|
||||
|
this.setInitialValueIfNecessary( |
||||
|
symbolMetricsHelper, |
||||
|
transactionInvestment, |
||||
|
transactionInvestmentWithCurrencyEffect |
||||
|
); |
||||
|
|
||||
|
this.accumulateFees(symbolMetricsHelper, order); |
||||
|
|
||||
|
symbolMetricsHelper.totalUnits = symbolMetricsHelper.totalUnits.plus( |
||||
|
order.quantity.mul(getFactor(order.type)) |
||||
|
); |
||||
|
|
||||
|
this.fillOrderUnitPricesIfMissing(order, symbolMetricsHelper); |
||||
|
|
||||
|
const valueOfInvestment = symbolMetricsHelper.totalUnits.mul( |
||||
|
order.unitPriceInBaseCurrency |
||||
|
); |
||||
|
|
||||
|
const valueOfInvestmentWithCurrencyEffect = |
||||
|
symbolMetricsHelper.totalUnits.mul( |
||||
|
order.unitPriceInBaseCurrencyWithCurrencyEffect |
||||
|
); |
||||
|
|
||||
|
const valueOfPositionsSold = |
||||
|
order.type === 'SELL' |
||||
|
? order.unitPriceInBaseCurrency.mul(order.quantity) |
||||
|
: new Big(0); |
||||
|
|
||||
|
const valueOfPositionsSoldWithCurrencyEffect = |
||||
|
order.type === 'SELL' |
||||
|
? order.unitPriceInBaseCurrencyWithCurrencyEffect.mul(order.quantity) |
||||
|
: new Big(0); |
||||
|
|
||||
|
symbolMetricsHelper.totalValueOfPositionsSold = |
||||
|
symbolMetricsHelper.totalValueOfPositionsSold.plus(valueOfPositionsSold); |
||||
|
symbolMetricsHelper.totalValueOfPositionsSoldWithCurrencyEffect = |
||||
|
symbolMetricsHelper.totalValueOfPositionsSoldWithCurrencyEffect.plus( |
||||
|
valueOfPositionsSoldWithCurrencyEffect |
||||
|
); |
||||
|
|
||||
|
this.handlePerformanceCalculation( |
||||
|
valueOfInvestment, |
||||
|
symbolMetricsHelper, |
||||
|
valueOfInvestmentWithCurrencyEffect, |
||||
|
order |
||||
|
); |
||||
|
|
||||
|
symbolMetricsHelper.symbolMetrics.investmentValuesAccumulated[order.date] = |
||||
|
new Big(symbolMetricsHelper.symbolMetrics.totalInvestment.toNumber()); |
||||
|
|
||||
|
symbolMetricsHelper.symbolMetrics.investmentValuesAccumulatedWithCurrencyEffect[ |
||||
|
order.date |
||||
|
] = new Big( |
||||
|
symbolMetricsHelper.symbolMetrics.totalInvestmentWithCurrencyEffect.toNumber() |
||||
|
); |
||||
|
|
||||
|
symbolMetricsHelper.symbolMetrics.investmentValuesWithCurrencyEffect[ |
||||
|
order.date |
||||
|
] = ( |
||||
|
symbolMetricsHelper.symbolMetrics.investmentValuesWithCurrencyEffect[ |
||||
|
order.date |
||||
|
] ?? new Big(0) |
||||
|
).add(transactionInvestmentWithCurrencyEffect); |
||||
|
} |
||||
|
|
||||
|
public handlePerformanceCalculation( |
||||
|
valueOfInvestment: Big, |
||||
|
symbolMetricsHelper: PortfolioCalculatorSymbolMetricsHelperObject, |
||||
|
valueOfInvestmentWithCurrencyEffect: Big, |
||||
|
order: PortfolioOrderItem |
||||
|
) { |
||||
|
this.calculateGrossPerformance( |
||||
|
valueOfInvestment, |
||||
|
symbolMetricsHelper, |
||||
|
valueOfInvestmentWithCurrencyEffect |
||||
|
); |
||||
|
|
||||
|
this.calculateNetPerformance( |
||||
|
symbolMetricsHelper, |
||||
|
order, |
||||
|
valueOfInvestment, |
||||
|
valueOfInvestmentWithCurrencyEffect |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
public calculateNetPerformance( |
||||
|
symbolMetricsHelper: PortfolioCalculatorSymbolMetricsHelperObject, |
||||
|
order: PortfolioOrderItem, |
||||
|
valueOfInvestment: Big, |
||||
|
valueOfInvestmentWithCurrencyEffect: Big |
||||
|
) { |
||||
|
symbolMetricsHelper.symbolMetrics.currentValues[order.date] = new Big( |
||||
|
valueOfInvestment |
||||
|
); |
||||
|
symbolMetricsHelper.symbolMetrics.currentValuesWithCurrencyEffect[ |
||||
|
order.date |
||||
|
] = new Big(valueOfInvestmentWithCurrencyEffect); |
||||
|
|
||||
|
symbolMetricsHelper.symbolMetrics.timeWeightedInvestmentValues[order.date] = |
||||
|
new Big(symbolMetricsHelper.totalInvestmentFromBuyTransactions); |
||||
|
symbolMetricsHelper.symbolMetrics.timeWeightedInvestmentValuesWithCurrencyEffect[ |
||||
|
order.date |
||||
|
] = new Big( |
||||
|
symbolMetricsHelper.totalInvestmentFromBuyTransactionsWithCurrencyEffect |
||||
|
); |
||||
|
|
||||
|
symbolMetricsHelper.symbolMetrics.netPerformanceValues[order.date] = |
||||
|
symbolMetricsHelper.symbolMetrics.grossPerformance |
||||
|
.minus(symbolMetricsHelper.grossPerformanceAtStartDate) |
||||
|
.minus( |
||||
|
symbolMetricsHelper.fees.minus(symbolMetricsHelper.feesAtStartDate) |
||||
|
); |
||||
|
|
||||
|
symbolMetricsHelper.symbolMetrics.netPerformanceValuesWithCurrencyEffect[ |
||||
|
order.date |
||||
|
] = symbolMetricsHelper.symbolMetrics.grossPerformanceWithCurrencyEffect |
||||
|
.minus(symbolMetricsHelper.grossPerformanceAtStartDateWithCurrencyEffect) |
||||
|
.minus( |
||||
|
symbolMetricsHelper.feesWithCurrencyEffect.minus( |
||||
|
symbolMetricsHelper.feesAtStartDateWithCurrencyEffect |
||||
|
) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
public calculateGrossPerformance( |
||||
|
valueOfInvestment: Big, |
||||
|
symbolMetricsHelper: PortfolioCalculatorSymbolMetricsHelperObject, |
||||
|
valueOfInvestmentWithCurrencyEffect: Big |
||||
|
) { |
||||
|
const newGrossPerformance = valueOfInvestment |
||||
|
.minus(symbolMetricsHelper.totalInvestmentFromBuyTransactions) |
||||
|
.plus(symbolMetricsHelper.totalValueOfPositionsSold) |
||||
|
.plus( |
||||
|
symbolMetricsHelper.symbolMetrics.totalDividend.mul( |
||||
|
symbolMetricsHelper.currentExchangeRate |
||||
|
) |
||||
|
) |
||||
|
.plus( |
||||
|
symbolMetricsHelper.symbolMetrics.totalInterest.mul( |
||||
|
symbolMetricsHelper.currentExchangeRate |
||||
|
) |
||||
|
); |
||||
|
|
||||
|
const newGrossPerformanceWithCurrencyEffect = |
||||
|
valueOfInvestmentWithCurrencyEffect |
||||
|
.minus( |
||||
|
symbolMetricsHelper.totalInvestmentFromBuyTransactionsWithCurrencyEffect |
||||
|
) |
||||
|
.plus(symbolMetricsHelper.totalValueOfPositionsSoldWithCurrencyEffect) |
||||
|
.plus(symbolMetricsHelper.symbolMetrics.totalDividendInBaseCurrency) |
||||
|
.plus(symbolMetricsHelper.symbolMetrics.totalInterestInBaseCurrency); |
||||
|
|
||||
|
symbolMetricsHelper.symbolMetrics.grossPerformance = newGrossPerformance; |
||||
|
symbolMetricsHelper.symbolMetrics.grossPerformanceWithCurrencyEffect = |
||||
|
newGrossPerformanceWithCurrencyEffect; |
||||
|
} |
||||
|
|
||||
|
public accumulateFees( |
||||
|
symbolMetricsHelper: PortfolioCalculatorSymbolMetricsHelperObject, |
||||
|
order: PortfolioOrderItem |
||||
|
) { |
||||
|
symbolMetricsHelper.fees = symbolMetricsHelper.fees.plus( |
||||
|
order.feeInBaseCurrency ?? 0 |
||||
|
); |
||||
|
|
||||
|
symbolMetricsHelper.feesWithCurrencyEffect = |
||||
|
symbolMetricsHelper.feesWithCurrencyEffect.plus( |
||||
|
order.feeInBaseCurrencyWithCurrencyEffect ?? 0 |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
public updateTotalInvestments( |
||||
|
symbolMetricsHelper: PortfolioCalculatorSymbolMetricsHelperObject, |
||||
|
transactionInvestment: Big, |
||||
|
transactionInvestmentWithCurrencyEffect: Big |
||||
|
) { |
||||
|
symbolMetricsHelper.symbolMetrics.totalInvestment = |
||||
|
symbolMetricsHelper.symbolMetrics.totalInvestment.plus( |
||||
|
transactionInvestment |
||||
|
); |
||||
|
|
||||
|
symbolMetricsHelper.symbolMetrics.totalInvestmentWithCurrencyEffect = |
||||
|
symbolMetricsHelper.symbolMetrics.totalInvestmentWithCurrencyEffect.plus( |
||||
|
transactionInvestmentWithCurrencyEffect |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
public setInitialValueIfNecessary( |
||||
|
symbolMetricsHelper: PortfolioCalculatorSymbolMetricsHelperObject, |
||||
|
transactionInvestment: Big, |
||||
|
transactionInvestmentWithCurrencyEffect: Big |
||||
|
) { |
||||
|
if (!symbolMetricsHelper.initialValue && transactionInvestment.gt(0)) { |
||||
|
symbolMetricsHelper.initialValue = transactionInvestment; |
||||
|
symbolMetricsHelper.initialValueWithCurrencyEffect = |
||||
|
transactionInvestmentWithCurrencyEffect; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public logTransactionValuesIfRequested( |
||||
|
order: PortfolioOrderItem, |
||||
|
transactionInvestment: Big, |
||||
|
transactionInvestmentWithCurrencyEffect: Big |
||||
|
) { |
||||
|
if (this.ENABLE_LOGGING) { |
||||
|
console.log('order.quantity', order.quantity.toNumber()); |
||||
|
console.log('transactionInvestment', transactionInvestment.toNumber()); |
||||
|
|
||||
|
console.log( |
||||
|
'transactionInvestmentWithCurrencyEffect', |
||||
|
transactionInvestmentWithCurrencyEffect.toNumber() |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public handleBuyAndSellTranscation( |
||||
|
order: PortfolioOrderItem, |
||||
|
symbolMetricsHelper: PortfolioCalculatorSymbolMetricsHelperObject |
||||
|
) { |
||||
|
switch (order.type) { |
||||
|
case 'BUY': |
||||
|
return this.handleBuyTransaction(order, symbolMetricsHelper); |
||||
|
case 'SELL': |
||||
|
return this.handleSellTransaction(symbolMetricsHelper, order); |
||||
|
default: |
||||
|
return { |
||||
|
transactionInvestment: new Big(0), |
||||
|
transactionInvestmentWithCurrencyEffect: new Big(0) |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public handleSellTransaction( |
||||
|
symbolMetricsHelper: PortfolioCalculatorSymbolMetricsHelperObject, |
||||
|
order: PortfolioOrderItem |
||||
|
) { |
||||
|
let transactionInvestment = new Big(0); |
||||
|
let transactionInvestmentWithCurrencyEffect = new Big(0); |
||||
|
if (symbolMetricsHelper.totalUnits.gt(0)) { |
||||
|
transactionInvestment = symbolMetricsHelper.symbolMetrics.totalInvestment |
||||
|
.div(symbolMetricsHelper.totalUnits) |
||||
|
.mul(order.quantity) |
||||
|
.mul(getFactor(order.type)); |
||||
|
transactionInvestmentWithCurrencyEffect = |
||||
|
symbolMetricsHelper.symbolMetrics.totalInvestmentWithCurrencyEffect |
||||
|
.div(symbolMetricsHelper.totalUnits) |
||||
|
.mul(order.quantity) |
||||
|
.mul(getFactor(order.type)); |
||||
|
} |
||||
|
return { transactionInvestment, transactionInvestmentWithCurrencyEffect }; |
||||
|
} |
||||
|
|
||||
|
public handleBuyTransaction( |
||||
|
order: PortfolioOrderItem, |
||||
|
symbolMetricsHelper: PortfolioCalculatorSymbolMetricsHelperObject |
||||
|
) { |
||||
|
const transactionInvestment = order.quantity |
||||
|
.mul(order.unitPriceInBaseCurrency) |
||||
|
.mul(getFactor(order.type)); |
||||
|
|
||||
|
const transactionInvestmentWithCurrencyEffect = order.quantity |
||||
|
.mul(order.unitPriceInBaseCurrencyWithCurrencyEffect) |
||||
|
.mul(getFactor(order.type)); |
||||
|
|
||||
|
symbolMetricsHelper.totalQuantityFromBuyTransactions = |
||||
|
symbolMetricsHelper.totalQuantityFromBuyTransactions.plus(order.quantity); |
||||
|
|
||||
|
symbolMetricsHelper.totalInvestmentFromBuyTransactions = |
||||
|
symbolMetricsHelper.totalInvestmentFromBuyTransactions.plus( |
||||
|
transactionInvestment |
||||
|
); |
||||
|
|
||||
|
symbolMetricsHelper.totalInvestmentFromBuyTransactionsWithCurrencyEffect = |
||||
|
symbolMetricsHelper.totalInvestmentFromBuyTransactionsWithCurrencyEffect.plus( |
||||
|
transactionInvestmentWithCurrencyEffect |
||||
|
); |
||||
|
return { transactionInvestment, transactionInvestmentWithCurrencyEffect }; |
||||
|
} |
||||
|
|
||||
|
public handleInitialInvestmentValues( |
||||
|
symbolMetricsHelper: PortfolioCalculatorSymbolMetricsHelperObject, |
||||
|
i: number, |
||||
|
order: PortfolioOrderItem |
||||
|
) { |
||||
|
if ( |
||||
|
!symbolMetricsHelper.investmentAtStartDate && |
||||
|
i >= symbolMetricsHelper.indexOfStartOrder |
||||
|
) { |
||||
|
symbolMetricsHelper.investmentAtStartDate = new Big( |
||||
|
symbolMetricsHelper.symbolMetrics.totalInvestment.toNumber() |
||||
|
); |
||||
|
symbolMetricsHelper.investmentAtStartDateWithCurrencyEffect = new Big( |
||||
|
symbolMetricsHelper.symbolMetrics.totalInvestmentWithCurrencyEffect.toNumber() |
||||
|
); |
||||
|
|
||||
|
symbolMetricsHelper.valueAtStartDate = new Big( |
||||
|
symbolMetricsHelper.investmentValueBeforeTransaction.toNumber() |
||||
|
); |
||||
|
|
||||
|
symbolMetricsHelper.valueAtStartDateWithCurrencyEffect = new Big( |
||||
|
symbolMetricsHelper.investmentValueBeforeTransactionWithCurrencyEffect.toNumber() |
||||
|
); |
||||
|
} |
||||
|
if (order.itemType === 'start') { |
||||
|
symbolMetricsHelper.feesAtStartDate = symbolMetricsHelper.fees; |
||||
|
symbolMetricsHelper.feesAtStartDateWithCurrencyEffect = |
||||
|
symbolMetricsHelper.feesWithCurrencyEffect; |
||||
|
symbolMetricsHelper.grossPerformanceAtStartDate = |
||||
|
symbolMetricsHelper.symbolMetrics.grossPerformance; |
||||
|
|
||||
|
symbolMetricsHelper.grossPerformanceAtStartDateWithCurrencyEffect = |
||||
|
symbolMetricsHelper.symbolMetrics.grossPerformanceWithCurrencyEffect; |
||||
|
} |
||||
|
|
||||
|
if ( |
||||
|
i >= symbolMetricsHelper.indexOfStartOrder && |
||||
|
!symbolMetricsHelper.initialValue |
||||
|
) { |
||||
|
if ( |
||||
|
i === symbolMetricsHelper.indexOfStartOrder && |
||||
|
!symbolMetricsHelper.symbolMetrics.totalInvestment.eq(0) |
||||
|
) { |
||||
|
symbolMetricsHelper.initialValue = new Big( |
||||
|
symbolMetricsHelper.symbolMetrics.totalInvestment.toNumber() |
||||
|
); |
||||
|
|
||||
|
symbolMetricsHelper.initialValueWithCurrencyEffect = new Big( |
||||
|
symbolMetricsHelper.symbolMetrics.totalInvestmentWithCurrencyEffect.toNumber() |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public getSymbolMetricHelperObject( |
||||
|
exchangeRates: { [dateString: string]: number }, |
||||
|
start: Date, |
||||
|
end: Date, |
||||
|
marketSymbolMap: { [date: string]: { [symbol: string]: Big } }, |
||||
|
symbol: string |
||||
|
): PortfolioCalculatorSymbolMetricsHelperObject { |
||||
|
const symbolMetricsHelper = |
||||
|
new PortfolioCalculatorSymbolMetricsHelperObject(); |
||||
|
symbolMetricsHelper.symbolMetrics = this.createEmptySymbolMetrics(); |
||||
|
symbolMetricsHelper.currentExchangeRate = |
||||
|
exchangeRates[format(new Date(), DATE_FORMAT)]; |
||||
|
symbolMetricsHelper.startDateString = format(start, DATE_FORMAT); |
||||
|
symbolMetricsHelper.endDateString = format(end, DATE_FORMAT); |
||||
|
symbolMetricsHelper.unitPriceAtStartDate = |
||||
|
marketSymbolMap[symbolMetricsHelper.startDateString]?.[symbol]; |
||||
|
symbolMetricsHelper.unitPriceAtEndDate = |
||||
|
marketSymbolMap[symbolMetricsHelper.endDateString]?.[symbol]; |
||||
|
|
||||
|
symbolMetricsHelper.totalUnits = new Big(0); |
||||
|
|
||||
|
return symbolMetricsHelper; |
||||
|
} |
||||
|
|
||||
|
public getUnitPriceAndFillCurrencyDeviations( |
||||
|
order: PortfolioOrderItem, |
||||
|
symbolMetricsHelper: PortfolioCalculatorSymbolMetricsHelperObject |
||||
|
) { |
||||
|
const unitprice = ['BUY', 'SELL'].includes(order.type) |
||||
|
? order.unitPrice |
||||
|
: order.unitPriceFromMarketData; |
||||
|
if (unitprice) { |
||||
|
order.unitPriceInBaseCurrency = unitprice.mul( |
||||
|
symbolMetricsHelper.currentExchangeRate ?? 1 |
||||
|
); |
||||
|
|
||||
|
order.unitPriceInBaseCurrencyWithCurrencyEffect = unitprice.mul( |
||||
|
symbolMetricsHelper.exchangeRateAtOrderDate ?? 1 |
||||
|
); |
||||
|
} |
||||
|
return unitprice; |
||||
|
} |
||||
|
|
||||
|
public handleOrderFee( |
||||
|
order: PortfolioOrderItem, |
||||
|
symbolMetricsHelper: PortfolioCalculatorSymbolMetricsHelperObject |
||||
|
) { |
||||
|
if (order.fee) { |
||||
|
order.feeInBaseCurrency = order.fee.mul( |
||||
|
symbolMetricsHelper.currentExchangeRate ?? 1 |
||||
|
); |
||||
|
order.feeInBaseCurrencyWithCurrencyEffect = order.fee.mul( |
||||
|
symbolMetricsHelper.exchangeRateAtOrderDate ?? 1 |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public handleStartOrder( |
||||
|
order: PortfolioOrderItem, |
||||
|
i: number, |
||||
|
orders: PortfolioOrderItem[], |
||||
|
unitPriceAtStartDate: Big.Big |
||||
|
) { |
||||
|
if (order.itemType === 'start') { |
||||
|
// Take the unit price of the order as the market price if there are no
|
||||
|
// orders of this symbol before the start date
|
||||
|
order.unitPrice = |
||||
|
i === 0 ? orders[i + 1]?.unitPrice : unitPriceAtStartDate; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public handleNoneBuyAndSellOrders( |
||||
|
order: PortfolioOrderItem, |
||||
|
value: Big.Big, |
||||
|
symbolMetricsHelper: PortfolioCalculatorSymbolMetricsHelperObject |
||||
|
) { |
||||
|
const symbolMetricsKey = this.getSymbolMetricsKeyFromOrderType(order.type); |
||||
|
if (symbolMetricsKey) { |
||||
|
this.calculateMetrics(value, symbolMetricsHelper, symbolMetricsKey); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public getSymbolMetricsKeyFromOrderType( |
||||
|
orderType: PortfolioOrderItem['type'] |
||||
|
): keyof SymbolMetrics { |
||||
|
switch (orderType) { |
||||
|
case 'DIVIDEND': |
||||
|
return 'totalDividend'; |
||||
|
case 'INTEREST': |
||||
|
return 'totalInterest'; |
||||
|
case 'ITEM': |
||||
|
return 'totalValuables'; |
||||
|
case 'LIABILITY': |
||||
|
return 'totalLiabilities'; |
||||
|
default: |
||||
|
return undefined; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public calculateMetrics( |
||||
|
value: Big, |
||||
|
symbolMetricsHelper: PortfolioCalculatorSymbolMetricsHelperObject, |
||||
|
key: keyof SymbolMetrics |
||||
|
) { |
||||
|
const stringKey = key.toString(); |
||||
|
symbolMetricsHelper.symbolMetrics[stringKey] = ( |
||||
|
symbolMetricsHelper.symbolMetrics[stringKey] as Big |
||||
|
).plus(value); |
||||
|
|
||||
|
if ( |
||||
|
Object.keys(symbolMetricsHelper.symbolMetrics).includes( |
||||
|
stringKey + this.baseCurrencySuffix |
||||
|
) |
||||
|
) { |
||||
|
symbolMetricsHelper.symbolMetrics[stringKey + this.baseCurrencySuffix] = ( |
||||
|
symbolMetricsHelper.symbolMetrics[ |
||||
|
stringKey + this.baseCurrencySuffix |
||||
|
] as Big |
||||
|
).plus(value.mul(symbolMetricsHelper.exchangeRateAtOrderDate ?? 1)); |
||||
|
} else { |
||||
|
throw new Error( |
||||
|
`Key ${stringKey + this.baseCurrencySuffix} not found in symbolMetrics` |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public writeOrderToLogIfNecessary(i: number, order: PortfolioOrderItem) { |
||||
|
if (this.ENABLE_LOGGING) { |
||||
|
console.log(); |
||||
|
console.log(); |
||||
|
console.log( |
||||
|
i + 1, |
||||
|
order.date, |
||||
|
order.type, |
||||
|
order.itemType ? `(${order.itemType})` : '' |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public fillOrdersAndSortByTime( |
||||
|
orders: PortfolioOrderItem[], |
||||
|
symbolMetricsHelper: PortfolioCalculatorSymbolMetricsHelperObject, |
||||
|
chartDateMap: { [date: string]: boolean }, |
||||
|
marketSymbolMap: { [date: string]: { [symbol: string]: Big.Big } }, |
||||
|
symbol: string, |
||||
|
dataSource: DataSource |
||||
|
) { |
||||
|
this.fillOrdersByDate(orders, symbolMetricsHelper.ordersByDate); |
||||
|
|
||||
|
this.chartDates ??= Object.keys(chartDateMap).sort(); |
||||
|
|
||||
|
this.fillOrdersWithDatesFromChartDate( |
||||
|
symbolMetricsHelper, |
||||
|
marketSymbolMap, |
||||
|
symbol, |
||||
|
orders, |
||||
|
dataSource |
||||
|
); |
||||
|
|
||||
|
// Sort orders so that the start and end placeholder order are at the correct
|
||||
|
// position
|
||||
|
orders = this.sortOrdersByTime(orders); |
||||
|
return orders; |
||||
|
} |
||||
|
|
||||
|
public sortOrdersByTime(orders: PortfolioOrderItem[]) { |
||||
|
orders = sortBy(orders, ({ date, itemType }) => { |
||||
|
let sortIndex = new Date(date); |
||||
|
|
||||
|
if (itemType === 'end') { |
||||
|
sortIndex = addMilliseconds(sortIndex, 1); |
||||
|
} else if (itemType === 'start') { |
||||
|
sortIndex = addMilliseconds(sortIndex, -1); |
||||
|
} |
||||
|
|
||||
|
return sortIndex.getTime(); |
||||
|
}); |
||||
|
return orders; |
||||
|
} |
||||
|
|
||||
|
public fillOrdersWithDatesFromChartDate( |
||||
|
symbolMetricsHelper: PortfolioCalculatorSymbolMetricsHelperObject, |
||||
|
marketSymbolMap: { [date: string]: { [symbol: string]: Big.Big } }, |
||||
|
symbol: string, |
||||
|
orders: PortfolioOrderItem[], |
||||
|
dataSource: DataSource |
||||
|
) { |
||||
|
let lastUnitPrice: Big; |
||||
|
for (const dateString of this.chartDates) { |
||||
|
if (dateString < symbolMetricsHelper.startDateString) { |
||||
|
continue; |
||||
|
} else if (dateString > symbolMetricsHelper.endDateString) { |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
if (symbolMetricsHelper.ordersByDate[dateString]?.length > 0) { |
||||
|
for (const order of symbolMetricsHelper.ordersByDate[dateString]) { |
||||
|
order.unitPriceFromMarketData = |
||||
|
marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice; |
||||
|
} |
||||
|
} else { |
||||
|
orders.push( |
||||
|
this.getFakeOrder( |
||||
|
dateString, |
||||
|
dataSource, |
||||
|
symbol, |
||||
|
marketSymbolMap, |
||||
|
lastUnitPrice |
||||
|
) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
const lastOrder = orders.at(-1); |
||||
|
|
||||
|
lastUnitPrice = lastOrder.unitPriceFromMarketData ?? lastOrder.unitPrice; |
||||
|
} |
||||
|
return lastUnitPrice; |
||||
|
} |
||||
|
|
||||
|
public getFakeOrder( |
||||
|
dateString: string, |
||||
|
dataSource: DataSource, |
||||
|
symbol: string, |
||||
|
marketSymbolMap: { [date: string]: { [symbol: string]: Big.Big } }, |
||||
|
lastUnitPrice: Big.Big |
||||
|
): PortfolioOrderItem { |
||||
|
return { |
||||
|
date: dateString, |
||||
|
fee: new Big(0), |
||||
|
feeInBaseCurrency: new Big(0), |
||||
|
quantity: new Big(0), |
||||
|
SymbolProfile: { |
||||
|
dataSource, |
||||
|
symbol |
||||
|
}, |
||||
|
type: 'BUY', |
||||
|
unitPrice: marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice, |
||||
|
unitPriceFromMarketData: |
||||
|
marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public fillOrdersByDate( |
||||
|
orders: PortfolioOrderItem[], |
||||
|
ordersByDate: { [date: string]: PortfolioOrderItem[] } |
||||
|
) { |
||||
|
for (const order of orders) { |
||||
|
ordersByDate[order.date] = ordersByDate[order.date] ?? []; |
||||
|
ordersByDate[order.date].push(order); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public addSyntheticStartAndEndOrder( |
||||
|
orders: PortfolioOrderItem[], |
||||
|
symbolMetricsHelper: PortfolioCalculatorSymbolMetricsHelperObject, |
||||
|
dataSource: DataSource, |
||||
|
symbol: string |
||||
|
) { |
||||
|
orders.push({ |
||||
|
date: symbolMetricsHelper.startDateString, |
||||
|
fee: new Big(0), |
||||
|
feeInBaseCurrency: new Big(0), |
||||
|
itemType: 'start', |
||||
|
quantity: new Big(0), |
||||
|
SymbolProfile: { |
||||
|
dataSource, |
||||
|
symbol |
||||
|
}, |
||||
|
type: 'BUY', |
||||
|
unitPrice: symbolMetricsHelper.unitPriceAtStartDate |
||||
|
}); |
||||
|
|
||||
|
orders.push({ |
||||
|
date: symbolMetricsHelper.endDateString, |
||||
|
fee: new Big(0), |
||||
|
feeInBaseCurrency: new Big(0), |
||||
|
itemType: 'end', |
||||
|
SymbolProfile: { |
||||
|
dataSource, |
||||
|
symbol |
||||
|
}, |
||||
|
quantity: new Big(0), |
||||
|
type: 'BUY', |
||||
|
unitPrice: symbolMetricsHelper.unitPriceAtEndDate |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public hasNoUnitPriceAtEndOrStartDate( |
||||
|
unitPriceAtEndDate: Big.Big, |
||||
|
unitPriceAtStartDate: Big.Big, |
||||
|
orders: PortfolioOrderItem[], |
||||
|
start: Date |
||||
|
) { |
||||
|
return ( |
||||
|
!unitPriceAtEndDate || |
||||
|
(!unitPriceAtStartDate && isBefore(new Date(orders[0].date), start)) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
public createEmptySymbolMetrics(): SymbolMetrics { |
||||
|
return { |
||||
|
currentValues: {}, |
||||
|
currentValuesWithCurrencyEffect: {}, |
||||
|
feesWithCurrencyEffect: new Big(0), |
||||
|
grossPerformance: new Big(0), |
||||
|
grossPerformancePercentage: new Big(0), |
||||
|
grossPerformancePercentageWithCurrencyEffect: new Big(0), |
||||
|
grossPerformanceWithCurrencyEffect: new Big(0), |
||||
|
hasErrors: false, |
||||
|
initialValue: new Big(0), |
||||
|
initialValueWithCurrencyEffect: new Big(0), |
||||
|
investmentValuesAccumulated: {}, |
||||
|
investmentValuesAccumulatedWithCurrencyEffect: {}, |
||||
|
investmentValuesWithCurrencyEffect: {}, |
||||
|
netPerformance: new Big(0), |
||||
|
netPerformancePercentage: new Big(0), |
||||
|
netPerformancePercentageWithCurrencyEffectMap: {}, |
||||
|
netPerformanceValues: {}, |
||||
|
netPerformanceValuesWithCurrencyEffect: {}, |
||||
|
netPerformanceWithCurrencyEffectMap: {}, |
||||
|
timeWeightedInvestment: new Big(0), |
||||
|
timeWeightedInvestmentValues: {}, |
||||
|
timeWeightedInvestmentValuesWithCurrencyEffect: {}, |
||||
|
timeWeightedInvestmentWithCurrencyEffect: new Big(0), |
||||
|
totalAccountBalanceInBaseCurrency: new Big(0), |
||||
|
totalDividend: new Big(0), |
||||
|
totalDividendInBaseCurrency: new Big(0), |
||||
|
totalInterest: new Big(0), |
||||
|
totalInterestInBaseCurrency: new Big(0), |
||||
|
totalInvestment: new Big(0), |
||||
|
totalInvestmentWithCurrencyEffect: new Big(0), |
||||
|
unitPrices: {}, |
||||
|
totalLiabilities: new Big(0), |
||||
|
totalLiabilitiesInBaseCurrency: new Big(0), |
||||
|
totalValuables: new Big(0), |
||||
|
totalValuablesInBaseCurrency: new Big(0) |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private fillOrderUnitPricesIfMissing( |
||||
|
order: PortfolioOrderItem, |
||||
|
symbolMetricsHelper: PortfolioCalculatorSymbolMetricsHelperObject |
||||
|
) { |
||||
|
order.unitPriceInBaseCurrency ??= this.marketSymbolMap[order.date]?.[ |
||||
|
order.SymbolProfile.symbol |
||||
|
].mul(symbolMetricsHelper.currentExchangeRate); |
||||
|
|
||||
|
order.unitPriceInBaseCurrencyWithCurrencyEffect ??= this.marketSymbolMap[ |
||||
|
order.date |
||||
|
]?.[order.SymbolProfile.symbol].mul( |
||||
|
symbolMetricsHelper.exchangeRateAtOrderDate |
||||
|
); |
||||
|
} |
||||
|
} |
@ -1,29 +1,272 @@ |
|||||
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; |
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; |
||||
|
import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor'; |
||||
import { |
import { |
||||
AssetProfileIdentifier, |
AssetProfileIdentifier, |
||||
SymbolMetrics |
SymbolMetrics |
||||
} from '@ghostfolio/common/interfaces'; |
} from '@ghostfolio/common/interfaces'; |
||||
import { PortfolioSnapshot } from '@ghostfolio/common/models'; |
import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models'; |
||||
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; |
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; |
||||
|
|
||||
|
import { Logger } from '@nestjs/common'; |
||||
|
import { Big } from 'big.js'; |
||||
|
import { cloneDeep } from 'lodash'; |
||||
|
|
||||
|
import { PortfolioOrderItem } from '../../interfaces/portfolio-order-item.interface'; |
||||
|
import { RoiPortfolioCalculatorSymbolMetricsHelper } from './portfolio-calculator-symbolmetrics-helper'; |
||||
|
|
||||
export class RoiPortfolioCalculator extends PortfolioCalculator { |
export class RoiPortfolioCalculator extends PortfolioCalculator { |
||||
protected calculateOverallPerformance(): PortfolioSnapshot { |
private chartDates: string[]; |
||||
throw new Error('Method not implemented.'); |
|
||||
|
@LogPerformance |
||||
|
protected calculateOverallPerformance( |
||||
|
positions: TimelinePosition[] |
||||
|
): PortfolioSnapshot { |
||||
|
let currentValueInBaseCurrency = new Big(0); |
||||
|
let grossPerformance = new Big(0); |
||||
|
let grossPerformanceWithCurrencyEffect = new Big(0); |
||||
|
let hasErrors = false; |
||||
|
let netPerformance = new Big(0); |
||||
|
let totalFeesWithCurrencyEffect = new Big(0); |
||||
|
const totalInterestWithCurrencyEffect = new Big(0); |
||||
|
let totalInvestment = new Big(0); |
||||
|
let totalInvestmentWithCurrencyEffect = new Big(0); |
||||
|
let totalTimeWeightedInvestment = new Big(0); |
||||
|
let totalTimeWeightedInvestmentWithCurrencyEffect = new Big(0); |
||||
|
|
||||
|
for (const currentPosition of positions) { |
||||
|
({ |
||||
|
totalFeesWithCurrencyEffect, |
||||
|
currentValueInBaseCurrency, |
||||
|
hasErrors, |
||||
|
totalInvestment, |
||||
|
totalInvestmentWithCurrencyEffect, |
||||
|
grossPerformance, |
||||
|
grossPerformanceWithCurrencyEffect, |
||||
|
netPerformance, |
||||
|
totalTimeWeightedInvestment, |
||||
|
totalTimeWeightedInvestmentWithCurrencyEffect |
||||
|
} = this.calculatePositionMetrics( |
||||
|
currentPosition, |
||||
|
totalFeesWithCurrencyEffect, |
||||
|
currentValueInBaseCurrency, |
||||
|
hasErrors, |
||||
|
totalInvestment, |
||||
|
totalInvestmentWithCurrencyEffect, |
||||
|
grossPerformance, |
||||
|
grossPerformanceWithCurrencyEffect, |
||||
|
netPerformance, |
||||
|
totalTimeWeightedInvestment, |
||||
|
totalTimeWeightedInvestmentWithCurrencyEffect |
||||
|
)); |
||||
} |
} |
||||
|
|
||||
protected getPerformanceCalculationType() { |
return { |
||||
return PerformanceCalculationType.ROI; |
currentValueInBaseCurrency, |
||||
|
hasErrors, |
||||
|
positions, |
||||
|
totalFeesWithCurrencyEffect, |
||||
|
totalInterestWithCurrencyEffect, |
||||
|
totalInvestment, |
||||
|
totalInvestmentWithCurrencyEffect, |
||||
|
activitiesCount: this.activities.filter(({ type }) => { |
||||
|
return ['BUY', 'SELL', 'STAKE'].includes(type); |
||||
|
}).length, |
||||
|
createdAt: new Date(), |
||||
|
errors: [], |
||||
|
historicalData: [], |
||||
|
totalLiabilitiesWithCurrencyEffect: new Big(0), |
||||
|
totalValuablesWithCurrencyEffect: new Big(0) |
||||
|
}; |
||||
} |
} |
||||
|
|
||||
protected getSymbolMetrics({}: { |
protected getSymbolMetrics({ |
||||
|
chartDateMap, |
||||
|
dataSource, |
||||
|
end, |
||||
|
exchangeRates, |
||||
|
marketSymbolMap, |
||||
|
start, |
||||
|
symbol |
||||
|
}: { |
||||
|
chartDateMap?: { [date: string]: boolean }; |
||||
end: Date; |
end: Date; |
||||
exchangeRates: { [dateString: string]: number }; |
exchangeRates: { [dateString: string]: number }; |
||||
marketSymbolMap: { |
marketSymbolMap: { |
||||
[date: string]: { [symbol: string]: Big }; |
[date: string]: { [symbol: string]: Big }; |
||||
}; |
}; |
||||
start: Date; |
start: Date; |
||||
step?: number; |
|
||||
} & AssetProfileIdentifier): SymbolMetrics { |
} & AssetProfileIdentifier): SymbolMetrics { |
||||
throw new Error('Method not implemented.'); |
if (!this.chartDates) { |
||||
|
this.chartDates = Object.keys(chartDateMap).sort(); |
||||
|
} |
||||
|
const symbolMetricsHelperClass = |
||||
|
new RoiPortfolioCalculatorSymbolMetricsHelper( |
||||
|
PortfolioCalculator.ENABLE_LOGGING, |
||||
|
marketSymbolMap, |
||||
|
this.chartDates |
||||
|
); |
||||
|
const symbolMetricsHelper = |
||||
|
symbolMetricsHelperClass.getSymbolMetricHelperObject( |
||||
|
exchangeRates, |
||||
|
start, |
||||
|
end, |
||||
|
marketSymbolMap, |
||||
|
symbol |
||||
|
); |
||||
|
|
||||
|
let orders: PortfolioOrderItem[] = cloneDeep( |
||||
|
this.activities.filter(({ SymbolProfile }) => { |
||||
|
return SymbolProfile.symbol === symbol; |
||||
|
}) |
||||
|
); |
||||
|
|
||||
|
if (!orders.length) { |
||||
|
return symbolMetricsHelper.symbolMetrics; |
||||
|
} |
||||
|
|
||||
|
if ( |
||||
|
symbolMetricsHelperClass.hasNoUnitPriceAtEndOrStartDate( |
||||
|
symbolMetricsHelper.unitPriceAtEndDate, |
||||
|
symbolMetricsHelper.unitPriceAtStartDate, |
||||
|
orders, |
||||
|
start |
||||
|
) |
||||
|
) { |
||||
|
symbolMetricsHelper.symbolMetrics.hasErrors = true; |
||||
|
return symbolMetricsHelper.symbolMetrics; |
||||
|
} |
||||
|
|
||||
|
symbolMetricsHelperClass.addSyntheticStartAndEndOrder( |
||||
|
orders, |
||||
|
symbolMetricsHelper, |
||||
|
dataSource, |
||||
|
symbol |
||||
|
); |
||||
|
|
||||
|
orders = symbolMetricsHelperClass.fillOrdersAndSortByTime( |
||||
|
orders, |
||||
|
symbolMetricsHelper, |
||||
|
chartDateMap, |
||||
|
marketSymbolMap, |
||||
|
symbol, |
||||
|
dataSource |
||||
|
); |
||||
|
|
||||
|
symbolMetricsHelper.indexOfStartOrder = orders.findIndex(({ itemType }) => { |
||||
|
return itemType === 'start'; |
||||
|
}); |
||||
|
symbolMetricsHelper.indexOfEndOrder = orders.findIndex(({ itemType }) => { |
||||
|
return itemType === 'end'; |
||||
|
}); |
||||
|
|
||||
|
for (let i = 0; i < orders.length; i++) { |
||||
|
symbolMetricsHelperClass.processOrderMetrics( |
||||
|
orders, |
||||
|
i, |
||||
|
exchangeRates, |
||||
|
symbolMetricsHelper |
||||
|
); |
||||
|
if (i === symbolMetricsHelper.indexOfEndOrder) { |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
symbolMetricsHelperClass.handleOverallPerformanceCalculation( |
||||
|
symbolMetricsHelper |
||||
|
); |
||||
|
symbolMetricsHelperClass.calculateNetPerformanceByDateRange( |
||||
|
start, |
||||
|
symbolMetricsHelper |
||||
|
); |
||||
|
|
||||
|
return symbolMetricsHelper.symbolMetrics; |
||||
|
} |
||||
|
|
||||
|
protected getPerformanceCalculationType() { |
||||
|
return PerformanceCalculationType.ROI; |
||||
|
} |
||||
|
|
||||
|
private calculatePositionMetrics( |
||||
|
currentPosition: TimelinePosition, |
||||
|
totalFeesWithCurrencyEffect: Big, |
||||
|
currentValueInBaseCurrency: Big, |
||||
|
hasErrors: boolean, |
||||
|
totalInvestment: Big, |
||||
|
totalInvestmentWithCurrencyEffect: Big, |
||||
|
grossPerformance: Big, |
||||
|
grossPerformanceWithCurrencyEffect: Big, |
||||
|
netPerformance: Big, |
||||
|
totalTimeWeightedInvestment: Big, |
||||
|
totalTimeWeightedInvestmentWithCurrencyEffect: Big |
||||
|
) { |
||||
|
if (currentPosition.feeInBaseCurrency) { |
||||
|
totalFeesWithCurrencyEffect = totalFeesWithCurrencyEffect.plus( |
||||
|
currentPosition.feeInBaseCurrency |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
if (currentPosition.valueInBaseCurrency) { |
||||
|
currentValueInBaseCurrency = currentValueInBaseCurrency.plus( |
||||
|
currentPosition.valueInBaseCurrency |
||||
|
); |
||||
|
} else { |
||||
|
hasErrors = true; |
||||
|
} |
||||
|
|
||||
|
if (currentPosition.investment) { |
||||
|
totalInvestment = totalInvestment.plus(currentPosition.investment); |
||||
|
|
||||
|
totalInvestmentWithCurrencyEffect = |
||||
|
totalInvestmentWithCurrencyEffect.plus( |
||||
|
currentPosition.investmentWithCurrencyEffect |
||||
|
); |
||||
|
} else { |
||||
|
hasErrors = true; |
||||
|
} |
||||
|
|
||||
|
if (currentPosition.grossPerformance) { |
||||
|
grossPerformance = grossPerformance.plus( |
||||
|
currentPosition.grossPerformance |
||||
|
); |
||||
|
|
||||
|
grossPerformanceWithCurrencyEffect = |
||||
|
grossPerformanceWithCurrencyEffect.plus( |
||||
|
currentPosition.grossPerformanceWithCurrencyEffect |
||||
|
); |
||||
|
|
||||
|
netPerformance = netPerformance.plus(currentPosition.netPerformance); |
||||
|
} else if (!currentPosition.quantity.eq(0)) { |
||||
|
hasErrors = true; |
||||
|
} |
||||
|
|
||||
|
if (currentPosition.timeWeightedInvestment) { |
||||
|
totalTimeWeightedInvestment = totalTimeWeightedInvestment.plus( |
||||
|
currentPosition.timeWeightedInvestment |
||||
|
); |
||||
|
|
||||
|
totalTimeWeightedInvestmentWithCurrencyEffect = |
||||
|
totalTimeWeightedInvestmentWithCurrencyEffect.plus( |
||||
|
currentPosition.timeWeightedInvestmentWithCurrencyEffect |
||||
|
); |
||||
|
} else if (!currentPosition.quantity.eq(0)) { |
||||
|
Logger.warn( |
||||
|
`Missing historical market data for ${currentPosition.symbol} (${currentPosition.dataSource})`, |
||||
|
'PortfolioCalculator' |
||||
|
); |
||||
|
|
||||
|
hasErrors = true; |
||||
|
} |
||||
|
return { |
||||
|
totalFeesWithCurrencyEffect, |
||||
|
currentValueInBaseCurrency, |
||||
|
hasErrors, |
||||
|
totalInvestment, |
||||
|
totalInvestmentWithCurrencyEffect, |
||||
|
grossPerformance, |
||||
|
grossPerformanceWithCurrencyEffect, |
||||
|
netPerformance, |
||||
|
totalTimeWeightedInvestment, |
||||
|
totalTimeWeightedInvestmentWithCurrencyEffect |
||||
|
}; |
||||
} |
} |
||||
} |
} |
||||
|
@ -1,5 +1,9 @@ |
|||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces'; |
import { |
||||
|
PortfolioPosition, |
||||
|
PortfolioPerformance |
||||
|
} from '@ghostfolio/common/interfaces'; |
||||
|
|
||||
export interface PortfolioHoldingsResponse { |
export interface PortfolioHoldingsResponse { |
||||
holdings: PortfolioPosition[]; |
holdings: PortfolioPosition[]; |
||||
|
performance: PortfolioPerformance; |
||||
} |
} |
||||
|
Loading…
Reference in new issue