///|
/// Robust risk snapshot for a return series.
pub struct RiskSnapshot {
count : Int
center : Double
scale : Double
downside : Double
upside : Double
value_at_risk : Double
conditional_var : Double
maximum_drawdown : Double
sharpe : Double
sortino : Double
tail_ratio : Double
}
///|
/// One completed drawdown episode.
pub struct DrawdownEpisode {
start : Int
trough : Int
recovery : Int
depth : Double
duration : Int
recovered : Bool
}
///|
/// Scenario configuration for stress analysis.
pub struct RiskRule {
confidence : Double
target : Double
risk_free : Double
stress_scale : Double
}
///|
pub fn risk_default_rule() -> RiskRule {
{ confidence: 0.95, target: 0.0, risk_free: 0.0, stress_scale: 2.0 }
}
///|
pub fn risk_rule(
confidence : Double,
target : Double,
risk_free : Double,
stress_scale : Double,
) -> RiskRule {
{
confidence: if confidence <= 0.0 {
0.5
} else if confidence >= 1.0 {
0.999
} else {
confidence
},
target,
risk_free,
stress_scale: if stress_scale < 0.0 {
-stress_scale
} else {
stress_scale
},
}
}
///|
pub fn risk_returns(prices : Array[Double]) -> Array[Double] {
let result = []
for index = 1; index < prices.length(); index = index + 1 {
if abs_double(prices[index - 1]) <= 1.0e-12 {
result.push(0.0)
} else {
result.push(
(prices[index] - prices[index - 1]) / abs_double(prices[index - 1]),
)
}
}
result
}
///|
pub fn risk_log_returns(prices : Array[Double]) -> Array[Double] {
let result = []
for index = 1; index < prices.length(); index = index + 1 {
if prices[index] <= 0.0 || prices[index - 1] <= 0.0 {
result.push(0.0)
} else {
result.push(drift_log(prices[index]) - drift_log(prices[index - 1]))
}
}
result
}
///|
pub fn risk_cumulative_returns(returns : Array[Double]) -> Array[Double] {
let result = []
let mut wealth = 1.0
for value in returns {
wealth *= 1.0 + value
result.push(wealth - 1.0)
}
result
}
///|
pub fn risk_wealth_curve(
returns : Array[Double],
initial : Double,
) -> Array[Double] {
let result = []
let mut wealth = initial
for value in returns {
wealth *= 1.0 + value
result.push(wealth)
}
result
}
///|
pub fn risk_annualized_return(returns : Array[Double], periods : Int) -> Double {
if returns.length() == 0 || periods <= 0 {
return 0.0
}
mean(returns) * periods.to_double()
}
///|
pub fn risk_annualized_volatility(
returns : Array[Double],
periods : Int,
) -> Double {
if returns.length() == 0 || periods <= 0 {
0.0
} else {
sample_stddev(returns) * periods.to_double().sqrt()
}
}
///|
pub fn risk_downside(returns : Array[Double], target : Double) -> Double {
downside_deviation(returns, target)
}
///|
pub fn risk_upside(returns : Array[Double], target : Double) -> Double {
upside_deviation(returns, target)
}
///|
pub fn risk_lower_tail(
returns : Array[Double],
probability : Double,
) -> Array[Double] {
let threshold = quantile(returns, probability)
let result = []
for value in returns {
if value <= threshold {
result.push(value)
}
}
result
}
///|
pub fn risk_upper_tail(
returns : Array[Double],
probability : Double,
) -> Array[Double] {
let threshold = quantile(returns, probability)
let result = []
for value in returns {
if value >= threshold {
result.push(value)
}
}
result
}
///|
pub fn risk_var(returns : Array[Double], confidence : Double) -> Double {
value_at_risk(returns, confidence)
}
///|
pub fn risk_cvar(returns : Array[Double], confidence : Double) -> Double {
conditional_value_at_risk(returns, confidence)
}
///|
pub fn risk_expected_shortfall(
returns : Array[Double],
confidence : Double,
) -> Double {
expected_shortfall(returns, confidence)
}
///|
pub fn risk_tail_ratio(returns : Array[Double], probability : Double) -> Double {
let lower = abs_double(quantile(returns, 1.0 - probability))
let upper = quantile(returns, probability)
if lower <= 1.0e-12 {
0.0
} else {
upper / lower
}
}
///|
pub fn risk_drawdown_series(wealth : Array[Double]) -> Array[Double] {
drawdown_series(wealth)
}
///|
pub fn risk_recovery_series(wealth : Array[Double]) -> Array[Double] {
let drawdowns = drawdown_series(wealth)
let result = []
let mut peak = 0.0
for index = 0; index < wealth.length(); index = index + 1 {
if wealth[index] > peak {
peak = wealth[index]
}
result.push(if peak == 0.0 { 0.0 } else { wealth[index] / peak })
}
ignore(drawdowns)
result
}
///|
pub fn risk_maximum_drawdown(wealth : Array[Double]) -> Double {
maximum_drawdown(wealth)
}
///|
pub fn risk_drawdown_duration(wealth : Array[Double]) -> Array[Int] {
let result = []
let drawdowns = drawdown_series(wealth)
let duration = 0
let mut current = duration
for value in drawdowns {
if value < 0.0 {
current += 1
} else {
current = 0
}
result.push(current)
}
result
}
///|
pub fn risk_drawdown_episodes(wealth : Array[Double]) -> Array[DrawdownEpisode] {
let result = []
let drawdowns = drawdown_series(wealth)
let index = 0
let mut cursor = index
while cursor < drawdowns.length() {
if drawdowns[cursor] >= 0.0 {
cursor += 1
} else {
let start = cursor
let mut trough = cursor
let mut end = cursor
while end + 1 < drawdowns.length() && drawdowns[end + 1] < 0.0 {
end += 1
if drawdowns[end] < drawdowns[trough] {
trough = end
}
}
let mut recovery = end + 1
while recovery < drawdowns.length() && drawdowns[recovery] < 0.0 {
recovery += 1
}
let recovered = recovery < drawdowns.length()
result.push({
start,
trough,
recovery: if recovered {
recovery
} else {
-1
},
depth: drawdowns[trough],
duration: if recovered {
recovery - start
} else {
drawdowns.length() - start
},
recovered,
})
cursor = if recovered { recovery } else { drawdowns.length() }
}
}
result
}
///|
pub fn risk_recovery_rate(wealth : Array[Double]) -> Double {
let episodes = risk_drawdown_episodes(wealth)
if episodes.length() == 0 {
1.0
} else {
let mut recovered = 0
for episode in episodes {
if episode.recovered {
recovered += 1
}
}
recovered.to_double() / episodes.length().to_double()
}
}
///|
pub fn risk_sharpe(returns : Array[Double], risk_free : Double) -> Double {
robust_sharpe_ratio(returns, risk_free~)
}
///|
pub fn risk_sortino(returns : Array[Double], target : Double) -> Double {
robust_sortino_ratio(returns, target~)
}
///|
pub fn risk_calmar(returns : Array[Double], risk_free : Double) -> Double {
let wealth = risk_wealth_curve(returns, 1.0)
let annual = risk_annualized_return(returns, 252)
let drawdown = abs_double(risk_maximum_drawdown(wealth))
if drawdown <= 1.0e-12 {
annual - risk_free
} else {
(annual - risk_free) / drawdown
}
}
///|
pub fn risk_snapshot(returns : Array[Double], rule : RiskRule) -> RiskSnapshot {
let wealth = risk_wealth_curve(returns, 1.0)
{
count: returns.length(),
center: median(returns),
scale: mad(returns) * 1.4826,
downside: risk_downside(returns, rule.target),
upside: risk_upside(returns, rule.target),
value_at_risk: risk_var(returns, rule.confidence),
conditional_var: risk_cvar(returns, rule.confidence),
maximum_drawdown: risk_maximum_drawdown(wealth),
sharpe: risk_sharpe(returns, rule.risk_free),
sortino: risk_sortino(returns, rule.target),
tail_ratio: risk_tail_ratio(returns, 1.0 - (1.0 - rule.confidence) / 2.0),
}
}
///|
pub fn risk_snapshot_vector(snapshot : RiskSnapshot) -> Array[Double] {
[
snapshot.count.to_double(),
snapshot.center,
snapshot.scale,
snapshot.downside,
snapshot.upside,
snapshot.value_at_risk,
snapshot.conditional_var,
snapshot.maximum_drawdown,
snapshot.sharpe,
snapshot.sortino,
snapshot.tail_ratio,
]
}
///|
pub fn risk_snapshot_lines(snapshot : RiskSnapshot) -> Array[String] {
[
"count=" + snapshot.count.to_string(),
"center=" + snapshot.center.to_string(),
"scale=" + snapshot.scale.to_string(),
"downside=" + snapshot.downside.to_string(),
"upside=" + snapshot.upside.to_string(),
"value_at_risk=" + snapshot.value_at_risk.to_string(),
"conditional_var=" + snapshot.conditional_var.to_string(),
"maximum_drawdown=" + snapshot.maximum_drawdown.to_string(),
"sharpe=" + snapshot.sharpe.to_string(),
"sortino=" + snapshot.sortino.to_string(),
"tail_ratio=" + snapshot.tail_ratio.to_string(),
]
}
///|
pub fn risk_snapshot_string(snapshot : RiskSnapshot) -> String {
risk_snapshot_lines(snapshot).join("\n")
}
///|
pub fn risk_stress_returns(
returns : Array[Double],
shock : Double,
) -> Array[Double] {
let result = []
for value in returns {
result.push(value - shock)
}
result
}
///|
pub fn risk_scale_stress(
returns : Array[Double],
scale : Double,
) -> Array[Double] {
let result = []
for value in returns {
result.push(value * scale)
}
result
}
///|
pub fn risk_reverse_stress(returns : Array[Double]) -> Array[Double] {
let result = []
for value in returns {
result.push(-value)
}
result
}
///|
pub fn risk_stress_table(
returns : Array[Double],
shocks : Array[Double],
) -> Array[Array[Double]] {
let result = []
for shock in shocks {
let stressed = risk_stress_returns(returns, shock)
let snapshot = risk_snapshot(stressed, risk_default_rule())
result.push([
shock,
snapshot.value_at_risk,
snapshot.conditional_var,
snapshot.maximum_drawdown,
snapshot.sharpe,
])
}
result
}
///|
pub fn risk_rolling_snapshot_score(
returns : Array[Double],
window : Int,
rule : RiskRule,
) -> Array[Double] {
let result = []
if window <= 0 {
return result
}
for end = window; end <= returns.length(); end = end + 1 {
let values = []
for index = end - window; index < end; index = index + 1 {
values.push(returns[index])
}
let snapshot = risk_snapshot(values, rule)
result.push(
abs_double(snapshot.value_at_risk) + abs_double(snapshot.maximum_drawdown),
)
}
result
}
///|
pub fn risk_rolling_var(
returns : Array[Double],
window : Int,
confidence : Double,
) -> Array[Double] {
let result = []
if window <= 0 {
return result
}
for end = window; end <= returns.length(); end = end + 1 {
let values = []
for index = end - window; index < end; index = index + 1 {
values.push(returns[index])
}
result.push(risk_var(values, confidence))
}
result
}
///|
pub fn risk_rolling_drawdown(
returns : Array[Double],
window : Int,
) -> Array[Double] {
let result = []
if window <= 0 {
return result
}
for end = window; end <= returns.length(); end = end + 1 {
let values = []
for index = end - window; index < end; index = index + 1 {
values.push(returns[index])
}
result.push(risk_maximum_drawdown(risk_wealth_curve(values, 1.0)))
}
result
}
///|
pub fn risk_contribution(
component_returns : Array[Array[Double]],
weights : Array[Double],
) -> Array[Double] {
let result = []
let total_weight = sum_absolute(weights)
for index = 0; index < component_returns.length(); index = index + 1 {
let contribution = if index < weights.length() {
abs_double(weights[index]) *
risk_snapshot(component_returns[index], risk_default_rule()).scale
} else {
0.0
}
result.push(
if total_weight == 0.0 {
0.0
} else {
contribution / total_weight
},
)
}
result
}
///|
pub fn risk_portfolio_return(
component_returns : Array[Array[Double]],
weights : Array[Double],
) -> Array[Double] {
let result = []
if component_returns.length() == 0 {
return result
}
let length = component_returns[0].length()
for time = 0; time < length; time = time + 1 {
let mut value = 0.0
for component = 0
component < component_returns.length()
component = component + 1 {
if component < weights.length() &&
time < component_returns[component].length() {
value += component_returns[component][time] * weights[component]
}
}
result.push(value)
}
result
}
///|
pub fn risk_portfolio_snapshot(
component_returns : Array[Array[Double]],
weights : Array[Double],
rule : RiskRule,
) -> RiskSnapshot {
risk_snapshot(risk_portfolio_return(component_returns, weights), rule)
}
///|
pub fn risk_concentration(weights : Array[Double]) -> Double {
let total = sum_absolute(weights)
if total == 0.0 {
0.0
} else {
sum_squared(weights) / (total * total)
}
}
///|
pub fn risk_effective_number(weights : Array[Double]) -> Double {
let concentration = risk_concentration(weights)
if concentration <= 1.0e-12 {
0.0
} else {
1.0 / concentration
}
}
///|
pub fn risk_diversification_score(weights : Array[Double]) -> Double {
let number = risk_effective_number(weights)
if weights.length() == 0 {
0.0
} else {
number / weights.length().to_double()
}
}
///|
pub fn risk_return_quality(returns : Array[Double], rule : RiskRule) -> Double {
let snapshot = risk_snapshot(returns, rule)
(snapshot.sharpe + snapshot.sortino + snapshot.tail_ratio) / 3.0
}
///|
pub fn risk_stress_sensitivity(
returns : Array[Double],
shocks : Array[Double],
rule : RiskRule,
) -> Array[Double] {
let base = risk_snapshot(returns, rule).value_at_risk
let result = []
for shock in shocks {
result.push(
risk_snapshot(risk_stress_returns(returns, shock), rule).value_at_risk -
base,
)
}
result
}