Functional Weave
Code in TypeScript

invest.time-weighted-return@1.0.1

vectors.json

13,135 bytes · view raw

[
 {
  "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"
 }
]