Add test case for BUY and SELL (partially) (#826)
* Add test case for BUY and SELL (partially) * Fix investment calculation for sell activities * Do not show total value if sell activity * Update changelog
This commit is contained in:
parent
65e062ad26
commit
ba7c98d325
@ -15,6 +15,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
- Migrated the search functionality to `yahoo-finance2`
|
- Migrated the search functionality to `yahoo-finance2`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue in the average price / investment calculation for sell activities
|
||||||
|
|
||||||
## 1.136.0 - 13.04.2022
|
## 1.136.0 - 13.04.2022
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
@ -20,6 +20,13 @@ function mockGetValue(symbol: string, date: Date) {
|
|||||||
|
|
||||||
return { marketPrice: 0 };
|
return { marketPrice: 0 };
|
||||||
|
|
||||||
|
case 'NOVN.SW':
|
||||||
|
if (isSameDay(parseDate('2022-04-11'), date)) {
|
||||||
|
return { marketPrice: 87.8 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { marketPrice: 0 };
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return { marketPrice: 0 };
|
return { marketPrice: 0 };
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,96 @@
|
|||||||
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
|
import Big from 'big.js';
|
||||||
|
|
||||||
|
import { CurrentRateServiceMock } from './current-rate.service.mock';
|
||||||
|
import { PortfolioCalculator } from './portfolio-calculator';
|
||||||
|
|
||||||
|
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
CurrentRateService: jest.fn().mockImplementation(() => {
|
||||||
|
return CurrentRateServiceMock;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PortfolioCalculator', () => {
|
||||||
|
let currentRateService: CurrentRateService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
currentRateService = new CurrentRateService(null, null, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('get current positions', () => {
|
||||||
|
it.only('with BALN.SW buy and sell', async () => {
|
||||||
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
|
currentRateService,
|
||||||
|
currency: 'CHF',
|
||||||
|
orders: [
|
||||||
|
{
|
||||||
|
currency: 'CHF',
|
||||||
|
date: '2022-03-07',
|
||||||
|
dataSource: 'YAHOO',
|
||||||
|
fee: new Big(1.3),
|
||||||
|
name: 'Novartis AG',
|
||||||
|
quantity: new Big(2),
|
||||||
|
symbol: 'NOVN.SW',
|
||||||
|
type: 'BUY',
|
||||||
|
unitPrice: new Big(75.8)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
currency: 'CHF',
|
||||||
|
date: '2022-04-08',
|
||||||
|
dataSource: 'YAHOO',
|
||||||
|
fee: new Big(2.95),
|
||||||
|
name: 'Novartis AG',
|
||||||
|
quantity: new Big(1),
|
||||||
|
symbol: 'NOVN.SW',
|
||||||
|
type: 'SELL',
|
||||||
|
unitPrice: new Big(85.73)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
portfolioCalculator.computeTransactionPoints();
|
||||||
|
|
||||||
|
const spy = jest
|
||||||
|
.spyOn(Date, 'now')
|
||||||
|
.mockImplementation(() => parseDate('2022-04-11').getTime());
|
||||||
|
|
||||||
|
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||||
|
parseDate('2022-03-07')
|
||||||
|
);
|
||||||
|
|
||||||
|
spy.mockRestore();
|
||||||
|
|
||||||
|
expect(currentPositions).toEqual({
|
||||||
|
currentValue: new Big('87.8'),
|
||||||
|
errors: [],
|
||||||
|
grossPerformance: new Big('21.93'),
|
||||||
|
grossPerformancePercentage: new Big('0.14465699208443271768'),
|
||||||
|
hasErrors: false,
|
||||||
|
netPerformance: new Big('17.68'),
|
||||||
|
netPerformancePercentage: new Big('0.11662269129287598945'),
|
||||||
|
positions: [
|
||||||
|
{
|
||||||
|
averagePrice: new Big('75.80'),
|
||||||
|
currency: 'CHF',
|
||||||
|
dataSource: 'YAHOO',
|
||||||
|
firstBuyDate: '2022-03-07',
|
||||||
|
grossPerformance: new Big('21.93'),
|
||||||
|
grossPerformancePercentage: new Big('0.14465699208443271768'),
|
||||||
|
investment: new Big('75.80'),
|
||||||
|
netPerformance: new Big('17.68'),
|
||||||
|
netPerformancePercentage: new Big('0.11662269129287598945'),
|
||||||
|
marketPrice: 87.8,
|
||||||
|
quantity: new Big('1'),
|
||||||
|
symbol: 'NOVN.SW',
|
||||||
|
transactionCount: 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
totalInvestment: new Big('75.80')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -77,17 +77,30 @@ export class PortfolioCalculator {
|
|||||||
const newQuantity = order.quantity
|
const newQuantity = order.quantity
|
||||||
.mul(factor)
|
.mul(factor)
|
||||||
.plus(oldAccumulatedSymbol.quantity);
|
.plus(oldAccumulatedSymbol.quantity);
|
||||||
|
|
||||||
|
let investment = new Big(0);
|
||||||
|
|
||||||
|
if (newQuantity.gt(0)) {
|
||||||
|
if (order.type === 'BUY') {
|
||||||
|
investment = oldAccumulatedSymbol.investment.plus(
|
||||||
|
order.quantity.mul(unitPrice)
|
||||||
|
);
|
||||||
|
} else if (order.type === 'SELL') {
|
||||||
|
const averagePrice = oldAccumulatedSymbol.investment.div(
|
||||||
|
oldAccumulatedSymbol.quantity
|
||||||
|
);
|
||||||
|
investment = oldAccumulatedSymbol.investment.minus(
|
||||||
|
order.quantity.mul(averagePrice)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
currentTransactionPointItem = {
|
currentTransactionPointItem = {
|
||||||
|
investment,
|
||||||
currency: order.currency,
|
currency: order.currency,
|
||||||
dataSource: order.dataSource,
|
dataSource: order.dataSource,
|
||||||
fee: order.fee.plus(oldAccumulatedSymbol.fee),
|
fee: order.fee.plus(oldAccumulatedSymbol.fee),
|
||||||
firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
|
firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
|
||||||
investment: newQuantity.eq(0)
|
|
||||||
? new Big(0)
|
|
||||||
: unitPrice
|
|
||||||
.mul(order.quantity)
|
|
||||||
.mul(factor)
|
|
||||||
.plus(oldAccumulatedSymbol.investment),
|
|
||||||
quantity: newQuantity,
|
quantity: newQuantity,
|
||||||
symbol: order.symbol,
|
symbol: order.symbol,
|
||||||
transactionCount: oldAccumulatedSymbol.transactionCount + 1
|
transactionCount: oldAccumulatedSymbol.transactionCount + 1
|
||||||
|
@ -271,6 +271,7 @@
|
|||||||
<td *matFooterCellDef class="d-none d-lg-table-cell px-1" mat-footer-cell>
|
<td *matFooterCellDef class="d-none d-lg-table-cell px-1" mat-footer-cell>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<gf-value
|
<gf-value
|
||||||
|
*ngIf="totalValue !== null"
|
||||||
[isAbsolute]="true"
|
[isAbsolute]="true"
|
||||||
[isCurrency]="true"
|
[isCurrency]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
@ -302,6 +303,7 @@
|
|||||||
<td *matFooterCellDef class="d-lg-none d-xl-none px-1" mat-footer-cell>
|
<td *matFooterCellDef class="d-lg-none d-xl-none px-1" mat-footer-cell>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<gf-value
|
<gf-value
|
||||||
|
*ngIf="totalValue !== null"
|
||||||
[isAbsolute]="true"
|
[isAbsolute]="true"
|
||||||
[isCurrency]="true"
|
[isCurrency]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
|
@ -325,7 +325,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
|||||||
if (activity.type === 'BUY' || activity.type === 'ITEM') {
|
if (activity.type === 'BUY' || activity.type === 'ITEM') {
|
||||||
totalValue = totalValue.plus(activity.valueInBaseCurrency);
|
totalValue = totalValue.plus(activity.valueInBaseCurrency);
|
||||||
} else if (activity.type === 'SELL') {
|
} else if (activity.type === 'SELL') {
|
||||||
totalValue = totalValue.minus(activity.valueInBaseCurrency);
|
return null;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user