fitr
Version:
Financial Investment Tracker
336 lines (292 loc) • 8.86 kB
text/typescript
import { jest, describe, expect, test, beforeAll, afterAll, beforeEach } from '@jest/globals';
import { Transaction, TransactionType } from '../../types';
import { calculateTWR } from '../portfolioService';
// Mock date for consistent testing
beforeAll(() => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2023-12-31'));
});
afterAll(() => {
jest.useRealTimers();
});
describe('calculateTWR', () => {
beforeEach(() => {
jest.clearAllMocks();
});
test('should return 0 for empty transactions array', () => {
const transactions: Transaction[] = [];
const currentPrice = 100;
const result = calculateTWR(transactions, currentPrice, new Date());
expect(result).toBe(0);
});
test('should return 0 when currentPrice is 0 or negative', () => {
const transactions: Transaction[] = [
{
date: '2023-01-01',
type: TransactionType.BUY,
quantity: 10,
price: 150,
fees: 0,
notes: ''
}
];
expect(calculateTWR(transactions, 0, new Date())).toBe(0);
expect(calculateTWR(transactions, -10, new Date())).toBe(0);
});
test('should return 0 when there are no relevant transactions', () => {
const transactions: Transaction[] = [
{
date: '2023-01-01',
type: TransactionType.DIVIDEND,
quantity: 0,
price: 0,
fees: 0,
notes: ''
}
];
const result = calculateTWR(transactions, 100, new Date());
expect(result).toBe(0);
});
test('should calculate positive return correctly for a single buy transaction', () => {
const transactions: Transaction[] = [
{
date: '2023-01-01',
type: TransactionType.BUY,
quantity: 10,
price: 150,
fees: 0,
notes: ''
}
];
// Price increased by 10%
const currentPrice = 165;
const result = calculateTWR(transactions, currentPrice, new Date());
// Expected return is 10%
expect(result).toBeCloseTo(0.1, 5);
});
test('should calculate negative return correctly for a single buy transaction', () => {
const transactions: Transaction[] = [
{
date: '2023-01-01',
type: TransactionType.BUY,
quantity: 10,
price: 150,
fees: 0,
notes: ''
}
];
// Price decreased by 10%
const currentPrice = 135;
const result = calculateTWR(transactions, currentPrice, new Date());
// Expected return is -10%
expect(result).toBeCloseTo(-0.1, 5);
});
test('should handle multiple buy transactions correctly', () => {
const transactions: Transaction[] = [
{
date: '2023-01-15',
type: TransactionType.BUY,
quantity: 2,
price: 150,
fees: 0,
notes: ''
},
{
date: '2023-03-10',
type: TransactionType.BUY,
quantity: 3,
price: 160,
fees: 0,
notes: ''
},
{
date: '2023-06-05',
type: TransactionType.BUY,
quantity: 2,
price: 170,
fees: 0,
notes: ''
},
{
date: '2023-09-20',
type: TransactionType.BUY,
quantity: 4,
price: 180,
fees: 0,
notes: ''
},
{
date: '2023-11-15',
type: TransactionType.BUY,
quantity: 1,
price: 190,
fees: 0,
notes: ''
}
];
// Final price is 5% higher than the last purchase
const currentPrice = 199.5;
const result = calculateTWR(transactions, currentPrice, new Date());
// Expected TWR calculation:
// Period 1: 160/150 - 1 = 0.0667 (6.67% increase)
// Period 2: 170/160 - 1 = 0.0625 (6.25% increase)
// Period 3: 180/170 - 1 = 0.0588 (5.88% increase)
// Period 4: 190/180 - 1 = 0.0556 (5.56% increase)
// Period 5: 199.5/190 - 1 = 0.05 (5% increase)
// TWR = (1 + 0.0667) * (1 + 0.0625) * (1 + 0.0588) * (1 + 0.0556) * (1 + 0.05) - 1
// TWR ≈ 0.33 (33% total return)
expect(result).toBeCloseTo(0.33, 2);
});
test('should handle buy and sell transactions correctly', () => {
const transactions: Transaction[] = [
{
date: '2023-01-15',
type: TransactionType.BUY,
quantity: 10,
price: 150,
fees: 0,
notes: 'Initial purchase'
},
{
date: '2023-03-10',
type: TransactionType.BUY,
quantity: 5,
price: 140,
fees: 0,
notes: 'Buying the dip'
},
{
date: '2023-05-20',
type: TransactionType.SELL,
quantity: 3,
price: 160,
fees: 0,
notes: 'Taking some profits'
},
{
date: '2023-07-15',
type: TransactionType.BUY,
quantity: 8,
price: 170,
fees: 0,
notes: 'Adding to position'
},
{
date: '2023-09-05',
type: TransactionType.SELL,
quantity: 7,
price: 185,
fees: 0,
notes: 'Taking more profits'
},
{
date: '2023-11-20',
type: TransactionType.BUY,
quantity: 4,
price: 175,
fees: 0,
notes: 'Final purchase of the year'
}
];
// Final price is about 10% higher than the last purchase
const currentPrice = 192;
const result = calculateTWR(transactions, currentPrice, new Date());
// Expected TWR calculation with multiple buy/sell events:
// Period 1 (Jan 15 - Mar 10): 140/150 = 0.933, HPR = -0.067 (6.7% decrease)
// Period 2 (Mar 10 - May 20): 160/140 = 1.143, HPR = 0.143 (14.3% increase)
// Period 3 (May 20 - Jul 15): 170/160 = 1.063, HPR = 0.063 (6.3% increase)
// Period 4 (Jul 15 - Sep 05): 185/170 = 1.088, HPR = 0.088 (8.8% increase)
// Period 5 (Sep 05 - Nov 20): 175/185 = 0.946, HPR = -0.054 (5.4% decrease)
// Period 6 (Nov 20 - Dec 31): 192/175 = 1.097, HPR = 0.097 (9.7% increase)
// TWR = (1 - 0.067) * (1 + 0.143) * (1 + 0.063) * (1 + 0.088) * (1 - 0.054) * (1 + 0.097) - 1
// TWR ≈ 0.28 (28% total return)
expect(result).toBeCloseTo(0.28, 2);
});
test('should handle vested stock transactions correctly', () => {
const transactions: Transaction[] = [
{
date: '2023-01-01',
type: TransactionType.VESTED,
quantity: 10,
price: 150,
fees: 0,
notes: ''
}
];
// Price increased by 10%
const currentPrice = 165;
const result = calculateTWR(transactions, currentPrice, new Date());
// Expected return is 10%
expect(result).toBeCloseTo(0.1, 5);
});
test('should handle transactions with extreme price changes within reasonable limits', () => {
const transactions: Transaction[] = [
{
date: '2023-01-01',
type: TransactionType.BUY,
quantity: 10,
price: 10,
fees: 0,
notes: ''
}
];
// Price increased by 500%
const currentPrice = 60;
const result = calculateTWR(transactions, currentPrice, new Date());
// Expected return is 500%
expect(result).toBeCloseTo(5.0, 5);
});
test('should handle transactions in non-chronological order', () => {
const transactions: Transaction[] = [
{
date: '2023-06-01',
type: TransactionType.BUY,
quantity: 5,
price: 180,
fees: 0,
notes: ''
},
{
date: '2023-01-01',
type: TransactionType.BUY,
quantity: 5,
price: 150,
fees: 0,
notes: ''
}
];
// Final price is 10% higher than the last purchase
const currentPrice = 198;
const result = calculateTWR(transactions, currentPrice, new Date());
// Expected TWR should be the same as if transactions were in order
// First period: 180/150 - 1 = 0.2 (20% increase)
// Second period: 198/180 - 1 = 0.1 (10% increase)
// TWR = (1 + 0.2) * (1 + 0.1) - 1 = 0.32 (32% total return)
expect(result).toBeCloseTo(0.32, 5);
});
test('should handle a complete sell of all holdings', () => {
const transactions: Transaction[] = [
{
date: '2023-01-01',
type: TransactionType.BUY,
quantity: 10,
price: 150,
fees: 0,
notes: ''
},
{
date: '2023-06-01',
type: TransactionType.SELL,
quantity: 10,
price: 180,
fees: 0,
notes: ''
}
];
// Current price doesn't matter since all holdings were sold
const currentPrice = 200;
const result = calculateTWR(transactions, currentPrice, new Date());
// Expected return is 20% (from the buy to sell)
expect(result).toBeCloseTo(0.2, 5);
});
});