rust_finprim/tvm/pv.rs
1use crate::FloatLike;
2
3/// PV - Present Value
4///
5/// A general present value calculation, similar to the Excel `PV` function. Commonly used
6/// for bond pricing and annuity calculations.
7/// The `due` parameter expresses whether the annuity type is an ordinary annuity (false and the default) or an annuity due (true),
8/// Excel provides this parameter as `type` with 0 for ordinary annuity and 1 for annuity due.
9///
10/// The present value (PV) is the current value of a future sum of money or cash flow given a
11/// specified rate of return.
12///
13/// # Arguments
14/// * `rate` - The interest rate per period
15/// * `nper` - The number of compounding periods
16/// * `pmt` - The payment amount per period (negative for cash outflows)
17/// * `fv` (optional) - The future value
18/// * `due` (optional) - The timing of the payment (false = end of period, true = beginning of period), default is false
19/// (ordinary annuity)
20///
21/// At least one of `pmt` or `fv` should be non-zero.
22///
23/// # Returns
24/// The present value (PV)
25///
26/// # Example
27/// 10 Year bond with 3% YTM, $1000 future value, and 5% coupon rate (paid annually)
28/// * 5% interest rate
29/// * 10 compounding periods
30/// * $50 payment per period (5% of $1000)
31/// ```
32/// use rust_finprim::tvm::pv;
33///
34/// let rate = 0.05; let nper = 10.0; let pmt = -50.0; let fv = 1000.0;
35/// pv(rate, nper, pmt, Some(fv), None);
36/// ```
37pub fn pv<T: FloatLike>(rate: T, nper: T, pmt: T, fv: Option<T>, due: Option<bool>) -> T {
38 let fv: T = fv.unwrap_or(T::zero());
39 let due = due.unwrap_or(false);
40
41 let pv = if rate == T::zero() {
42 // Simplified formula when rate is zero
43 fv + (pmt * nper)
44 } else {
45 let nth_power = (T::one() + rate).powf(nper);
46 let fv_discounted = fv / nth_power;
47 let factor = (T::one() - (T::one() / nth_power)) / rate;
48
49 if due {
50 pmt * factor * (T::one() + rate) + fv_discounted
51 } else {
52 pmt * factor + fv_discounted
53 }
54 };
55
56 -pv
57}
58
59/// NPV - Net Present Value
60///
61/// The net present value (NPV) is the difference between the present value of cash inflows and the present value of cash outflows over a period of time.
62/// NPV is used in capital budgeting to analyze the profitability of an investment or project.
63/// The NPV is calculated by discounting all cash flows to the present value using a specified discount rate.
64/// If the NPV is positive, the investment is considered profitable. If the NPV is negative, the investment is considered unprofitable.
65/// Similar to the Excel `NPV` function, with the main difference is that this implementation
66/// assumes the first cash flow is at time value 0 (initial investment).
67///
68/// # Arguments
69/// * `rate` - The discount rate per period
70/// * `cash_flows` - A slice of values representing the cash flows of the investment,
71/// note that the first cash flow is assumed to be at time value 0 (initial investment)
72///
73/// # Returns
74/// * The net present value (NPV)
75///
76/// # Example
77/// * 5% discount rate
78/// * Cash flows of $-100, $50, $40, $30, $20
79/// ```
80/// use rust_finprim::tvm::npv;
81///
82/// let rate = 0.05;
83/// let cash_flows = vec![-100.0, 50.0, 40.0, 30.0, 20.0];
84/// npv(rate, &cash_flows);
85/// ```
86/// # Formula
87/// The NPV is calculated by discounting all cash flows to the present value using a specified discount rate.
88/// The formula is:
89/// $$NPV = \sum_{t=0}^{n} \frac{CF_t}{(1+r)^t}$$
90/// Where:
91/// * \\(CF_t\\) = cash flow at time \\(t\\)
92/// * \\(r\\) = discount rate
93pub fn npv<T: FloatLike>(rate: T, cash_flows: &[T]) -> T {
94 // Accumulator for the power of (1 + rate)
95 // (1 + rate)^(t) so first iteration is (1 + rate)^0 = 1
96 let mut powf_acc = T::one();
97 let mut npv = T::zero();
98 for &cf in cash_flows.iter() {
99 npv += cf / (powf_acc);
100 powf_acc *= T::one() + rate;
101 }
102 npv
103}
104
105/// NPV Differing Rates - Net Present Value with differing discount rates
106///
107/// The net present value (NPV) is the difference between the present value of cash inflows and the present value of cash outflows over a period of time.
108/// NPV is used in capital budgeting to analyze the profitability of an investment or project.
109/// The NPV is calculated by discounting all cash flows to the present value using a specified discount rate.
110/// If the NPV is positive, the investment is considered profitable. If the NPV is negative, the investment is considered unprofitable.
111/// This function allows for differing discount rates for each cash flow.
112///
113/// # Arguments
114/// * `flow_table` - A slice of tuples representing the cash flows and discount rates for each period `(cash_flow, discount_rate)`,
115/// note that the first cash flow is assumed to be at time value 0 (initial investment)
116///
117/// # Returns
118/// * The net present value (NPV)
119///
120/// # Example
121/// * Cash flows of $-100, $50, $40, $30, $20
122/// * Discount rates of 5%, 6%, 7%, 8%, 9%
123/// ```
124/// use rust_finprim::tvm::npv_differing_rates;
125///
126/// let flow_table = vec![
127/// (-100.0, 0.05),
128/// (50.0, 0.06),
129/// (40.0, 0.07),
130/// (30.0, 0.08),
131/// (20.0, 0.09),
132/// ];
133/// npv_differing_rates(&flow_table);
134/// ```
135///
136/// # Formula
137/// The NPV is calculated by discounting all cash flows to the present value using a specified discount rate.
138/// $$NPV = \sum_{t=0}^{n} \frac{CF_t}{(1+r_t)^t}$$
139/// Where:
140/// * \\(CF_t\\) = cash flow at time \\(t\\)
141/// * \\(r_t\\) = discount rate at time \\(t\\)
142pub fn npv_differing_rates<T: FloatLike>(flow_table: &[(T, T)]) -> T {
143 flow_table
144 .iter()
145 .enumerate()
146 .map(|(t, &(cf, rate))| cf / (T::one() + rate).powf(T::from_usize(t)))
147 .sum()
148}
149
150/// XNPV - Net Present Value for irregular cash flows
151///
152/// The XNPV function calculates the net present value of a series of cash flows that are not necessarily periodic.
153///
154/// # Arguments
155/// * `rate` - The discount rate
156/// * `flow_table` - A slice of tuples representing the cash flows and dates for each period `(cash_flow, date)`
157/// where `date` represents the number of days from an arbitrary epoch. The first cash flow
158/// is assumed to be the initial investment date, the order of subsequent cash flows does
159/// not matter.
160///
161/// Most time libraries will provide a method yielding the number of days from an epoch. For example, in the `chrono` library
162/// you can use the `num_days_from_ce` method to get the number of days from the Common Era (CE) epoch, simply convert
163/// your date types to an integer representing the number of days from any epoch. Alternatively, you can calculate the
164/// time delta in days from an arbitrary epoch, such as the initial investment date.
165///
166/// Cash flows are discounted assuming a 365-day year.
167///
168/// # Returns
169/// * The net present value (NPV)
170///
171/// # Example
172/// * 5% discount rate
173/// * Cash flows of $-100, $50, $40, $30, $20
174/// * Dates of 0, 365, 420, 1360, 1460
175///
176/// ```
177/// use rust_finprim::tvm::xnpv;
178///
179/// let rate = 0.05;
180/// let flows_table = vec![
181/// (-100.0, 0),
182/// (50.0, 365),
183/// (40.0, 420),
184/// (30.0, 1360),
185/// (20.0, 1460),
186/// ];
187/// xnpv(rate, &flows_table);
188pub fn xnpv<T: FloatLike>(rate: T, flow_table: &[(T, i32)]) -> T {
189 // First date should be 0 (initial investment) and the rest should be difference from the initial date
190 let init_date = flow_table.first().unwrap().1;
191
192 let one_plus_r = T::one() + rate;
193 let yr_length: T = T::from_u16(365);
194 flow_table
195 .iter()
196 .map(|&(cf, date)| {
197 let years = T::from_i32(date - init_date) / yr_length;
198 cf / (one_plus_r).powf(years)
199 })
200 .sum()
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[cfg(not(feature = "std"))]
208 extern crate std;
209 #[cfg(not(feature = "std"))]
210 use std::{assert, vec, vec::Vec};
211
212 #[test]
213 fn test_xnpv() {
214 let rate = 0.05;
215 let flows_table = vec![(-100.0, 0), (50.0, 365), (40.0, 730), (30.0, 1095), (20.0, 1460)];
216
217 let result = xnpv(rate, &flows_table);
218 let expected: f64 = 26.26940;
219 assert!(
220 (result - expected).abs() < 1e-5,
221 "Failed on case: {}. Expected: {}, Result: {}",
222 "5% discount rate, cash flows of -100, 50, 40, 30, 20",
223 expected,
224 result
225 );
226 }
227
228 #[test]
229 fn test_pv() {
230 struct TestCase {
231 rate: f64,
232 nper: f64,
233 pmt: f64,
234 fv: Option<f64>,
235 due: Option<bool>,
236 expected: f64,
237 description: &'static str,
238 }
239 impl TestCase {
240 fn new(
241 rate: f64,
242 nper: f64,
243 pmt: f64,
244 fv: Option<f64>,
245 due: Option<bool>,
246 expected: f64,
247 description: &'static str,
248 ) -> TestCase {
249 TestCase {
250 rate,
251 nper,
252 pmt,
253 fv,
254 due,
255 expected,
256 description,
257 }
258 }
259 }
260
261 let cases = [
262 TestCase::new(
263 0.05,
264 10.0,
265 100.0,
266 None,
267 None,
268 -772.17349,
269 "Standard case with 5% rate, 10 periods, and $100 pmt",
270 ),
271 TestCase::new(
272 0.05,
273 10.0,
274 100.0,
275 None,
276 Some(true),
277 -810.78217,
278 "Payment at the beg of period should result in higher present value",
279 ),
280 TestCase::new(0.0, 10.0, -100.0, None, None, 1000.0, "Zero interest rate no growth"),
281 TestCase::new(
282 0.05,
283 10.0,
284 100.0,
285 Some(1000.0),
286 None,
287 -1386.08675,
288 "Bond with 5% rate, 10 periods, 10% coupon, and $1000 future value",
289 ),
290 TestCase::new(
291 0.05,
292 10.0,
293 0.0,
294 Some(2000.0),
295 None,
296 -1227.82651,
297 "No cash flows, just a future pay out",
298 ),
299 ];
300
301 for case in &cases {
302 let calculated_pv = pv(case.rate, case.nper, case.pmt, case.fv, case.due);
303 assert!(
304 (calculated_pv - case.expected).abs() < 1e-5,
305 "Failed on case: {}. Expected {}, got {}",
306 case.description,
307 case.expected,
308 calculated_pv
309 );
310 }
311 }
312
313 #[test]
314 fn test_npv() {
315 struct TestCase {
316 rate: f64,
317 cash_flows: Vec<f64>,
318 expected: f64,
319 description: &'static str,
320 }
321 impl TestCase {
322 fn new(rate: f64, cash_flows: Vec<f64>, expected: f64, description: &'static str) -> TestCase {
323 TestCase {
324 rate,
325 cash_flows,
326 expected,
327 description,
328 }
329 }
330 }
331
332 let cases = [
333 TestCase::new(
334 0.05,
335 vec![-100.0, 50.0, 40.0, 30.0, 20.0],
336 26.26940,
337 "Standard case with 5% rate and cash flows of -100, 50, 40, 30, 20",
338 ),
339 TestCase::new(
340 0.05,
341 vec![100.0, 50.0, 40.0, 30.0, 20.0],
342 226.26940,
343 "All positive cash flows",
344 ),
345 TestCase::new(
346 0.05,
347 vec![-100.0, 50.0, 40.0, 30.0, 20.0, 1000.0],
348 809.79557,
349 "Additional future cash flow should increase NPV",
350 ),
351 ];
352
353 for case in &cases {
354 let calculated_npv = npv(case.rate, &case.cash_flows);
355 assert!(
356 (calculated_npv - case.expected).abs() < 1e-5,
357 "Failed on case: {}. Expected {}, got {}",
358 case.description,
359 case.expected,
360 calculated_npv
361 );
362 }
363 }
364
365 #[test]
366 fn test_npv_differing_rates() {
367 struct TestCase {
368 flow_table: Vec<(f64, f64)>,
369 expected: f64,
370 description: &'static str,
371 }
372 impl TestCase {
373 fn new(rates: Vec<f64>, cash_flows: Vec<f64>, expected: f64, description: &'static str) -> TestCase {
374 let flow_table = cash_flows.iter().zip(rates.iter()).map(|(&cf, &r)| (cf, r)).collect();
375 TestCase {
376 flow_table,
377 expected,
378 description,
379 }
380 }
381 }
382
383 let cases = [
384 TestCase::new(
385 vec![0.05, 0.06, 0.07, 0.08, 0.09],
386 vec![-100.0, 50.0, 40.0, 30.0, 20.0],
387 20.09083,
388 "Increasing rate and cash flows of -100, 50, 40, 30, 20",
389 ),
390 TestCase::new(
391 vec![0.05, 0.06, 0.07, 0.08, 0.09],
392 vec![100.0, 50.0, 40.0, 30.0, 20.0],
393 220.09083,
394 "All positive cash flows",
395 ),
396 TestCase::new(
397 vec![0.05, 0.06, 0.07, 0.08, 0.09, 0.1],
398 vec![-100.0, 50.0, 40.0, 30.0, 20.0, 1000.0],
399 641.01215,
400 "Additional future cash flow should increase NPV",
401 ),
402 ];
403
404 for case in &cases {
405 let calculated_npv = npv_differing_rates(&case.flow_table);
406 assert!(
407 (calculated_npv - case.expected).abs() < 1e-5,
408 "Failed on case: {}. Expected {}, got {}",
409 case.description,
410 case.expected,
411 calculated_npv
412 );
413 }
414 }
415}