1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
//! # Portfolio Applications
//!
//! Provides functionality for portfolio selection and optimisation. The module contains functionality for performing portfolio selection,
//! measuring portfolio performance and risk.
// Re-Exports
pub use self::portfolio_performance::{PortfolioPerformanceMetric, SharpeRatio, InformationRatio, TreynorRatio, JensensAlpha, SortinoRatio};
pub use self::portfolio_taxonomy::PortfolioTaxonomy;
pub use self::risk_measures::{value_at_risk, expected_shortfall};
pub use self::utility_functions::{cara, crra};
pub use self::portfolio_composition::{AssetReturnsType, PortfolioReturnsType, PortfolioOptimisationResult, EfficientFrontier, Asset, generate_portfolio, Portfolio};
pub mod portfolio_performance;
pub mod portfolio_taxonomy;
pub mod risk_measures;
pub mod utility_functions;
pub mod portfolio_composition;
use ndarray::{Array1, s};
#[cfg(feature = "serde")]
use serde::{Serialize, Deserialize};
use crate::error::{DigiFiError, ErrorTitle};
use crate::utilities::{compare_len, Time};
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
/// Struct with data to be used inside the InstumentsPortfolio.
pub struct AssetHistData {
/// Historical price series of the instrument
price_array: Array1<f64>,
/// An array of preditable income readings (e.g., dividends for stocks, copouns for bonds, overnight fees, etc.)
predictable_income: Array1<f64>,
/// An array of time accompanying the price series
pub time: Time,
}
impl AssetHistData {
/// Creates a new `AssetHistData` instance.
///
/// # Input
/// - `price_array`: Historical price series of the instrument
/// - `predictable_income`: An array of preditable income readings (e.g., dividends for stocks, copouns for bonds, overnight fees, etc.)
/// - `time_array`: An array of time accompanying the price series
///
/// # Errors
/// - Returns an error if the length of `price_array`, `predictable_income` and/or `time_array` do not match.
pub fn build(price_array: Array1<f64>, predictable_income: Array1<f64>, time: Time) -> Result<Self, DigiFiError> {
compare_len(&price_array.iter(), &predictable_income.iter(), "price_array", "predictable_income")?;
compare_len(&price_array.iter(), &time.time_array().iter(), "price_array", "time_array")?;
Ok(Self { price_array, predictable_income, time })
}
/// Validation method for an index.
///
/// # Input
/// - `index`: Time index beyond which no data will be returned
/// - `index_lable`: Label of the index that will be included in the error message
///
/// # Errors
/// - Returns an error if the index provided is out of bounds for the price array.
fn validate_index(&self, index: usize, index_label: &str) -> Result<(), DigiFiError> {
if self.price_array.len() < index {
return Err(DigiFiError::IndexOutOfRange { title: Self::error_title(), index: index_label.to_owned(), array: "price array".to_owned(), });
}
Ok(())
}
/// Validates the pair of indices.
fn validate_index_pair(&self, end_index: usize, start_index: Option<usize>) -> Result<(usize, usize), DigiFiError> {
self.validate_index(end_index, "end_index")?;
let start_index: usize = match start_index {
Some(index) => {
self.validate_index(index, "start_index")?;
if end_index <= index {
return Err(DigiFiError::ParameterConstraint {
title: Self::error_title(),
constraint: "The argument `start_index` must be smaller than the `end_index`.".to_owned(),
});
}
index
},
None => 0,
};
Ok((start_index, end_index))
}
/// Returns the number of datapoints in the price time series.
///
/// Note: Predictable income and time arrays will have the same length as the price array.
pub fn len(&self) -> usize {
self.price_array.len()
}
/// Safe method for working with historical price data.
///
/// This method prevents the user from using the future prices based on the indices value provided.
///
/// # Input
/// - `end_index`: Time index beyond which no data will be returned
/// - `start_index`: Time index below which no data will be returned
///
/// # Output
/// - Historical and/or current prices(s)
///
/// # Errors
/// - Returns an error if the index provided is out of bounds for the price array.
pub fn price_slice(&self, end_index: usize, start_index: Option<usize>) -> Result<Array1<f64>, DigiFiError> {
let (start_index, end_index) = self.validate_index_pair(end_index, start_index)?;
Ok(self.price_array.slice(s![start_index..end_index]).to_owned())
}
/// Returns the clone of the entire price time series.
pub fn price_clone(&self) -> Array1<f64> {
self.price_array.clone()
}
/// Safe method for working with historical predictable income data.
///
/// This method prevents the user from using the future predictable incomes based on the indices value provided.
///
/// # Input
/// - `end_index`: Time index beyond which no data will be returned
/// - `start_index`: Time index below which no data will be returned
///
/// # Output
/// - Historical and/or current predictable income(s)
pub fn predictable_income_slice(&self, end_index: usize, start_index: Option<usize>) -> Result<Array1<f64>, DigiFiError> {
let (start_index, end_index) = self.validate_index_pair(end_index, start_index)?;
Ok(self.predictable_income.slice(s![start_index..end_index]).to_owned())
}
/// Returns the clone of the entire predictable income time series.
pub fn predictable_income_clone(&self) -> Array1<f64> {
self.predictable_income.clone()
}
}
impl ErrorTitle for AssetHistData {
fn error_title() -> String {
String::from("Asset Historical Data (AssetHistData)")
}
}
/// Provides access to the historical data of the financial instrument.
pub trait PortfolioInstrument {
/// Returns asset name/label.
fn asset_name(&self) -> String;
/// Returns the historical data about the financial instrument.
fn historical_data(&self) -> &AssetHistData;
}
/// Type of returns calculation.
pub enum ReturnsMethod {
/// Computes returns of the mean returns per interval of the time series (e.g., average daily returns) and then extrapolates it over a specified period
ImpliedAverageReturn,
/// Computes compounded return of the time series and then reduces it to the specified period
EstimatedFromTotalReturn,
}
/// Calculate the average return of returrns series.
///
/// # Input
/// - `returns`: Price time series
/// - `method`: Method for computing the returns
/// - `n_periods`: Number of periods used to estimate the average over (e.g., for daily prices n_periods=252 produces annualized average)
///
/// # Output
/// - Average return over a certain period
pub fn returns_average(returns: &Array1<f64>, method: &ReturnsMethod, n_periods: usize) -> Result<f64, DigiFiError> {
match method {
ReturnsMethod::ImpliedAverageReturn => {
let mean: f64 = returns.mean().ok_or(DigiFiError::MeanCalculation { title: "Returns Average".to_owned(), series: "returns".to_owned(), })?;
Ok((1.0 + mean).powi(n_periods as i32) - 1.0)
},
ReturnsMethod::EstimatedFromTotalReturn => {
let returns_len: f64 = returns.len() as f64;
Ok((1.0 + returns).product().powf((n_periods as f64)/returns_len) - 1.0)
},
}
}