@ebowwa/quant-rust 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -0
- package/bun-ffi.d.ts +54 -0
- package/dist/index.js +576 -0
- package/dist/src/index.d.ts +324 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/types/index.d.ts +403 -0
- package/dist/types/index.d.ts.map +1 -0
- package/native/README.md +62 -0
- package/native/darwin-arm64/libquant_rust.dylib +0 -0
- package/package.json +70 -0
- package/scripts/postinstall.cjs +85 -0
- package/src/ffi.rs +496 -0
- package/src/index.ts +1073 -0
- package/src/indicators/ma.rs +222 -0
- package/src/indicators/mod.rs +18 -0
- package/src/indicators/momentum.rs +353 -0
- package/src/indicators/sr.rs +195 -0
- package/src/indicators/trend.rs +351 -0
- package/src/indicators/volatility.rs +270 -0
- package/src/indicators/volume.rs +213 -0
- package/src/lib.rs +130 -0
- package/src/patterns/breakout.rs +431 -0
- package/src/patterns/chart.rs +772 -0
- package/src/patterns/mod.rs +394 -0
- package/src/patterns/sr.rs +423 -0
- package/src/prediction/amm.rs +338 -0
- package/src/prediction/arbitrage.rs +230 -0
- package/src/prediction/calibration.rs +317 -0
- package/src/prediction/kelly.rs +232 -0
- package/src/prediction/lmsr.rs +194 -0
- package/src/prediction/mod.rs +59 -0
- package/src/prediction/odds.rs +229 -0
- package/src/prediction/pnl.rs +254 -0
- package/src/prediction/risk.rs +228 -0
- package/src/risk/beta.rs +257 -0
- package/src/risk/drawdown.rs +256 -0
- package/src/risk/leverage.rs +201 -0
- package/src/risk/mod.rs +388 -0
- package/src/risk/portfolio.rs +287 -0
- package/src/risk/ratios.rs +290 -0
- package/src/risk/sizing.rs +194 -0
- package/src/risk/var.rs +222 -0
- package/src/stats/cdf.rs +257 -0
- package/src/stats/correlation.rs +225 -0
- package/src/stats/distribution.rs +194 -0
- package/src/stats/hypothesis.rs +177 -0
- package/src/stats/matrix.rs +346 -0
- package/src/stats/mod.rs +257 -0
- package/src/stats/regression.rs +239 -0
- package/src/stats/rolling.rs +193 -0
- package/src/stats/timeseries.rs +263 -0
- package/src/types.rs +224 -0
- package/src/utils/mod.rs +215 -0
- package/src/utils/normalize.rs +192 -0
- package/src/utils/price.rs +167 -0
- package/src/utils/quantiles.rs +177 -0
- package/src/utils/returns.rs +158 -0
- package/src/utils/rolling.rs +97 -0
- package/src/utils/stats.rs +154 -0
- package/types/index.ts +513 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
//! Risk-Adjusted Return Ratios
|
|
2
|
+
//!
|
|
3
|
+
//! Sharpe, Sortino, Calmar, Treynor, and Information ratios.
|
|
4
|
+
|
|
5
|
+
use serde::{Deserialize, Serialize};
|
|
6
|
+
|
|
7
|
+
use crate::utils::{mean, std_dev, sum};
|
|
8
|
+
|
|
9
|
+
use super::drawdown::calculate_drawdown;
|
|
10
|
+
|
|
11
|
+
/// Calculate Sharpe Ratio
|
|
12
|
+
///
|
|
13
|
+
/// Measures risk-adjusted returns using total volatility.
|
|
14
|
+
///
|
|
15
|
+
/// # Arguments
|
|
16
|
+
/// * `returns` - Array of periodic returns
|
|
17
|
+
/// * `risk_free_rate` - Annual risk-free rate (e.g., 0.04 for 4%)
|
|
18
|
+
/// * `periods_per_year` - Number of periods in a year (252 for daily, 12 for monthly)
|
|
19
|
+
///
|
|
20
|
+
/// # Returns
|
|
21
|
+
/// Annualized Sharpe ratio
|
|
22
|
+
///
|
|
23
|
+
/// # Example
|
|
24
|
+
/// ```
|
|
25
|
+
/// use quant_rust::risk::ratios::sharpe_ratio;
|
|
26
|
+
/// let returns = vec![0.01, 0.02, -0.01, 0.03, -0.02, 0.01, 0.02, 0.01, -0.01, 0.02];
|
|
27
|
+
/// let sharpe = sharpe_ratio(&returns, 0.04, 252);
|
|
28
|
+
/// assert!(sharpe.is_finite());
|
|
29
|
+
/// ```
|
|
30
|
+
pub fn sharpe_ratio(returns: &[f64], risk_free_rate: f64, periods_per_year: usize) -> f64 {
|
|
31
|
+
if returns.is_empty() {
|
|
32
|
+
return 0.0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let avg_return = mean(returns);
|
|
36
|
+
let sd = std_dev(returns, false); // Sample std dev
|
|
37
|
+
|
|
38
|
+
if sd == 0.0 {
|
|
39
|
+
return 0.0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Annualize
|
|
43
|
+
let annualized_return = avg_return * periods_per_year as f64;
|
|
44
|
+
let annualized_vol = sd * (periods_per_year as f64).sqrt();
|
|
45
|
+
|
|
46
|
+
(annualized_return - risk_free_rate) / annualized_vol
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// Calculate Sortino Ratio
|
|
50
|
+
///
|
|
51
|
+
/// Like Sharpe, but only penalizes downside volatility.
|
|
52
|
+
///
|
|
53
|
+
/// # Arguments
|
|
54
|
+
/// * `returns` - Array of periodic returns
|
|
55
|
+
/// * `risk_free_rate` - Annual risk-free rate
|
|
56
|
+
/// * `periods_per_year` - Number of periods in a year
|
|
57
|
+
///
|
|
58
|
+
/// # Returns
|
|
59
|
+
/// Annualized Sortino ratio
|
|
60
|
+
///
|
|
61
|
+
/// # Example
|
|
62
|
+
/// ```
|
|
63
|
+
/// use quant_rust::risk::ratios::sortino_ratio;
|
|
64
|
+
/// let returns = vec![0.01, 0.02, -0.01, 0.03, -0.02, 0.01, 0.02, 0.01, -0.01, 0.02];
|
|
65
|
+
/// let sortino = sortino_ratio(&returns, 0.04, 252);
|
|
66
|
+
/// assert!(sortino.is_finite() || sortino.is_infinite());
|
|
67
|
+
/// ```
|
|
68
|
+
pub fn sortino_ratio(returns: &[f64], risk_free_rate: f64, periods_per_year: usize) -> f64 {
|
|
69
|
+
if returns.is_empty() {
|
|
70
|
+
return 0.0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let avg_return = mean(returns);
|
|
74
|
+
let target_return = risk_free_rate / periods_per_year as f64;
|
|
75
|
+
|
|
76
|
+
// Downside deviation
|
|
77
|
+
let downside_returns: Vec<f64> = returns.iter().filter(|&&r| r < target_return).cloned().collect();
|
|
78
|
+
|
|
79
|
+
if downside_returns.is_empty() {
|
|
80
|
+
return f64::INFINITY;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let squared_downside: Vec<f64> = downside_returns.iter().map(|r| (r - target_return).powi(2)).collect();
|
|
84
|
+
let downside_deviation = (sum(&squared_downside) / returns.len() as f64).sqrt();
|
|
85
|
+
|
|
86
|
+
if downside_deviation == 0.0 {
|
|
87
|
+
return 0.0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let annualized_return = avg_return * periods_per_year as f64;
|
|
91
|
+
let annualized_downside = downside_deviation * (periods_per_year as f64).sqrt();
|
|
92
|
+
|
|
93
|
+
(annualized_return - risk_free_rate) / annualized_downside
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// Calculate Calmar Ratio
|
|
97
|
+
///
|
|
98
|
+
/// Return divided by maximum drawdown.
|
|
99
|
+
///
|
|
100
|
+
/// # Arguments
|
|
101
|
+
/// * `returns` - Array of periodic returns
|
|
102
|
+
/// * `equity_curve` - Array of equity values over time
|
|
103
|
+
/// * `periods_per_year` - Number of periods in a year
|
|
104
|
+
///
|
|
105
|
+
/// # Returns
|
|
106
|
+
/// Calmar ratio
|
|
107
|
+
///
|
|
108
|
+
/// # Example
|
|
109
|
+
/// ```
|
|
110
|
+
/// use quant_rust::risk::ratios::calmar_ratio;
|
|
111
|
+
/// let returns = vec![0.01, 0.02, -0.01, 0.03, -0.02, 0.01, 0.02, 0.01, -0.01, 0.02];
|
|
112
|
+
/// let equity = vec![100.0, 101.0, 103.0, 102.0, 105.0, 103.0, 104.0, 106.0, 107.0, 106.0, 108.0];
|
|
113
|
+
/// let calmar = calmar_ratio(&returns, &equity, 252);
|
|
114
|
+
/// assert!(calmar.is_finite());
|
|
115
|
+
/// ```
|
|
116
|
+
pub fn calmar_ratio(returns: &[f64], equity_curve: &[f64], periods_per_year: usize) -> f64 {
|
|
117
|
+
if returns.is_empty() || equity_curve.is_empty() {
|
|
118
|
+
return 0.0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let dd_result = calculate_drawdown(equity_curve);
|
|
122
|
+
if dd_result.max_drawdown == 0.0 {
|
|
123
|
+
return 0.0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let annualized_return = mean(returns) * periods_per_year as f64;
|
|
127
|
+
|
|
128
|
+
annualized_return / dd_result.max_drawdown
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// Calculate Treynor Ratio
|
|
132
|
+
///
|
|
133
|
+
/// Risk-adjusted return relative to systematic risk (beta).
|
|
134
|
+
///
|
|
135
|
+
/// # Arguments
|
|
136
|
+
/// * `returns` - Array of portfolio periodic returns
|
|
137
|
+
/// * `beta` - Portfolio beta relative to benchmark
|
|
138
|
+
/// * `risk_free_rate` - Annual risk-free rate
|
|
139
|
+
/// * `periods_per_year` - Number of periods in a year
|
|
140
|
+
///
|
|
141
|
+
/// # Returns
|
|
142
|
+
/// Treynor ratio
|
|
143
|
+
///
|
|
144
|
+
/// # Example
|
|
145
|
+
/// ```
|
|
146
|
+
/// use quant_rust::risk::ratios::treynor_ratio;
|
|
147
|
+
/// let returns = vec![0.01, 0.02, -0.01, 0.03, -0.02, 0.01, 0.02, 0.01, -0.01, 0.02];
|
|
148
|
+
/// let treynor = treynor_ratio(&returns, 1.2, 0.04, 252);
|
|
149
|
+
/// assert!(treynor.is_finite());
|
|
150
|
+
/// ```
|
|
151
|
+
pub fn treynor_ratio(returns: &[f64], beta: f64, risk_free_rate: f64, periods_per_year: usize) -> f64 {
|
|
152
|
+
if returns.is_empty() || beta == 0.0 {
|
|
153
|
+
return 0.0;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let annualized_return = mean(returns) * periods_per_year as f64;
|
|
157
|
+
|
|
158
|
+
(annualized_return - risk_free_rate) / beta
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/// Calculate Information Ratio
|
|
162
|
+
///
|
|
163
|
+
/// Active return divided by tracking error.
|
|
164
|
+
///
|
|
165
|
+
/// # Arguments
|
|
166
|
+
/// * `portfolio_returns` - Array of portfolio periodic returns
|
|
167
|
+
/// * `benchmark_returns` - Array of benchmark periodic returns
|
|
168
|
+
///
|
|
169
|
+
/// # Returns
|
|
170
|
+
/// Information ratio
|
|
171
|
+
///
|
|
172
|
+
/// # Example
|
|
173
|
+
/// ```
|
|
174
|
+
/// use quant_rust::risk::ratios::information_ratio;
|
|
175
|
+
/// let portfolio = vec![0.01, 0.02, -0.01, 0.03, -0.02, 0.01, 0.02, 0.01, -0.01, 0.02];
|
|
176
|
+
/// let benchmark = vec![0.005, 0.015, -0.005, 0.02, -0.01, 0.005, 0.015, 0.01, -0.005, 0.015];
|
|
177
|
+
/// let ir = information_ratio(&portfolio, &benchmark);
|
|
178
|
+
/// assert!(ir.is_finite());
|
|
179
|
+
/// ```
|
|
180
|
+
pub fn information_ratio(portfolio_returns: &[f64], benchmark_returns: &[f64]) -> f64 {
|
|
181
|
+
if portfolio_returns.len() != benchmark_returns.len() || portfolio_returns.is_empty() {
|
|
182
|
+
return 0.0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Active returns
|
|
186
|
+
let active_returns: Vec<f64> = portfolio_returns
|
|
187
|
+
.iter()
|
|
188
|
+
.zip(benchmark_returns.iter())
|
|
189
|
+
.map(|(p, b)| p - b)
|
|
190
|
+
.collect();
|
|
191
|
+
|
|
192
|
+
let avg_active = mean(&active_returns);
|
|
193
|
+
let tracking_error = std_dev(&active_returns, false); // Sample std dev
|
|
194
|
+
|
|
195
|
+
if tracking_error == 0.0 {
|
|
196
|
+
return 0.0;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
avg_active / tracking_error
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/// Comprehensive risk-adjusted metrics
|
|
203
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
204
|
+
pub struct RiskAdjustedMetrics {
|
|
205
|
+
/// Sharpe ratio
|
|
206
|
+
pub sharpe_ratio: f64,
|
|
207
|
+
/// Sortino ratio
|
|
208
|
+
pub sortino_ratio: f64,
|
|
209
|
+
/// Calmar ratio
|
|
210
|
+
pub calmar_ratio: f64,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/// Calculate all risk-adjusted metrics
|
|
214
|
+
///
|
|
215
|
+
/// # Arguments
|
|
216
|
+
/// * `returns` - Array of periodic returns
|
|
217
|
+
/// * `equity_curve` - Array of equity values over time
|
|
218
|
+
/// * `risk_free_rate` - Annual risk-free rate
|
|
219
|
+
/// * `periods_per_year` - Number of periods in a year
|
|
220
|
+
///
|
|
221
|
+
/// # Returns
|
|
222
|
+
/// RiskAdjustedMetrics struct
|
|
223
|
+
pub fn calculate_risk_adjusted_metrics(
|
|
224
|
+
returns: &[f64],
|
|
225
|
+
equity_curve: &[f64],
|
|
226
|
+
risk_free_rate: f64,
|
|
227
|
+
periods_per_year: usize,
|
|
228
|
+
) -> RiskAdjustedMetrics {
|
|
229
|
+
RiskAdjustedMetrics {
|
|
230
|
+
sharpe_ratio: sharpe_ratio(returns, risk_free_rate, periods_per_year),
|
|
231
|
+
sortino_ratio: sortino_ratio(returns, risk_free_rate, periods_per_year),
|
|
232
|
+
calmar_ratio: calmar_ratio(returns, equity_curve, periods_per_year),
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
#[cfg(test)]
|
|
237
|
+
mod tests {
|
|
238
|
+
use super::*;
|
|
239
|
+
|
|
240
|
+
#[test]
|
|
241
|
+
fn test_sharpe_ratio() {
|
|
242
|
+
let returns = vec![0.01, 0.02, -0.01, 0.03, -0.02, 0.01, 0.02, 0.01, -0.01, 0.02];
|
|
243
|
+
let sharpe = sharpe_ratio(&returns, 0.04, 252);
|
|
244
|
+
assert!(sharpe.is_finite());
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
#[test]
|
|
248
|
+
fn test_sharpe_ratio_empty() {
|
|
249
|
+
let sharpe = sharpe_ratio(&[], 0.04, 252);
|
|
250
|
+
assert_eq!(sharpe, 0.0);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
#[test]
|
|
254
|
+
fn test_sortino_ratio() {
|
|
255
|
+
let returns = vec![0.01, 0.02, -0.01, 0.03, -0.02, 0.01, 0.02, 0.01, -0.01, 0.02];
|
|
256
|
+
let sortino = sortino_ratio(&returns, 0.04, 252);
|
|
257
|
+
assert!(sortino.is_finite() || sortino.is_infinite());
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
#[test]
|
|
261
|
+
fn test_calmar_ratio() {
|
|
262
|
+
let returns = vec![0.01, 0.02, -0.01, 0.03, -0.02, 0.01, 0.02, 0.01, -0.01, 0.02];
|
|
263
|
+
let equity = vec![100.0, 101.0, 103.0, 102.0, 105.0, 103.0, 104.0, 106.0, 107.0, 106.0, 108.0];
|
|
264
|
+
let calmar = calmar_ratio(&returns, &equity, 252);
|
|
265
|
+
assert!(calmar.is_finite());
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
#[test]
|
|
269
|
+
fn test_treynor_ratio() {
|
|
270
|
+
let returns = vec![0.01, 0.02, -0.01, 0.03, -0.02, 0.01, 0.02, 0.01, -0.01, 0.02];
|
|
271
|
+
let treynor = treynor_ratio(&returns, 1.2, 0.04, 252);
|
|
272
|
+
assert!(treynor.is_finite());
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
#[test]
|
|
276
|
+
fn test_information_ratio() {
|
|
277
|
+
let portfolio = vec![0.01, 0.02, -0.01, 0.03, -0.02, 0.01, 0.02, 0.01, -0.01, 0.02];
|
|
278
|
+
let benchmark = vec![0.005, 0.015, -0.005, 0.02, -0.01, 0.005, 0.015, 0.01, -0.005, 0.015];
|
|
279
|
+
let ir = information_ratio(&portfolio, &benchmark);
|
|
280
|
+
assert!(ir.is_finite());
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
#[test]
|
|
284
|
+
fn test_information_ratio_mismatched_lengths() {
|
|
285
|
+
let portfolio = vec![0.01, 0.02, -0.01];
|
|
286
|
+
let benchmark = vec![0.005, 0.015];
|
|
287
|
+
let ir = information_ratio(&portfolio, &benchmark);
|
|
288
|
+
assert_eq!(ir, 0.0);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
//! Position Sizing Functions
|
|
2
|
+
//!
|
|
3
|
+
//! Methods for calculating appropriate position sizes based on risk parameters.
|
|
4
|
+
|
|
5
|
+
use serde::{Deserialize, Serialize};
|
|
6
|
+
|
|
7
|
+
/// Result of position sizing calculation
|
|
8
|
+
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
9
|
+
pub struct PositionSizeResult {
|
|
10
|
+
/// Number of shares/contracts
|
|
11
|
+
pub shares: usize,
|
|
12
|
+
/// Total position value
|
|
13
|
+
pub position_value: f64,
|
|
14
|
+
/// Amount of capital at risk
|
|
15
|
+
pub risk_amount: f64,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// Calculate position size based on fixed fractional method
|
|
19
|
+
///
|
|
20
|
+
/// Determines the number of shares to buy based on the percentage of capital
|
|
21
|
+
/// you're willing to risk and the distance to your stop loss.
|
|
22
|
+
///
|
|
23
|
+
/// # Arguments
|
|
24
|
+
/// * `capital` - Total trading capital
|
|
25
|
+
/// * `risk_percent` - Percent of capital to risk (e.g., 0.02 for 2%)
|
|
26
|
+
/// * `entry_price` - Planned entry price
|
|
27
|
+
/// * `stop_loss` - Stop loss price
|
|
28
|
+
///
|
|
29
|
+
/// # Returns
|
|
30
|
+
/// PositionSizeResult with shares, position value, and risk amount
|
|
31
|
+
///
|
|
32
|
+
/// # Example
|
|
33
|
+
/// ```
|
|
34
|
+
/// use quant_rust::risk::sizing::fixed_fractional_size;
|
|
35
|
+
/// let result = fixed_fractional_size(100000.0, 0.02, 50.0, 45.0);
|
|
36
|
+
/// assert_eq!(result.shares, 400);
|
|
37
|
+
/// assert_eq!(result.risk_amount, 2000.0);
|
|
38
|
+
/// ```
|
|
39
|
+
pub fn fixed_fractional_size(
|
|
40
|
+
capital: f64,
|
|
41
|
+
risk_percent: f64,
|
|
42
|
+
entry_price: f64,
|
|
43
|
+
stop_loss: f64,
|
|
44
|
+
) -> PositionSizeResult {
|
|
45
|
+
let risk_amount = capital * risk_percent;
|
|
46
|
+
let risk_per_share = (entry_price - stop_loss).abs();
|
|
47
|
+
|
|
48
|
+
if risk_per_share == 0.0 {
|
|
49
|
+
return PositionSizeResult {
|
|
50
|
+
shares: 0,
|
|
51
|
+
position_value: 0.0,
|
|
52
|
+
risk_amount: 0.0,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let shares = (risk_amount / risk_per_share).floor() as usize;
|
|
57
|
+
let position_value = shares as f64 * entry_price;
|
|
58
|
+
|
|
59
|
+
PositionSizeResult {
|
|
60
|
+
shares,
|
|
61
|
+
position_value,
|
|
62
|
+
risk_amount,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Calculate position size using volatility-based sizing
|
|
67
|
+
///
|
|
68
|
+
/// Sizes positions such that a 1 standard deviation move equals the risk amount.
|
|
69
|
+
/// This normalizes risk across assets with different volatilities.
|
|
70
|
+
///
|
|
71
|
+
/// # Arguments
|
|
72
|
+
/// * `capital` - Total trading capital
|
|
73
|
+
/// * `risk_percent` - Percent of capital to risk
|
|
74
|
+
/// * `volatility` - Asset volatility (daily standard deviation as decimal)
|
|
75
|
+
/// * `price` - Current price
|
|
76
|
+
///
|
|
77
|
+
/// # Returns
|
|
78
|
+
/// PositionSizeResult with shares, position value, and risk amount
|
|
79
|
+
///
|
|
80
|
+
/// # Example
|
|
81
|
+
/// ```
|
|
82
|
+
/// use quant_rust::risk::sizing::volatility_based_size;
|
|
83
|
+
/// let result = volatility_based_size(100000.0, 0.02, 0.02, 100.0);
|
|
84
|
+
/// assert_eq!(result.shares, 100);
|
|
85
|
+
/// assert_eq!(result.risk_amount, 2000.0);
|
|
86
|
+
/// ```
|
|
87
|
+
pub fn volatility_based_size(
|
|
88
|
+
capital: f64,
|
|
89
|
+
risk_percent: f64,
|
|
90
|
+
volatility: f64,
|
|
91
|
+
price: f64,
|
|
92
|
+
) -> PositionSizeResult {
|
|
93
|
+
let risk_amount = capital * risk_percent;
|
|
94
|
+
|
|
95
|
+
if volatility == 0.0 || price == 0.0 {
|
|
96
|
+
return PositionSizeResult {
|
|
97
|
+
shares: 0,
|
|
98
|
+
position_value: 0.0,
|
|
99
|
+
risk_amount: 0.0,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Size such that expected 1 SD move = risk amount
|
|
104
|
+
let shares = (risk_amount / (price * volatility)).floor() as usize;
|
|
105
|
+
let position_value = shares as f64 * price;
|
|
106
|
+
|
|
107
|
+
PositionSizeResult {
|
|
108
|
+
shares,
|
|
109
|
+
position_value,
|
|
110
|
+
risk_amount,
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// Calculate optimal position size using Kelly criterion (for continuous returns)
|
|
115
|
+
///
|
|
116
|
+
/// The Kelly criterion maximizes long-term growth rate.
|
|
117
|
+
/// Kelly = expected_return / variance
|
|
118
|
+
///
|
|
119
|
+
/// # Arguments
|
|
120
|
+
/// * `expected_return` - Expected return of the asset
|
|
121
|
+
/// * `variance` - Variance of returns
|
|
122
|
+
/// * `capital` - Total trading capital
|
|
123
|
+
/// * `fraction` - Fraction of Kelly to use (1.0 = full Kelly, 0.5 = half Kelly)
|
|
124
|
+
///
|
|
125
|
+
/// # Returns
|
|
126
|
+
/// Optimal position size in currency units
|
|
127
|
+
///
|
|
128
|
+
/// # Example
|
|
129
|
+
/// ```
|
|
130
|
+
/// use quant_rust::risk::sizing::kelly_size;
|
|
131
|
+
/// let size = kelly_size(0.10, 0.04, 100000.0, 0.5);
|
|
132
|
+
/// assert!((size - 125000.0).abs() < 1e-10);
|
|
133
|
+
/// ```
|
|
134
|
+
pub fn kelly_size(expected_return: f64, variance: f64, capital: f64, fraction: f64) -> f64 {
|
|
135
|
+
if variance <= 0.0 {
|
|
136
|
+
return 0.0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let kelly = expected_return / variance;
|
|
140
|
+
capital * kelly * fraction
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#[cfg(test)]
|
|
144
|
+
mod tests {
|
|
145
|
+
use super::*;
|
|
146
|
+
|
|
147
|
+
#[test]
|
|
148
|
+
fn test_fixed_fractional_size() {
|
|
149
|
+
let result = fixed_fractional_size(100000.0, 0.02, 50.0, 45.0);
|
|
150
|
+
assert_eq!(result.shares, 400);
|
|
151
|
+
assert_eq!(result.risk_amount, 2000.0);
|
|
152
|
+
assert!((result.position_value - 20000.0).abs() < 1e-10);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
#[test]
|
|
156
|
+
fn test_fixed_fractional_size_zero_risk() {
|
|
157
|
+
let result = fixed_fractional_size(100000.0, 0.02, 50.0, 50.0);
|
|
158
|
+
assert_eq!(result.shares, 0);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#[test]
|
|
162
|
+
fn test_volatility_based_size() {
|
|
163
|
+
let result = volatility_based_size(100000.0, 0.02, 0.02, 100.0);
|
|
164
|
+
// risk_amount = 100000 * 0.02 = 2000
|
|
165
|
+
// shares = floor(2000 / (100 * 0.02)) = floor(2000 / 2) = 1000
|
|
166
|
+
assert_eq!(result.shares, 1000);
|
|
167
|
+
assert_eq!(result.risk_amount, 2000.0);
|
|
168
|
+
assert!((result.position_value - 100000.0).abs() < 1e-10);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
#[test]
|
|
172
|
+
fn test_volatility_based_size_zero_vol() {
|
|
173
|
+
let result = volatility_based_size(100000.0, 0.02, 0.0, 100.0);
|
|
174
|
+
assert_eq!(result.shares, 0);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#[test]
|
|
178
|
+
fn test_kelly_size() {
|
|
179
|
+
let size = kelly_size(0.10, 0.04, 100000.0, 1.0);
|
|
180
|
+
assert!((size - 250000.0).abs() < 1e-10);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
#[test]
|
|
184
|
+
fn test_kelly_size_half() {
|
|
185
|
+
let size = kelly_size(0.10, 0.04, 100000.0, 0.5);
|
|
186
|
+
assert!((size - 125000.0).abs() < 1e-10);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
#[test]
|
|
190
|
+
fn test_kelly_size_zero_variance() {
|
|
191
|
+
let size = kelly_size(0.10, 0.0, 100000.0, 1.0);
|
|
192
|
+
assert_eq!(size, 0.0);
|
|
193
|
+
}
|
|
194
|
+
}
|
package/src/risk/var.rs
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
//! Value at Risk (VaR) Functions
|
|
2
|
+
//!
|
|
3
|
+
//! Historical and parametric VaR calculations with Expected Shortfall (CVaR).
|
|
4
|
+
|
|
5
|
+
use serde::{Deserialize, Serialize};
|
|
6
|
+
|
|
7
|
+
use crate::utils::{mean, std_dev};
|
|
8
|
+
|
|
9
|
+
/// Configuration for VaR calculation
|
|
10
|
+
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
11
|
+
pub struct VaRConfig {
|
|
12
|
+
/// Confidence level for VaR (default: 0.95)
|
|
13
|
+
pub confidence_level: f64,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
impl Default for VaRConfig {
|
|
17
|
+
fn default() -> Self {
|
|
18
|
+
VaRConfig {
|
|
19
|
+
confidence_level: 0.95,
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// Result of VaR calculation containing both 95% and 99% confidence levels
|
|
25
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
26
|
+
pub struct VaRResult {
|
|
27
|
+
/// Value at Risk at 95% confidence
|
|
28
|
+
pub var95: f64,
|
|
29
|
+
/// Value at Risk at 99% confidence
|
|
30
|
+
pub var99: f64,
|
|
31
|
+
/// Expected Shortfall (CVaR) at 95% confidence
|
|
32
|
+
pub expected_shortfall95: f64,
|
|
33
|
+
/// Expected Shortfall (CVaR) at 99% confidence
|
|
34
|
+
pub expected_shortfall99: f64,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// Calculate historical Value at Risk
|
|
38
|
+
///
|
|
39
|
+
/// Uses historical simulation to estimate potential losses at a given confidence level.
|
|
40
|
+
///
|
|
41
|
+
/// # Arguments
|
|
42
|
+
/// * `returns` - Array of historical returns
|
|
43
|
+
/// * `confidence` - Confidence level (e.g., 0.95 for 95%)
|
|
44
|
+
///
|
|
45
|
+
/// # Returns
|
|
46
|
+
/// VaR as a positive number representing potential loss
|
|
47
|
+
///
|
|
48
|
+
/// # Example
|
|
49
|
+
/// ```
|
|
50
|
+
/// use quant_rust::risk::var::historical_var;
|
|
51
|
+
/// let returns = vec![-0.05, -0.02, 0.01, 0.03, 0.02, -0.04, 0.05, -0.01, 0.02, 0.01];
|
|
52
|
+
/// let var = historical_var(&returns, 0.95);
|
|
53
|
+
/// assert!(var > 0.0);
|
|
54
|
+
/// ```
|
|
55
|
+
pub fn historical_var(returns: &[f64], confidence: f64) -> f64 {
|
|
56
|
+
if returns.is_empty() {
|
|
57
|
+
return 0.0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let mut sorted = returns.to_vec();
|
|
61
|
+
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
|
62
|
+
|
|
63
|
+
let index = ((1.0 - confidence) * returns.len() as f64).floor() as usize;
|
|
64
|
+
let index = index.min(sorted.len() - 1);
|
|
65
|
+
|
|
66
|
+
sorted[index].abs()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// Calculate parametric Value at Risk (assuming normal distribution)
|
|
70
|
+
///
|
|
71
|
+
/// Uses the assumption of normally distributed returns to estimate VaR.
|
|
72
|
+
///
|
|
73
|
+
/// # Arguments
|
|
74
|
+
/// * `returns` - Array of historical returns
|
|
75
|
+
/// * `confidence` - Confidence level (e.g., 0.95 for 95%)
|
|
76
|
+
///
|
|
77
|
+
/// # Returns
|
|
78
|
+
/// VaR as a positive number representing potential loss
|
|
79
|
+
///
|
|
80
|
+
/// # Example
|
|
81
|
+
/// ```
|
|
82
|
+
/// use quant_rust::risk::var::parametric_var;
|
|
83
|
+
/// let returns = vec![-0.05, -0.02, 0.01, 0.03, 0.02, -0.04, 0.05, -0.01, 0.02, 0.01];
|
|
84
|
+
/// let var = parametric_var(&returns, 0.95);
|
|
85
|
+
/// assert!(var > 0.0);
|
|
86
|
+
/// ```
|
|
87
|
+
pub fn parametric_var(returns: &[f64], confidence: f64) -> f64 {
|
|
88
|
+
if returns.is_empty() {
|
|
89
|
+
return 0.0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let avg = mean(returns);
|
|
93
|
+
let sd = std_dev(returns, false); // Sample std dev
|
|
94
|
+
|
|
95
|
+
// Z-scores for common confidence levels
|
|
96
|
+
let z = match confidence {
|
|
97
|
+
c if (c - 0.90).abs() < 1e-6 => 1.282,
|
|
98
|
+
c if (c - 0.95).abs() < 1e-6 => 1.645,
|
|
99
|
+
c if (c - 0.99).abs() < 1e-6 => 2.326,
|
|
100
|
+
_ => 1.645, // Default to 95%
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
(avg - z * sd).abs()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// Calculate VaR and Expected Shortfall (CVaR) at 95% and 99% confidence
|
|
107
|
+
///
|
|
108
|
+
/// Comprehensive VaR calculation returning both VaR and Expected Shortfall
|
|
109
|
+
/// at two common confidence levels.
|
|
110
|
+
///
|
|
111
|
+
/// # Arguments
|
|
112
|
+
/// * `returns` - Array of historical returns
|
|
113
|
+
/// * `config` - Optional configuration (uses defaults if not provided)
|
|
114
|
+
///
|
|
115
|
+
/// # Returns
|
|
116
|
+
/// VaRResult containing VaR and CVaR at 95% and 99%
|
|
117
|
+
///
|
|
118
|
+
/// # Example
|
|
119
|
+
/// ```
|
|
120
|
+
/// use quant_rust::risk::var::{calculate_var, VaRConfig};
|
|
121
|
+
/// let returns = vec![-0.05, -0.02, 0.01, 0.03, 0.02, -0.04, 0.05, -0.01, 0.02, 0.01];
|
|
122
|
+
/// let result = calculate_var(&returns, VaRConfig::default());
|
|
123
|
+
/// assert!(result.var95 > 0.0);
|
|
124
|
+
/// assert!(result.var99 >= result.var95);
|
|
125
|
+
/// assert!(result.expected_shortfall95 >= result.var95);
|
|
126
|
+
/// ```
|
|
127
|
+
pub fn calculate_var(returns: &[f64], _config: VaRConfig) -> VaRResult {
|
|
128
|
+
if returns.is_empty() {
|
|
129
|
+
return VaRResult {
|
|
130
|
+
var95: 0.0,
|
|
131
|
+
var99: 0.0,
|
|
132
|
+
expected_shortfall95: 0.0,
|
|
133
|
+
expected_shortfall99: 0.0,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let mut sorted = returns.to_vec();
|
|
138
|
+
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
|
139
|
+
|
|
140
|
+
let index95 = ((1.0 - 0.95) * returns.len() as f64).floor() as usize;
|
|
141
|
+
let index99 = ((1.0 - 0.99) * returns.len() as f64).floor() as usize;
|
|
142
|
+
|
|
143
|
+
let index95 = index95.min(sorted.len() - 1);
|
|
144
|
+
let index99 = index99.min(sorted.len() - 1);
|
|
145
|
+
|
|
146
|
+
let var95 = sorted[index95].abs();
|
|
147
|
+
let var99 = sorted[index99].abs();
|
|
148
|
+
|
|
149
|
+
// Expected Shortfall (average of returns below VaR)
|
|
150
|
+
let tail95: Vec<f64> = sorted.iter().take(index95 + 1).cloned().collect();
|
|
151
|
+
let tail99: Vec<f64> = sorted.iter().take(index99 + 1).cloned().collect();
|
|
152
|
+
|
|
153
|
+
let expected_shortfall95 = if tail95.is_empty() {
|
|
154
|
+
var95
|
|
155
|
+
} else {
|
|
156
|
+
tail95.iter().sum::<f64>().abs() / tail95.len() as f64
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
let expected_shortfall99 = if tail99.is_empty() {
|
|
160
|
+
var99
|
|
161
|
+
} else {
|
|
162
|
+
tail99.iter().sum::<f64>().abs() / tail99.len() as f64
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
VaRResult {
|
|
166
|
+
var95,
|
|
167
|
+
var99,
|
|
168
|
+
expected_shortfall95,
|
|
169
|
+
expected_shortfall99,
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#[cfg(test)]
|
|
174
|
+
mod tests {
|
|
175
|
+
use super::*;
|
|
176
|
+
|
|
177
|
+
#[test]
|
|
178
|
+
fn test_historical_var() {
|
|
179
|
+
let returns = vec![-0.05, -0.02, 0.01, 0.03, 0.02, -0.04, 0.05, -0.01, 0.02, 0.01];
|
|
180
|
+
let var = historical_var(&returns, 0.95);
|
|
181
|
+
assert!(var > 0.0);
|
|
182
|
+
assert!(var <= 0.1);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#[test]
|
|
186
|
+
fn test_historical_var_empty() {
|
|
187
|
+
let var = historical_var(&[], 0.95);
|
|
188
|
+
assert_eq!(var, 0.0);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
#[test]
|
|
192
|
+
fn test_parametric_var() {
|
|
193
|
+
let returns = vec![-0.05, -0.02, 0.01, 0.03, 0.02, -0.04, 0.05, -0.01, 0.02, 0.01];
|
|
194
|
+
let var = parametric_var(&returns, 0.95);
|
|
195
|
+
assert!(var > 0.0);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
#[test]
|
|
199
|
+
fn test_parametric_var_99() {
|
|
200
|
+
let returns = vec![-0.05, -0.02, 0.01, 0.03, 0.02, -0.04, 0.05, -0.01, 0.02, 0.01];
|
|
201
|
+
let var95 = parametric_var(&returns, 0.95);
|
|
202
|
+
let var99 = parametric_var(&returns, 0.99);
|
|
203
|
+
assert!(var99 >= var95);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
#[test]
|
|
207
|
+
fn test_calculate_var() {
|
|
208
|
+
let returns = vec![-0.05, -0.02, 0.01, 0.03, 0.02, -0.04, 0.05, -0.01, 0.02, 0.01];
|
|
209
|
+
let result = calculate_var(&returns, VaRConfig::default());
|
|
210
|
+
assert!(result.var95 > 0.0);
|
|
211
|
+
assert!(result.var99 >= result.var95);
|
|
212
|
+
assert!(result.expected_shortfall95 >= result.var95);
|
|
213
|
+
assert!(result.expected_shortfall99 >= result.var99);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
#[test]
|
|
217
|
+
fn test_calculate_var_empty() {
|
|
218
|
+
let result = calculate_var(&[], VaRConfig::default());
|
|
219
|
+
assert_eq!(result.var95, 0.0);
|
|
220
|
+
assert_eq!(result.var99, 0.0);
|
|
221
|
+
}
|
|
222
|
+
}
|