mirror of https://github.com/ghostfolio/ghostfolio
8 changed files with 2020 additions and 6 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,253 @@ |
|||
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, |
|||
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, |
|||
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, |
|||
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,861 @@ |
|||
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); |
|||
|
|||
symbolMetricsHelper.symbolMetrics.netPerformancePercentageWithCurrencyEffectMap[ |
|||
dateRange |
|||
] = |
|||
symbolMetricsHelper.symbolMetrics.timeWeightedInvestmentValuesWithCurrencyEffect[ |
|||
rangeEndDateString |
|||
]?.gt(0) |
|||
? symbolMetricsHelper.symbolMetrics.netPerformanceWithCurrencyEffectMap[ |
|||
dateRange |
|||
].div( |
|||
symbolMetricsHelper.symbolMetrics |
|||
.timeWeightedInvestmentValuesWithCurrencyEffect[ |
|||
rangeEndDateString |
|||
] |
|||
) |
|||
: 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 |
|||
); |
|||
} |
|||
} |
Loading…
Reference in new issue