Skip to content

Commit bd8418f

Browse files
authored
feat: Added basic finance ratios, NPV sensitivity and Treynor ratio to the financial module (#937)
1 parent 83ad4d9 commit bd8418f

File tree

8 files changed

+247
-0
lines changed

8 files changed

+247
-0
lines changed

DIRECTORY.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@
105105
* [Word Break](https://github.com/TheAlgorithms/Rust/blob/master/src/dynamic_programming/word_break.rs)
106106
* Financial
107107
* [Present Value](https://github.com/TheAlgorithms/Rust/blob/master/src/financial/present_value.rs)
108+
* [Net Present Value](https://github.com/TheAlgorithms/Rust/blob/master/src/financial/npv.rs)
109+
* [NPV Sensitivity](https://github.com/TheAlgorithms/Rust/blob/master/src/financial/npv_sensitivity.rs)
110+
* [Compound Interest](https://github.com/TheAlgorithms/Rust/blob/master/src/financial/compound_interest.rs)
111+
* [Payback Period](https://github.com/TheAlgorithms/Rust/blob/master/src/financial/payback.rs)
112+
* [Finance Ratios](https://github.com/TheAlgorithms/Rust/blob/master/src/financial/finance_ratios.rs)
113+
* [Treynor Ratio](https://github.com/TheAlgorithms/Rust/blob/master/src/financial/treynor_ratio.rs)
108114
* General
109115
* [Convex Hull](https://github.com/TheAlgorithms/Rust/blob/master/src/general/convex_hull.rs)
110116
* [Fisher Yates Shuffle](https://github.com/TheAlgorithms/Rust/blob/master/src/general/fisher_yates_shuffle.rs)

src/financial/compound_interest.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// compound interest is given by A = P(1+r/n)^nt
2+
// where: A = Final Amount, P = Principal Amount, r = rate of interest,
3+
// n = number of times interest is compounded per year and t = time (in years)
4+
5+
pub fn compound_interest(principal: f64, rate: f64, comp_per_year: u32, years: f64) -> f64 {
6+
principal * (1.00 + rate / comp_per_year as f64).powf(comp_per_year as f64 * years)
7+
}
8+
9+
#[cfg(test)]
10+
mod tests {
11+
use super::*;
12+
13+
#[test]
14+
fn test_compound_interest() {
15+
let principal = 1000.0;
16+
let rate = 0.05; // 5% annual interest
17+
let times_per_year = 4; // interest compounded quarterly
18+
let years = 2.0; // 2 years tenure
19+
let result = compound_interest(principal, rate, times_per_year, years);
20+
assert!((result - 1104.486).abs() < 0.001); // expected value rounded to 3 decimal
21+
// places
22+
}
23+
}

src/financial/finance_ratios.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Calculating simple ratios like Return on Investment (ROI), Debt to Equity, Gross Profit Margin
2+
// and Earnings per Sale (EPS)
3+
pub fn return_on_investment(gain: f64, cost: f64) -> f64 {
4+
(gain - cost) / cost
5+
}
6+
7+
pub fn debt_to_equity(debt: f64, equity: f64) -> f64 {
8+
debt / equity
9+
}
10+
11+
pub fn gross_profit_margin(revenue: f64, cost: f64) -> f64 {
12+
(revenue - cost) / revenue
13+
}
14+
15+
pub fn earnings_per_sale(net_income: f64, pref_dividend: f64, share_avg: f64) -> f64 {
16+
(net_income - pref_dividend) / share_avg
17+
}
18+
19+
#[cfg(test)]
20+
mod tests {
21+
use super::*;
22+
23+
#[test]
24+
fn test_return_on_investment() {
25+
// let gain = 1200, cost = 1000 thus, ROI = (1200 - 1000)/1000 = 0.2
26+
let result = return_on_investment(1200.0, 1000.0);
27+
assert!((result - 0.2).abs() < 0.001);
28+
}
29+
30+
#[test]
31+
fn test_debt_to_equity() {
32+
// let debt = 300, equity = 150 thus, debt to equity ratio = 300/150 = 2
33+
let result = debt_to_equity(300.0, 150.0);
34+
assert!((result - 2.0).abs() < 0.001);
35+
}
36+
37+
#[test]
38+
fn test_gross_profit_margin() {
39+
// let revenue = 1000, cost = 800 thus, gross profit margin = (1000-800)/1000 = 0.2
40+
let result = gross_profit_margin(1000.0, 800.0);
41+
assert!((result - 0.2).abs() < 0.01);
42+
}
43+
44+
#[test]
45+
fn test_earnings_per_sale() {
46+
// let net_income = 350, pref_dividend = 50, share_avg = 25 this EPS = (350-50)/25 = 12
47+
let result = earnings_per_sale(350.0, 50.0, 25.0);
48+
assert!((result - 12.0).abs() < 0.001);
49+
}
50+
}

src/financial/mod.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,18 @@
1+
mod compound_interest;
2+
mod finance_ratios;
3+
mod npv;
4+
mod npv_sensitivity;
5+
mod payback;
16
mod present_value;
7+
mod treynor_ratio;
8+
pub use compound_interest::compound_interest;
9+
pub use npv::npv;
10+
pub use npv_sensitivity::npv_sensitivity;
11+
pub use payback::payback;
212
pub use present_value::present_value;
13+
pub use treynor_ratio::treynor_ratio;
14+
15+
pub use finance_ratios::debt_to_equity;
16+
pub use finance_ratios::earnings_per_sale;
17+
pub use finance_ratios::gross_profit_margin;
18+
pub use finance_ratios::return_on_investment;

src/financial/npv.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/// Calculates Net Present Value given a vector of cash flows and a discount rate.
2+
/// cash_flows: Vector of f64 representing cash flows for each period.
3+
/// rate: Discount rate as an f64 (e.g., 0.05 for 5%)
4+
5+
pub fn npv(cash_flows: &[f64], rate: f64) -> f64 {
6+
cash_flows
7+
.iter()
8+
.enumerate()
9+
.map(|(t, &cf)| cf / (1.00 + rate).powi(t as i32))
10+
.sum()
11+
}
12+
13+
// tests
14+
15+
#[cfg(test)]
16+
mod tests {
17+
use super::*;
18+
19+
#[test]
20+
fn test_npv_basic() {
21+
let cash_flows = vec![-1000.0, 300.0, 400.0, -50.0];
22+
let rate = 0.10;
23+
let result = npv(&cash_flows, rate);
24+
// Calculated value ≈ -434.25
25+
assert!((result - (-434.25)).abs() < 0.05); // Allow small margin of error
26+
}
27+
28+
#[test]
29+
fn test_npv_zero_rate() {
30+
let cash_flows = vec![100.0, 200.0, -50.0];
31+
let rate = 0.0;
32+
let result = npv(&cash_flows, rate);
33+
assert!((result - 250.0).abs() < 0.05);
34+
}
35+
36+
#[test]
37+
fn test_npv_empty() {
38+
// For empty cash flows: NPV should be 0
39+
let cash_flows: Vec<f64> = vec![];
40+
let rate = 0.05;
41+
let result = npv(&cash_flows, rate);
42+
assert_eq!(result, 0.0);
43+
}
44+
}

src/financial/npv_sensitivity.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/// Computes the Net Present Value (NPV) of a cash flow series
2+
/// at multiple discount rates to show sensitivity.
3+
///
4+
/// # Inputs:
5+
/// - `cash_flows`: A slice of cash flows, where each entry is a period value
6+
/// e.g., year 0 is initial investment, year 1+ are returns or costs
7+
/// - `discount_rates`: A slice of discount rates, e.g. `[0.05, 0.10, 0.20]`,
8+
/// where each rate is evaluated independently.
9+
///
10+
/// # Output:
11+
/// - Returns a vector of NPV values, each corresponding to a rate in `discount_rates`.
12+
/// For example, output is `[npv_rate1, npv_rate2, ...]`.
13+
14+
pub fn npv_sensitivity(cash_flows: &[f64], discount_rates: &[f64]) -> Vec<f64> {
15+
discount_rates
16+
.iter()
17+
.cloned()
18+
.map(|rate| {
19+
cash_flows
20+
.iter()
21+
.enumerate()
22+
.map(|(t, &cf)| cf / (1.0 + rate).powi(t as i32))
23+
.sum()
24+
})
25+
.collect()
26+
}
27+
28+
#[cfg(test)]
29+
mod tests {
30+
use super::*;
31+
#[test]
32+
fn test_npv_sensitivity() {
33+
let cashflows = [-1000.00, 400.00, 400.00, 400.00];
34+
let rates = [0.05, 0.1, 0.2];
35+
let expected = [89.30, -5.26, -157.41];
36+
let out = npv_sensitivity(&cashflows, &rates);
37+
assert_eq!(out.len(), 3);
38+
// value check
39+
for (o, e) in out.iter().zip(expected.iter()) {
40+
assert!((o - e).abs() < 0.1);
41+
}
42+
}
43+
}

src/financial/payback.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/// Returns the payback period in years
2+
/// If investment is not paid back, returns None.
3+
4+
pub fn payback(cash_flow: &[f64]) -> Option<usize> {
5+
let mut total = 0.00;
6+
for (year, &cf) in cash_flow.iter().enumerate() {
7+
total += cf;
8+
if total >= 0.00 {
9+
return Some(year);
10+
}
11+
}
12+
None
13+
}
14+
15+
#[cfg(test)]
16+
mod tests {
17+
use super::*;
18+
19+
#[test]
20+
fn test_payback() {
21+
let cash_flows = vec![-1000.0, 300.0, 400.0, 500.0];
22+
assert_eq!(payback(&cash_flows), Some(3)); // paid back in year 3
23+
}
24+
25+
#[test]
26+
fn test_no_payback() {
27+
let cash_flows = vec![-1000.0, 100.0, 100.0, 100.0];
28+
assert_eq!(payback(&cash_flows), None); // never paid back
29+
}
30+
}

src/financial/treynor_ratio.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/// Calculates the Treynor Ratio for a portfolio.
2+
///
3+
/// # Inputs
4+
/// - `portfolio_return`: Portfolio return
5+
/// - `risk_free_rate`: Risk-free rate
6+
/// - `beta`: Portfolio beta
7+
/// where Beta is a financial metric that measures the systematic risk of a security or portfolio compared to the overall market.
8+
///
9+
/// # Output
10+
/// - Returns excess return per unit of market risk
11+
pub fn treynor_ratio(portfolio_return: f64, risk_free_rate: f64, beta: f64) -> f64 {
12+
if beta == 0.0 {
13+
f64::NAN
14+
} else {
15+
(portfolio_return - risk_free_rate) / beta
16+
}
17+
}
18+
19+
#[cfg(test)]
20+
mod tests {
21+
use super::*;
22+
23+
#[test]
24+
fn test_treynor_ratio() {
25+
// for portfolio_return = 0.10, risk_free_rate = 0.05, beta = 1.5
26+
// expected result: (0.10 - 0.05) / 1.5 = 0.033333...
27+
assert!((treynor_ratio(0.10, 0.05, 1.50) - 0.03333).abs() < 0.01);
28+
}
29+
30+
#[test]
31+
fn test_treynor_ratio_empty_beta() {
32+
// test for zero beta (undefined ratio)
33+
assert!(treynor_ratio(0.10, 0.05, 0.00).is_nan());
34+
}
35+
}

0 commit comments

Comments
 (0)