|
@ -106,16 +106,19 @@ export class PortfolioCalculator { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
public async getCurrentPositions(start: Date): Promise<{ |
|
|
public async getCurrentPositions(start: Date): Promise<{ |
|
|
[symbol: string]: TimelinePosition; |
|
|
hasErrors: boolean; |
|
|
|
|
|
positions: TimelinePosition[]; |
|
|
}> { |
|
|
}> { |
|
|
if (!this.transactionPoints?.length) { |
|
|
if (!this.transactionPoints?.length) { |
|
|
return {}; |
|
|
return { |
|
|
|
|
|
hasErrors: false, |
|
|
|
|
|
positions: [] |
|
|
|
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const lastTransactionPoint = |
|
|
const lastTransactionPoint = |
|
|
this.transactionPoints[this.transactionPoints.length - 1]; |
|
|
this.transactionPoints[this.transactionPoints.length - 1]; |
|
|
|
|
|
|
|
|
const result: { [symbol: string]: TimelinePosition } = {}; |
|
|
|
|
|
// use Date.now() to use the mock for today
|
|
|
// use Date.now() to use the mock for today
|
|
|
const today = new Date(Date.now()); |
|
|
const today = new Date(Date.now()); |
|
|
|
|
|
|
|
@ -171,6 +174,7 @@ export class PortfolioCalculator { |
|
|
); |
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
let hasErrors = false; |
|
|
const startString = format(start, DATE_FORMAT); |
|
|
const startString = format(start, DATE_FORMAT); |
|
|
|
|
|
|
|
|
const holdingPeriodReturns: { [symbol: string]: Big } = {}; |
|
|
const holdingPeriodReturns: { [symbol: string]: Big } = {}; |
|
@ -178,12 +182,14 @@ export class PortfolioCalculator { |
|
|
let todayString = format(today, DATE_FORMAT); |
|
|
let todayString = format(today, DATE_FORMAT); |
|
|
// in case no symbols are there for today, use yesterday
|
|
|
// in case no symbols are there for today, use yesterday
|
|
|
if (!marketSymbolMap[todayString]) { |
|
|
if (!marketSymbolMap[todayString]) { |
|
|
|
|
|
hasErrors = true; |
|
|
todayString = format(subDays(today, 1), DATE_FORMAT); |
|
|
todayString = format(subDays(today, 1), DATE_FORMAT); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (firstIndex > 0) { |
|
|
if (firstIndex > 0) { |
|
|
firstIndex--; |
|
|
firstIndex--; |
|
|
} |
|
|
} |
|
|
|
|
|
const invalidSymbols = []; |
|
|
for (let i = firstIndex; i < this.transactionPoints.length; i++) { |
|
|
for (let i = firstIndex; i < this.transactionPoints.length; i++) { |
|
|
const currentDate = |
|
|
const currentDate = |
|
|
i === firstIndex ? startString : this.transactionPoints[i].date; |
|
|
i === firstIndex ? startString : this.transactionPoints[i].date; |
|
@ -198,6 +204,22 @@ export class PortfolioCalculator { |
|
|
if (!oldHoldingPeriodReturn) { |
|
|
if (!oldHoldingPeriodReturn) { |
|
|
oldHoldingPeriodReturn = new Big(1); |
|
|
oldHoldingPeriodReturn = new Big(1); |
|
|
} |
|
|
} |
|
|
|
|
|
if (!marketSymbolMap[nextDate]?.[item.symbol]) { |
|
|
|
|
|
invalidSymbols.push(item.symbol); |
|
|
|
|
|
hasErrors = true; |
|
|
|
|
|
console.error( |
|
|
|
|
|
`Missing value for symbol ${item.symbol} at ${nextDate}` |
|
|
|
|
|
); |
|
|
|
|
|
continue; |
|
|
|
|
|
} |
|
|
|
|
|
if (!marketSymbolMap[currentDate]?.[item.symbol]) { |
|
|
|
|
|
invalidSymbols.push(item.symbol); |
|
|
|
|
|
hasErrors = true; |
|
|
|
|
|
console.error( |
|
|
|
|
|
`Missing value for symbol ${item.symbol} at ${currentDate}` |
|
|
|
|
|
); |
|
|
|
|
|
continue; |
|
|
|
|
|
} |
|
|
holdingPeriodReturns[item.symbol] = oldHoldingPeriodReturn.mul( |
|
|
holdingPeriodReturns[item.symbol] = oldHoldingPeriodReturn.mul( |
|
|
marketSymbolMap[nextDate][item.symbol].div( |
|
|
marketSymbolMap[nextDate][item.symbol].div( |
|
|
marketSymbolMap[currentDate][item.symbol] |
|
|
marketSymbolMap[currentDate][item.symbol] |
|
@ -215,26 +237,31 @@ export class PortfolioCalculator { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const positions: TimelinePosition[] = []; |
|
|
for (const item of lastTransactionPoint.items) { |
|
|
for (const item of lastTransactionPoint.items) { |
|
|
const marketValue = marketSymbolMap[todayString][item.symbol]; |
|
|
const marketValue = marketSymbolMap[todayString]?.[item.symbol]; |
|
|
result[item.symbol] = { |
|
|
const isValid = invalidSymbols.indexOf(item.symbol) === -1; |
|
|
|
|
|
positions.push({ |
|
|
averagePrice: item.investment.div(item.quantity), |
|
|
averagePrice: item.investment.div(item.quantity), |
|
|
currency: item.currency, |
|
|
currency: item.currency, |
|
|
firstBuyDate: item.firstBuyDate, |
|
|
firstBuyDate: item.firstBuyDate, |
|
|
grossPerformance: grossPerformance[item.symbol] ?? null, |
|
|
grossPerformance: isValid |
|
|
grossPerformancePercentage: holdingPeriodReturns[item.symbol] |
|
|
? grossPerformance[item.symbol] ?? null |
|
|
|
|
|
: null, |
|
|
|
|
|
grossPerformancePercentage: |
|
|
|
|
|
isValid && holdingPeriodReturns[item.symbol] |
|
|
? holdingPeriodReturns[item.symbol].minus(1) |
|
|
? holdingPeriodReturns[item.symbol].minus(1) |
|
|
: null, |
|
|
: null, |
|
|
investment: item.investment, |
|
|
investment: item.investment, |
|
|
marketPrice: marketValue.toNumber(), |
|
|
marketPrice: marketValue?.toNumber() ?? null, |
|
|
name: item.name, |
|
|
name: item.name, |
|
|
quantity: item.quantity, |
|
|
quantity: item.quantity, |
|
|
symbol: item.symbol, |
|
|
symbol: item.symbol, |
|
|
transactionCount: item.transactionCount |
|
|
transactionCount: item.transactionCount |
|
|
}; |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
return result; |
|
|
return { hasErrors, positions }; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
public async calculateTimeline( |
|
|
public async calculateTimeline( |
|
|