@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
package/src/risk/beta.rs
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
//! Beta and Alpha Functions
|
|
2
|
+
//!
|
|
3
|
+
//! Market sensitivity (beta) and excess return (alpha) calculations.
|
|
4
|
+
|
|
5
|
+
use crate::utils::{mean, variance};
|
|
6
|
+
|
|
7
|
+
/// Calculate beta (market sensitivity)
|
|
8
|
+
///
|
|
9
|
+
/// Beta measures how much an asset moves relative to the market.
|
|
10
|
+
/// - Beta > 1: More volatile than market
|
|
11
|
+
/// - Beta < 1: Less volatile than market
|
|
12
|
+
/// - Beta = 1: Same volatility as market
|
|
13
|
+
///
|
|
14
|
+
/// # Arguments
|
|
15
|
+
/// * `asset_returns` - Array of asset periodic returns
|
|
16
|
+
/// * `market_returns` - Array of market/benchmark periodic returns
|
|
17
|
+
///
|
|
18
|
+
/// # Returns
|
|
19
|
+
/// Beta coefficient
|
|
20
|
+
///
|
|
21
|
+
/// # Example
|
|
22
|
+
/// ```
|
|
23
|
+
/// use quant_rust::risk::beta::beta;
|
|
24
|
+
/// let asset = vec![0.02, 0.03, -0.01, 0.04, -0.02];
|
|
25
|
+
/// let market = vec![0.01, 0.02, -0.005, 0.02, -0.01];
|
|
26
|
+
/// let b = beta(&asset, &market);
|
|
27
|
+
/// assert!(b > 0.0);
|
|
28
|
+
/// ```
|
|
29
|
+
pub fn beta(asset_returns: &[f64], market_returns: &[f64]) -> f64 {
|
|
30
|
+
if asset_returns.len() != market_returns.len() || asset_returns.is_empty() {
|
|
31
|
+
return 0.0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let n = asset_returns.len() as f64;
|
|
35
|
+
let avg_asset = mean(asset_returns);
|
|
36
|
+
let avg_market = mean(market_returns);
|
|
37
|
+
|
|
38
|
+
// Covariance
|
|
39
|
+
let cov: f64 = asset_returns
|
|
40
|
+
.iter()
|
|
41
|
+
.zip(market_returns.iter())
|
|
42
|
+
.map(|(a, m)| (a - avg_asset) * (m - avg_market))
|
|
43
|
+
.sum::<f64>()
|
|
44
|
+
/ n;
|
|
45
|
+
|
|
46
|
+
// Market variance (population variance since we're using n in covariance)
|
|
47
|
+
let market_var = variance(market_returns, true);
|
|
48
|
+
|
|
49
|
+
if market_var == 0.0 {
|
|
50
|
+
return 0.0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
cov / market_var
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Calculate alpha (excess return)
|
|
57
|
+
///
|
|
58
|
+
/// Alpha measures the excess return of an asset relative to its beta-adjusted
|
|
59
|
+
/// expected return. Positive alpha indicates outperformance.
|
|
60
|
+
///
|
|
61
|
+
/// Uses the Capital Asset Pricing Model (CAPM):
|
|
62
|
+
/// Alpha = Asset Return - (Risk-Free Rate + Beta * (Market Return - Risk-Free Rate))
|
|
63
|
+
///
|
|
64
|
+
/// # Arguments
|
|
65
|
+
/// * `asset_returns` - Array of asset periodic returns
|
|
66
|
+
/// * `market_returns` - Array of market/benchmark periodic returns
|
|
67
|
+
/// * `risk_free_rate` - Annual risk-free rate
|
|
68
|
+
/// * `periods_per_year` - Number of periods in a year
|
|
69
|
+
///
|
|
70
|
+
/// # Returns
|
|
71
|
+
/// Annualized alpha
|
|
72
|
+
///
|
|
73
|
+
/// # Example
|
|
74
|
+
/// ```
|
|
75
|
+
/// use quant_rust::risk::beta::alpha;
|
|
76
|
+
/// let asset = vec![0.02, 0.03, -0.01, 0.04, -0.02];
|
|
77
|
+
/// let market = vec![0.01, 0.02, -0.005, 0.02, -0.01];
|
|
78
|
+
/// let a = alpha(&asset, &market, 0.04, 252);
|
|
79
|
+
/// assert!(a.is_finite());
|
|
80
|
+
/// ```
|
|
81
|
+
pub fn alpha(
|
|
82
|
+
asset_returns: &[f64],
|
|
83
|
+
market_returns: &[f64],
|
|
84
|
+
risk_free_rate: f64,
|
|
85
|
+
periods_per_year: usize,
|
|
86
|
+
) -> f64 {
|
|
87
|
+
if asset_returns.len() != market_returns.len() || asset_returns.is_empty() {
|
|
88
|
+
return 0.0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let b = beta(asset_returns, market_returns);
|
|
92
|
+
let asset_return = mean(asset_returns) * periods_per_year as f64;
|
|
93
|
+
let market_return = mean(market_returns) * periods_per_year as f64;
|
|
94
|
+
|
|
95
|
+
// Jensen's alpha
|
|
96
|
+
asset_return - (risk_free_rate + b * (market_return - risk_free_rate))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// Result of beta/alpha calculation
|
|
100
|
+
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
|
|
101
|
+
pub struct BetaAlphaResult {
|
|
102
|
+
/// Beta coefficient
|
|
103
|
+
pub beta: f64,
|
|
104
|
+
/// Alpha (annualized excess return)
|
|
105
|
+
pub alpha: f64,
|
|
106
|
+
/// Correlation between asset and market
|
|
107
|
+
pub correlation: f64,
|
|
108
|
+
/// R-squared (coefficient of determination)
|
|
109
|
+
pub r_squared: f64,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// Calculate comprehensive beta and alpha metrics
|
|
113
|
+
///
|
|
114
|
+
/// # Arguments
|
|
115
|
+
/// * `asset_returns` - Array of asset periodic returns
|
|
116
|
+
/// * `market_returns` - Array of market/benchmark periodic returns
|
|
117
|
+
/// * `risk_free_rate` - Annual risk-free rate
|
|
118
|
+
/// * `periods_per_year` - Number of periods in a year
|
|
119
|
+
///
|
|
120
|
+
/// # Returns
|
|
121
|
+
/// BetaAlphaResult with beta, alpha, correlation, and R-squared
|
|
122
|
+
///
|
|
123
|
+
/// # Example
|
|
124
|
+
/// ```
|
|
125
|
+
/// use quant_rust::risk::beta::calculate_beta_alpha;
|
|
126
|
+
/// let asset = vec![0.02, 0.03, -0.01, 0.04, -0.02];
|
|
127
|
+
/// let market = vec![0.01, 0.02, -0.005, 0.02, -0.01];
|
|
128
|
+
/// let result = calculate_beta_alpha(&asset, &market, 0.04, 252);
|
|
129
|
+
/// assert!(result.beta > 0.0);
|
|
130
|
+
/// assert!(result.correlation >= -1.0 && result.correlation <= 1.0);
|
|
131
|
+
/// ```
|
|
132
|
+
pub fn calculate_beta_alpha(
|
|
133
|
+
asset_returns: &[f64],
|
|
134
|
+
market_returns: &[f64],
|
|
135
|
+
risk_free_rate: f64,
|
|
136
|
+
periods_per_year: usize,
|
|
137
|
+
) -> BetaAlphaResult {
|
|
138
|
+
if asset_returns.len() != market_returns.len() || asset_returns.is_empty() {
|
|
139
|
+
return BetaAlphaResult {
|
|
140
|
+
beta: 0.0,
|
|
141
|
+
alpha: 0.0,
|
|
142
|
+
correlation: 0.0,
|
|
143
|
+
r_squared: 0.0,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let n = asset_returns.len() as f64;
|
|
148
|
+
let avg_asset = mean(asset_returns);
|
|
149
|
+
let avg_market = mean(market_returns);
|
|
150
|
+
|
|
151
|
+
// Calculate variances and covariance
|
|
152
|
+
let mut cov = 0.0;
|
|
153
|
+
let mut var_asset = 0.0;
|
|
154
|
+
let mut var_market = 0.0;
|
|
155
|
+
|
|
156
|
+
for i in 0..asset_returns.len() {
|
|
157
|
+
let d_asset = asset_returns[i] - avg_asset;
|
|
158
|
+
let d_market = market_returns[i] - avg_market;
|
|
159
|
+
cov += d_asset * d_market;
|
|
160
|
+
var_asset += d_asset * d_asset;
|
|
161
|
+
var_market += d_market * d_market;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
cov /= n;
|
|
165
|
+
var_market /= n;
|
|
166
|
+
|
|
167
|
+
let b = if var_market > 0.0 { cov / var_market } else { 0.0 };
|
|
168
|
+
|
|
169
|
+
// Correlation
|
|
170
|
+
let std_asset = (var_asset / n).sqrt();
|
|
171
|
+
let std_market = var_market.sqrt();
|
|
172
|
+
let corr = if std_asset > 0.0 && std_market > 0.0 {
|
|
173
|
+
cov / (std_asset * std_market)
|
|
174
|
+
} else {
|
|
175
|
+
0.0
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// R-squared = correlation squared
|
|
179
|
+
let r_squared = corr * corr;
|
|
180
|
+
|
|
181
|
+
// Alpha
|
|
182
|
+
let asset_return = avg_asset * periods_per_year as f64;
|
|
183
|
+
let market_return = avg_market * periods_per_year as f64;
|
|
184
|
+
let a = asset_return - (risk_free_rate + b * (market_return - risk_free_rate));
|
|
185
|
+
|
|
186
|
+
BetaAlphaResult {
|
|
187
|
+
beta: b,
|
|
188
|
+
alpha: a,
|
|
189
|
+
correlation: corr,
|
|
190
|
+
r_squared,
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
#[cfg(test)]
|
|
195
|
+
mod tests {
|
|
196
|
+
use super::*;
|
|
197
|
+
|
|
198
|
+
#[test]
|
|
199
|
+
fn test_beta() {
|
|
200
|
+
let asset = vec![0.02, 0.03, -0.01, 0.04, -0.02];
|
|
201
|
+
let market = vec![0.01, 0.02, -0.005, 0.02, -0.01];
|
|
202
|
+
let b = beta(&asset, &market);
|
|
203
|
+
assert!(b > 0.0);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
#[test]
|
|
207
|
+
fn test_beta_equal_length() {
|
|
208
|
+
// Asset moves 2x market
|
|
209
|
+
let asset = vec![0.02, 0.04, -0.01, 0.02];
|
|
210
|
+
let market = vec![0.01, 0.02, -0.005, 0.01];
|
|
211
|
+
let b = beta(&asset, &market);
|
|
212
|
+
// Should be approximately 2
|
|
213
|
+
assert!((b - 2.0).abs() < 0.5);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
#[test]
|
|
217
|
+
fn test_beta_empty() {
|
|
218
|
+
let b = beta(&[], &[]);
|
|
219
|
+
assert_eq!(b, 0.0);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
#[test]
|
|
223
|
+
fn test_beta_mismatched() {
|
|
224
|
+
let asset = vec![0.02, 0.03];
|
|
225
|
+
let market = vec![0.01];
|
|
226
|
+
let b = beta(&asset, &market);
|
|
227
|
+
assert_eq!(b, 0.0);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
#[test]
|
|
231
|
+
fn test_alpha() {
|
|
232
|
+
let asset = vec![0.02, 0.03, -0.01, 0.04, -0.02];
|
|
233
|
+
let market = vec![0.01, 0.02, -0.005, 0.02, -0.01];
|
|
234
|
+
let a = alpha(&asset, &market, 0.04, 252);
|
|
235
|
+
assert!(a.is_finite());
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
#[test]
|
|
239
|
+
fn test_calculate_beta_alpha() {
|
|
240
|
+
let asset = vec![0.02, 0.03, -0.01, 0.04, -0.02];
|
|
241
|
+
let market = vec![0.01, 0.02, -0.005, 0.02, -0.01];
|
|
242
|
+
let result = calculate_beta_alpha(&asset, &market, 0.04, 252);
|
|
243
|
+
assert!(result.beta > 0.0);
|
|
244
|
+
assert!(result.correlation >= -1.0 && result.correlation <= 1.0);
|
|
245
|
+
assert!(result.r_squared >= 0.0 && result.r_squared <= 1.0);
|
|
246
|
+
assert!(result.alpha.is_finite());
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
#[test]
|
|
250
|
+
fn test_calculate_beta_alpha_perfect_correlation() {
|
|
251
|
+
let asset = vec![0.01, 0.02, -0.01, 0.02];
|
|
252
|
+
let market = vec![0.01, 0.02, -0.01, 0.02];
|
|
253
|
+
let result = calculate_beta_alpha(&asset, &market, 0.04, 252);
|
|
254
|
+
assert!((result.correlation - 1.0).abs() < 1e-10);
|
|
255
|
+
assert!((result.r_squared - 1.0).abs() < 1e-10);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
//! Drawdown Analysis Functions
|
|
2
|
+
//!
|
|
3
|
+
//! Maximum drawdown, underwater periods, and drawdown analysis.
|
|
4
|
+
|
|
5
|
+
use serde::{Deserialize, Serialize};
|
|
6
|
+
|
|
7
|
+
use crate::utils::min;
|
|
8
|
+
|
|
9
|
+
/// A single drawdown point in time
|
|
10
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
11
|
+
pub struct DrawdownPoint {
|
|
12
|
+
/// Start index of the drawdown
|
|
13
|
+
pub start: usize,
|
|
14
|
+
/// End index of the drawdown (recovery point)
|
|
15
|
+
pub end: usize,
|
|
16
|
+
/// Trough (lowest) value during drawdown
|
|
17
|
+
pub trough: f64,
|
|
18
|
+
/// Drawdown magnitude as decimal (e.g., 0.25 = 25%)
|
|
19
|
+
pub drawdown: f64,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// Result of drawdown analysis
|
|
23
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
24
|
+
pub struct DrawdownResult {
|
|
25
|
+
/// Maximum drawdown as decimal (e.g., 0.25 = 25%)
|
|
26
|
+
pub max_drawdown: f64,
|
|
27
|
+
/// Duration of maximum drawdown in periods
|
|
28
|
+
pub max_drawdown_duration: usize,
|
|
29
|
+
/// Time to recover from maximum drawdown in periods
|
|
30
|
+
pub recovery_time: usize,
|
|
31
|
+
/// All drawdown periods identified
|
|
32
|
+
pub drawdowns: Vec<DrawdownPoint>,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Calculate maximum drawdown and related metrics
|
|
36
|
+
///
|
|
37
|
+
/// Analyzes an equity curve to find maximum drawdown, its duration,
|
|
38
|
+
/// recovery time, and all drawdown periods.
|
|
39
|
+
///
|
|
40
|
+
/// # Arguments
|
|
41
|
+
/// * `equity_curve` - Array of equity values over time
|
|
42
|
+
///
|
|
43
|
+
/// # Returns
|
|
44
|
+
/// DrawdownResult with max drawdown, durations, and all drawdown periods
|
|
45
|
+
///
|
|
46
|
+
/// # Example
|
|
47
|
+
/// ```
|
|
48
|
+
/// use quant_rust::risk::drawdown::calculate_drawdown;
|
|
49
|
+
/// let equity = vec![100.0, 110.0, 105.0, 95.0, 100.0, 90.0, 95.0, 110.0];
|
|
50
|
+
/// let result = calculate_drawdown(&equity);
|
|
51
|
+
/// assert!(result.max_drawdown > 0.0);
|
|
52
|
+
/// assert!(result.max_drawdown <= 1.0);
|
|
53
|
+
/// ```
|
|
54
|
+
pub fn calculate_drawdown(equity_curve: &[f64]) -> DrawdownResult {
|
|
55
|
+
if equity_curve.is_empty() {
|
|
56
|
+
return DrawdownResult {
|
|
57
|
+
max_drawdown: 0.0,
|
|
58
|
+
max_drawdown_duration: 0,
|
|
59
|
+
recovery_time: 0,
|
|
60
|
+
drawdowns: vec![],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let mut peak = equity_curve[0];
|
|
65
|
+
let mut max_drawdown = 0.0;
|
|
66
|
+
let mut max_drawdown_start = 0;
|
|
67
|
+
let mut max_drawdown_end = 0;
|
|
68
|
+
let mut current_drawdown_start: Option<usize> = None;
|
|
69
|
+
let mut drawdowns: Vec<DrawdownPoint> = Vec::new();
|
|
70
|
+
|
|
71
|
+
for (i, &equity) in equity_curve.iter().enumerate() {
|
|
72
|
+
if equity > peak {
|
|
73
|
+
// New peak - check if we had a drawdown
|
|
74
|
+
if let Some(start) = current_drawdown_start {
|
|
75
|
+
if start > 0 {
|
|
76
|
+
let trough_slice = &equity_curve[start..i];
|
|
77
|
+
let trough = min(trough_slice);
|
|
78
|
+
let dd = (peak - trough) / peak;
|
|
79
|
+
drawdowns.push(DrawdownPoint {
|
|
80
|
+
start,
|
|
81
|
+
end: i,
|
|
82
|
+
trough,
|
|
83
|
+
drawdown: dd,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
peak = equity;
|
|
88
|
+
current_drawdown_start = None;
|
|
89
|
+
} else {
|
|
90
|
+
if current_drawdown_start.is_none() {
|
|
91
|
+
current_drawdown_start = Some(i);
|
|
92
|
+
}
|
|
93
|
+
let dd = (peak - equity) / peak;
|
|
94
|
+
if dd > max_drawdown {
|
|
95
|
+
max_drawdown = dd;
|
|
96
|
+
max_drawdown_start = current_drawdown_start.unwrap_or(i);
|
|
97
|
+
max_drawdown_end = i;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Handle any remaining drawdown at the end
|
|
103
|
+
if let Some(start) = current_drawdown_start {
|
|
104
|
+
if start > 0 && start < equity_curve.len() {
|
|
105
|
+
let trough_slice = &equity_curve[start..];
|
|
106
|
+
let trough = min(trough_slice);
|
|
107
|
+
let dd = (peak - trough) / peak;
|
|
108
|
+
drawdowns.push(DrawdownPoint {
|
|
109
|
+
start,
|
|
110
|
+
end: equity_curve.len() - 1,
|
|
111
|
+
trough,
|
|
112
|
+
drawdown: dd,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let max_drawdown_duration = max_drawdown_end.saturating_sub(max_drawdown_start);
|
|
118
|
+
|
|
119
|
+
// Calculate recovery time (time from trough to new peak)
|
|
120
|
+
let mut recovery_time = 0;
|
|
121
|
+
if max_drawdown > 0.0 && max_drawdown_start < equity_curve.len() {
|
|
122
|
+
let dd_slice = &equity_curve[max_drawdown_start..=max_drawdown_end.min(equity_curve.len() - 1)];
|
|
123
|
+
let trough = min(dd_slice);
|
|
124
|
+
|
|
125
|
+
// Find trough index
|
|
126
|
+
let trough_relative_idx = dd_slice.iter().position(|&x| x == trough).unwrap_or(0);
|
|
127
|
+
let trough_idx = max_drawdown_start + trough_relative_idx;
|
|
128
|
+
|
|
129
|
+
// Peak before drawdown
|
|
130
|
+
let peak_at_trough = if max_drawdown_start > 0 {
|
|
131
|
+
equity_curve[max_drawdown_start - 1]
|
|
132
|
+
} else {
|
|
133
|
+
equity_curve[0]
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Find recovery point
|
|
137
|
+
for i in trough_idx..equity_curve.len() {
|
|
138
|
+
if equity_curve[i] >= peak_at_trough {
|
|
139
|
+
recovery_time = i - trough_idx;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
DrawdownResult {
|
|
146
|
+
max_drawdown,
|
|
147
|
+
max_drawdown_duration,
|
|
148
|
+
recovery_time,
|
|
149
|
+
drawdowns,
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/// Calculate underwater periods
|
|
154
|
+
///
|
|
155
|
+
/// Identifies periods where the equity curve is below its running maximum.
|
|
156
|
+
///
|
|
157
|
+
/// # Arguments
|
|
158
|
+
/// * `equity_curve` - Array of equity values over time
|
|
159
|
+
///
|
|
160
|
+
/// # Returns
|
|
161
|
+
/// Vector of (start_index, end_index) tuples representing underwater periods
|
|
162
|
+
///
|
|
163
|
+
/// # Example
|
|
164
|
+
/// ```
|
|
165
|
+
/// use quant_rust::risk::drawdown::underwater_periods;
|
|
166
|
+
/// let equity = vec![100.0, 110.0, 105.0, 95.0, 100.0, 90.0, 95.0, 110.0];
|
|
167
|
+
/// let periods = underwater_periods(&equity);
|
|
168
|
+
/// assert!(!periods.is_empty());
|
|
169
|
+
/// ```
|
|
170
|
+
pub fn underwater_periods(equity_curve: &[f64]) -> Vec<(usize, usize)> {
|
|
171
|
+
if equity_curve.is_empty() {
|
|
172
|
+
return vec![];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let mut periods: Vec<(usize, usize)> = Vec::new();
|
|
176
|
+
let mut peak = equity_curve[0];
|
|
177
|
+
let mut underwater = false;
|
|
178
|
+
let mut start_idx = 0;
|
|
179
|
+
|
|
180
|
+
for (i, &equity) in equity_curve.iter().enumerate() {
|
|
181
|
+
if equity < peak {
|
|
182
|
+
if !underwater {
|
|
183
|
+
underwater = true;
|
|
184
|
+
start_idx = i;
|
|
185
|
+
}
|
|
186
|
+
} else if equity >= peak {
|
|
187
|
+
if underwater {
|
|
188
|
+
periods.push((start_idx, i));
|
|
189
|
+
underwater = false;
|
|
190
|
+
}
|
|
191
|
+
peak = equity;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Handle case where we're still underwater at the end
|
|
196
|
+
if underwater {
|
|
197
|
+
periods.push((start_idx, equity_curve.len() - 1));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
periods
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
#[cfg(test)]
|
|
204
|
+
mod tests {
|
|
205
|
+
use super::*;
|
|
206
|
+
|
|
207
|
+
#[test]
|
|
208
|
+
fn test_calculate_drawdown() {
|
|
209
|
+
let equity = vec![100.0, 110.0, 105.0, 95.0, 100.0, 90.0, 95.0, 110.0];
|
|
210
|
+
let result = calculate_drawdown(&equity);
|
|
211
|
+
// Max drawdown: from 110 to 90 = 18.18%
|
|
212
|
+
assert!(result.max_drawdown > 0.0);
|
|
213
|
+
assert!(result.max_drawdown <= 1.0);
|
|
214
|
+
assert!(!result.drawdowns.is_empty());
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
#[test]
|
|
218
|
+
fn test_calculate_drawdown_empty() {
|
|
219
|
+
let result = calculate_drawdown(&[]);
|
|
220
|
+
assert_eq!(result.max_drawdown, 0.0);
|
|
221
|
+
assert_eq!(result.max_drawdown_duration, 0);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
#[test]
|
|
225
|
+
fn test_calculate_drawdown_single() {
|
|
226
|
+
let result = calculate_drawdown(&[100.0]);
|
|
227
|
+
assert_eq!(result.max_drawdown, 0.0);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
#[test]
|
|
231
|
+
fn test_calculate_drawdown_no_drawdown() {
|
|
232
|
+
let equity = vec![100.0, 110.0, 120.0, 130.0];
|
|
233
|
+
let result = calculate_drawdown(&equity);
|
|
234
|
+
assert_eq!(result.max_drawdown, 0.0);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
#[test]
|
|
238
|
+
fn test_underwater_periods() {
|
|
239
|
+
let equity = vec![100.0, 110.0, 105.0, 95.0, 100.0, 90.0, 95.0, 110.0];
|
|
240
|
+
let periods = underwater_periods(&equity);
|
|
241
|
+
assert!(!periods.is_empty());
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
#[test]
|
|
245
|
+
fn test_underwater_periods_empty() {
|
|
246
|
+
let periods = underwater_periods(&[]);
|
|
247
|
+
assert!(periods.is_empty());
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
#[test]
|
|
251
|
+
fn test_underwater_periods_no_underwater() {
|
|
252
|
+
let equity = vec![100.0, 110.0, 120.0, 130.0];
|
|
253
|
+
let periods = underwater_periods(&equity);
|
|
254
|
+
assert!(periods.is_empty());
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
//! Leverage and Margin Functions
|
|
2
|
+
//!
|
|
3
|
+
//! Liquidation prices, effective leverage, and margin requirements.
|
|
4
|
+
|
|
5
|
+
use serde::{Deserialize, Serialize};
|
|
6
|
+
|
|
7
|
+
/// Position side for leverage calculations
|
|
8
|
+
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
|
9
|
+
pub enum PositionSide {
|
|
10
|
+
/// Long position
|
|
11
|
+
Long,
|
|
12
|
+
/// Short position
|
|
13
|
+
Short,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/// Result of margin calculation
|
|
17
|
+
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
18
|
+
pub struct MarginResult {
|
|
19
|
+
/// Price at which position will be liquidated
|
|
20
|
+
pub liquidation_price: f64,
|
|
21
|
+
/// Effective leverage ratio
|
|
22
|
+
pub effective_leverage: f64,
|
|
23
|
+
/// Margin required to open position
|
|
24
|
+
pub margin_required: f64,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Calculate liquidation price for a leveraged position
|
|
28
|
+
///
|
|
29
|
+
/// Determines the price at which the position will be forcibly closed
|
|
30
|
+
/// due to insufficient margin.
|
|
31
|
+
///
|
|
32
|
+
/// # Arguments
|
|
33
|
+
/// * `entry_price` - Entry price of the position
|
|
34
|
+
/// * `leverage` - Leverage ratio (e.g., 10.0 for 10x)
|
|
35
|
+
/// * `side` - Position side (Long or Short)
|
|
36
|
+
/// * `maintenance_margin` - Maintenance margin requirement (default: 0.05)
|
|
37
|
+
///
|
|
38
|
+
/// # Returns
|
|
39
|
+
/// Liquidation price
|
|
40
|
+
///
|
|
41
|
+
/// # Example
|
|
42
|
+
/// ```
|
|
43
|
+
/// use quant_rust::risk::leverage::{liquidation_price, PositionSide};
|
|
44
|
+
/// let liq_price = liquidation_price(100.0, 10.0, PositionSide::Long, 0.05);
|
|
45
|
+
/// assert!(liq_price < 100.0);
|
|
46
|
+
/// ```
|
|
47
|
+
pub fn liquidation_price(
|
|
48
|
+
entry_price: f64,
|
|
49
|
+
leverage: f64,
|
|
50
|
+
side: PositionSide,
|
|
51
|
+
maintenance_margin: f64,
|
|
52
|
+
) -> f64 {
|
|
53
|
+
if leverage <= 0.0 {
|
|
54
|
+
return entry_price;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
match side {
|
|
58
|
+
PositionSide::Long => entry_price * (1.0 - (1.0 - maintenance_margin) / leverage),
|
|
59
|
+
PositionSide::Short => entry_price * (1.0 + (1.0 - maintenance_margin) / leverage),
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Calculate effective leverage
|
|
64
|
+
///
|
|
65
|
+
/// The actual leverage being used based on position size and equity.
|
|
66
|
+
///
|
|
67
|
+
/// # Arguments
|
|
68
|
+
/// * `position_value` - Total value of the position
|
|
69
|
+
/// * `equity` - Account equity
|
|
70
|
+
///
|
|
71
|
+
/// # Returns
|
|
72
|
+
/// Effective leverage ratio
|
|
73
|
+
///
|
|
74
|
+
/// # Example
|
|
75
|
+
/// ```
|
|
76
|
+
/// use quant_rust::risk::leverage::effective_leverage;
|
|
77
|
+
/// let lev = effective_leverage(10000.0, 2000.0);
|
|
78
|
+
/// assert!((lev - 5.0).abs() < 1e-10);
|
|
79
|
+
/// ```
|
|
80
|
+
pub fn effective_leverage(position_value: f64, equity: f64) -> f64 {
|
|
81
|
+
if equity == 0.0 {
|
|
82
|
+
return f64::INFINITY;
|
|
83
|
+
}
|
|
84
|
+
position_value / equity
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// Calculate margin requirement
|
|
88
|
+
///
|
|
89
|
+
/// The amount of collateral needed to open a position.
|
|
90
|
+
///
|
|
91
|
+
/// # Arguments
|
|
92
|
+
/// * `position_value` - Total value of the position
|
|
93
|
+
/// * `initial_margin` - Initial margin requirement (e.g., 0.10 for 10%)
|
|
94
|
+
///
|
|
95
|
+
/// # Returns
|
|
96
|
+
/// Margin required
|
|
97
|
+
///
|
|
98
|
+
/// # Example
|
|
99
|
+
/// ```
|
|
100
|
+
/// use quant_rust::risk::leverage::margin_requirement;
|
|
101
|
+
/// let margin = margin_requirement(10000.0, 0.10);
|
|
102
|
+
/// assert!((margin - 1000.0).abs() < 1e-10);
|
|
103
|
+
/// ```
|
|
104
|
+
pub fn margin_requirement(position_value: f64, initial_margin: f64) -> f64 {
|
|
105
|
+
position_value * initial_margin
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// Calculate comprehensive margin metrics
|
|
109
|
+
///
|
|
110
|
+
/// # Arguments
|
|
111
|
+
/// * `entry_price` - Entry price of the position
|
|
112
|
+
/// * `leverage` - Leverage ratio
|
|
113
|
+
/// * `side` - Position side
|
|
114
|
+
/// * `position_value` - Total value of the position
|
|
115
|
+
/// * `equity` - Account equity
|
|
116
|
+
/// * `initial_margin` - Initial margin requirement
|
|
117
|
+
/// * `maintenance_margin` - Maintenance margin requirement
|
|
118
|
+
///
|
|
119
|
+
/// # Returns
|
|
120
|
+
/// MarginResult with all metrics
|
|
121
|
+
///
|
|
122
|
+
/// # Example
|
|
123
|
+
/// ```
|
|
124
|
+
/// use quant_rust::risk::leverage::{calculate_margin, PositionSide};
|
|
125
|
+
/// let result = calculate_margin(100.0, 10.0, PositionSide::Long, 10000.0, 2000.0, 0.10, 0.05);
|
|
126
|
+
/// assert!(result.liquidation_price > 0.0);
|
|
127
|
+
/// assert!((result.effective_leverage - 5.0).abs() < 1e-10);
|
|
128
|
+
/// assert!((result.margin_required - 1000.0).abs() < 1e-10);
|
|
129
|
+
/// ```
|
|
130
|
+
pub fn calculate_margin(
|
|
131
|
+
entry_price: f64,
|
|
132
|
+
leverage: f64,
|
|
133
|
+
side: PositionSide,
|
|
134
|
+
position_value: f64,
|
|
135
|
+
equity: f64,
|
|
136
|
+
initial_margin: f64,
|
|
137
|
+
maintenance_margin: f64,
|
|
138
|
+
) -> MarginResult {
|
|
139
|
+
MarginResult {
|
|
140
|
+
liquidation_price: liquidation_price(entry_price, leverage, side, maintenance_margin),
|
|
141
|
+
effective_leverage: effective_leverage(position_value, equity),
|
|
142
|
+
margin_required: margin_requirement(position_value, initial_margin),
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#[cfg(test)]
|
|
147
|
+
mod tests {
|
|
148
|
+
use super::*;
|
|
149
|
+
|
|
150
|
+
#[test]
|
|
151
|
+
fn test_liquidation_price_long() {
|
|
152
|
+
let liq_price = liquidation_price(100.0, 10.0, PositionSide::Long, 0.05);
|
|
153
|
+
// Long position: liquidation below entry
|
|
154
|
+
assert!(liq_price < 100.0);
|
|
155
|
+
// 100 * (1 - 0.95/10) = 100 * 0.905 = 90.5
|
|
156
|
+
assert!((liq_price - 90.5).abs() < 1e-10);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
#[test]
|
|
160
|
+
fn test_liquidation_price_short() {
|
|
161
|
+
let liq_price = liquidation_price(100.0, 10.0, PositionSide::Short, 0.05);
|
|
162
|
+
// Short position: liquidation above entry
|
|
163
|
+
assert!(liq_price > 100.0);
|
|
164
|
+
// 100 * (1 + 0.95/10) = 100 * 1.095 = 109.5
|
|
165
|
+
assert!((liq_price - 109.5).abs() < 1e-10);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
#[test]
|
|
169
|
+
fn test_effective_leverage() {
|
|
170
|
+
let lev = effective_leverage(10000.0, 2000.0);
|
|
171
|
+
assert!((lev - 5.0).abs() < 1e-10);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
#[test]
|
|
175
|
+
fn test_effective_leverage_zero_equity() {
|
|
176
|
+
let lev = effective_leverage(10000.0, 0.0);
|
|
177
|
+
assert!(lev.is_infinite());
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
#[test]
|
|
181
|
+
fn test_margin_requirement() {
|
|
182
|
+
let margin = margin_requirement(10000.0, 0.10);
|
|
183
|
+
assert!((margin - 1000.0).abs() < 1e-10);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#[test]
|
|
187
|
+
fn test_calculate_margin() {
|
|
188
|
+
let result = calculate_margin(
|
|
189
|
+
100.0,
|
|
190
|
+
10.0,
|
|
191
|
+
PositionSide::Long,
|
|
192
|
+
10000.0,
|
|
193
|
+
2000.0,
|
|
194
|
+
0.10,
|
|
195
|
+
0.05,
|
|
196
|
+
);
|
|
197
|
+
assert!((result.liquidation_price - 90.5).abs() < 1e-10);
|
|
198
|
+
assert!((result.effective_leverage - 5.0).abs() < 1e-10);
|
|
199
|
+
assert!((result.margin_required - 1000.0).abs() < 1e-10);
|
|
200
|
+
}
|
|
201
|
+
}
|