[ { "name": "one year, no flows: the simple return, annualised to itself", "args": [ [ { "date": "2025-01-01", "value": { "minor": 100000, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } }, { "date": "2026-01-01", "value": { "minor": 110000, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } } ] ], "expect": { "basisPoints": 1000, "subPeriodBasisPoints": [ 1000 ], "days": 365, "annualisedBasisPoints": 1000 } }, { "name": "a mid-year deposit is taken out: 10% then 5% links to 15.5%, not 18%", "args": [ [ { "date": "2025-01-01", "value": { "minor": 100000, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } }, { "date": "2025-07-01", "value": { "minor": 110000, "currency": "GBP" }, "flow": { "minor": 50000, "currency": "GBP" } }, { "date": "2026-01-01", "value": { "minor": 168000, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } } ] ], "expect": { "basisPoints": 1550, "subPeriodBasisPoints": [ 1000, 500 ], "days": 365, "annualisedBasisPoints": 1550 } }, { "name": "a withdrawal after a loss: -10% then +20% is +8%, not annualised under a year", "args": [ [ { "date": "2025-01-01", "value": { "minor": 100000, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } }, { "date": "2025-04-01", "value": { "minor": 90000, "currency": "GBP" }, "flow": { "minor": -40000, "currency": "GBP" } }, { "date": "2025-10-01", "value": { "minor": 60000, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } } ] ], "expect": { "basisPoints": 800, "subPeriodBasisPoints": [ -1000, 2000 ], "days": 273, "annualisedBasisPoints": null } }, { "name": "an account opened by its first flow from a value of zero", "args": [ [ { "date": "2025-01-01", "value": { "minor": 0, "currency": "GBP" }, "flow": { "minor": 100000, "currency": "GBP" } }, { "date": "2025-03-01", "value": { "minor": 105000, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } } ] ], "expect": { "basisPoints": 500, "subPeriodBasisPoints": [ 500 ], "days": 59, "annualisedBasisPoints": null } }, { "name": "the last point's flow does not change the return", "args": [ [ { "date": "2025-01-01", "value": { "minor": 100000, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } }, { "date": "2026-01-01", "value": { "minor": 110000, "currency": "GBP" }, "flow": { "minor": -110000, "currency": "GBP" } } ] ], "expect": { "basisPoints": 1000, "subPeriodBasisPoints": [ 1000 ], "days": 365, "annualisedBasisPoints": 1000 } }, { "name": "three years to 1.331 annualise over 1096 days, a shade under 10%", "args": [ [ { "date": "2020-01-01", "value": { "minor": 100000, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } }, { "date": "2023-01-01", "value": { "minor": 133100, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } } ] ], "expect": { "basisPoints": 3310, "subPeriodBasisPoints": [ 3310 ], "days": 1096, "annualisedBasisPoints": 999 } }, { "name": "three sub-periods with deposits and a withdrawal", "args": [ [ { "date": "2022-01-01", "value": { "minor": 250000, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } }, { "date": "2022-03-15", "value": { "minor": 262500, "currency": "GBP" }, "flow": { "minor": 25000, "currency": "GBP" } }, { "date": "2022-08-31", "value": { "minor": 280000, "currency": "GBP" }, "flow": { "minor": -30000, "currency": "GBP" } }, { "date": "2023-02-28", "value": { "minor": 262000, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } } ] ], "expect": { "basisPoints": 717, "subPeriodBasisPoints": [ 500, -261, 480 ], "days": 423, "annualisedBasisPoints": 616 } }, { "name": "a total loss is -100%, annualised too", "args": [ [ { "date": "2020-01-01", "value": { "minor": 100000, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } }, { "date": "2021-06-01", "value": { "minor": 0, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } } ] ], "expect": { "basisPoints": -10000, "subPeriodBasisPoints": [ -10000 ], "days": 517, "annualisedBasisPoints": -10000 } }, { "name": "364 days is not annualised", "args": [ [ { "date": "2025-01-02", "value": { "minor": 100000, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } }, { "date": "2026-01-01", "value": { "minor": 110000, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } } ] ], "expect": { "basisPoints": 1000, "subPeriodBasisPoints": [ 1000 ], "days": 364, "annualisedBasisPoints": null } }, { "name": "-0.5 bp rounds away from zero to -1", "args": [ [ { "date": "2025-01-01", "value": { "minor": 1000000, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } }, { "date": "2025-02-01", "value": { "minor": 999950, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } } ] ], "expect": { "basisPoints": -1, "subPeriodBasisPoints": [ -1 ], "days": 31, "annualisedBasisPoints": null } }, { "name": "+0.5 bp rounds away from zero to 1", "args": [ [ { "date": "2025-01-01", "value": { "minor": 1000000, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } }, { "date": "2025-02-01", "value": { "minor": 1000050, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } } ] ], "expect": { "basisPoints": 1, "subPeriodBasisPoints": [ 1 ], "days": 31, "annualisedBasisPoints": null } }, { "name": "a sub-period with no change is 0", "args": [ [ { "date": "2025-01-01", "value": { "minor": 100000, "currency": "GBP" }, "flow": { "minor": 5000, "currency": "GBP" } }, { "date": "2025-06-01", "value": { "minor": 105000, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } }, { "date": "2025-09-01", "value": { "minor": 99750, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } } ] ], "expect": { "basisPoints": -500, "subPeriodBasisPoints": [ 0, -500 ], "days": 243, "annualisedBasisPoints": null } }, { "name": "repeating fractions are linked exactly before rounding", "args": [ [ { "date": "2024-01-01", "value": { "minor": 300, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } }, { "date": "2024-06-01", "value": { "minor": 400, "currency": "GBP" }, "flow": { "minor": -100, "currency": "GBP" } }, { "date": "2025-06-01", "value": { "minor": 400, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } } ] ], "expect": { "basisPoints": 7778, "subPeriodBasisPoints": [ 3333, 3333 ], "days": 517, "annualisedBasisPoints": 5011 } }, { "name": "one point is not a period", "args": [ [ { "date": "2025-01-01", "value": { "minor": 100000, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } } ] ], "expectError": "at least two valuation points are needed" }, { "name": "dates out of order", "args": [ [ { "date": "2025-06-01", "value": { "minor": 100000, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } }, { "date": "2025-01-01", "value": { "minor": 110000, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } } ] ], "expectError": "dates must be strictly increasing" }, { "name": "two points on one date", "args": [ [ { "date": "2025-01-01", "value": { "minor": 100000, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } }, { "date": "2025-01-01", "value": { "minor": 110000, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } } ] ], "expectError": "dates must be strictly increasing" }, { "name": "a negative valuation", "args": [ [ { "date": "2025-01-01", "value": { "minor": 100000, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } }, { "date": "2025-06-01", "value": { "minor": -1, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } } ] ], "expectError": "valuations must not be negative" }, { "name": "everything withdrawn before the end leaves nothing to measure", "args": [ [ { "date": "2025-01-01", "value": { "minor": 100000, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } }, { "date": "2025-06-01", "value": { "minor": 100000, "currency": "GBP" }, "flow": { "minor": -100000, "currency": "GBP" } }, { "date": "2025-09-01", "value": { "minor": 0, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } } ] ], "expectError": "nothing is invested at the start of the sub-period from 2025-06-01" }, { "name": "a flow in another currency", "args": [ [ { "date": "2025-01-01", "value": { "minor": 100000, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } }, { "date": "2025-06-01", "value": { "minor": 100000, "currency": "GBP" }, "flow": { "minor": 500, "currency": "EUR" } }, { "date": "2025-09-01", "value": { "minor": 0, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } } ] ], "expectError": "currency mismatch" }, { "name": "fractional minor units", "args": [ [ { "date": "2025-01-01", "value": { "minor": 100000, "currency": "GBP" }, "flow": { "minor": 0.5, "currency": "GBP" } }, { "date": "2025-06-01", "value": { "minor": 100000, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } } ] ], "expectError": "amounts must be whole minor units" }, { "name": "an impossible date", "args": [ [ { "date": "2025-01-01", "value": { "minor": 100000, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } }, { "date": "2025-02-29", "value": { "minor": 100000, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } } ] ], "expectError": "is not a real calendar date" }, { "name": "more than 36500 days", "args": [ [ { "date": "1900-01-01", "value": { "minor": 100000, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } }, { "date": "2001-01-01", "value": { "minor": 100000, "currency": "GBP" }, "flow": { "minor": 0, "currency": "GBP" } } ] ], "expectError": "points must fall within 36500 days" } ]