///|
/// Multi-series robust aggregation for portfolio and sensor groups.
pub struct PortfolioMetric {
name : String
weight : Double
center : Double
scale : Double
contribution : Double
}
///|
pub struct PortfolioSummary {
value : Array[Double]
metrics : Array[PortfolioMetric]
diversification : Double
concentration : Double
}
///|
pub fn portfolio_metric(
name : String,
weight : Double,
data : Array[Double],
) -> PortfolioMetric {
{
name,
weight,
center: mean(data),
scale: mad(data),
contribution: weight * mean(data),
}
}
///|
pub fn portfolio_weights(weights : Array[Double]) -> Array[Double] {
let result = []
let mut total = 0.0
for weight in weights {
let safe = if weight < 0.0 { 0.0 } else { weight }
result.push(safe)
total += safe
}
if total == 0.0 {
return result
}
for index = 0; index < result.length(); index = index + 1 {
result[index] = result[index] / total
}
result
}
///|
pub fn portfolio_combine(
series : Array[Array[Double]],
weights : Array[Double],
) -> Array[Double] {
let result = []
if series.length() == 0 {
return result
}
let normalized = portfolio_weights(weights)
let length = series[0].length()
for index = 0; index < length; index = index + 1 {
let mut value = 0.0
for group = 0; group < series.length(); group = group + 1 {
if index < series[group].length() {
let weight = if group < normalized.length() {
normalized[group]
} else {
0.0
}
value += weight * series[group][index]
}
}
result.push(value)
}
result
}
///|
pub fn portfolio_summary(
names : Array[String],
series : Array[Array[Double]],
weights : Array[Double],
) -> PortfolioSummary {
let normalized = portfolio_weights(weights)
let values = portfolio_combine(series, normalized)
let metrics = []
for index = 0; index < series.length(); index = index + 1 {
let name = if index < names.length() {
names[index]
} else {
"series-" + index.to_string()
}
let weight = if index < normalized.length() {
normalized[index]
} else {
0.0
}
metrics.push(portfolio_metric(name, weight, series[index]))
}
let mut concentration = 0.0
for weight in normalized {
concentration += weight * weight
}
{
value: values,
metrics,
diversification: 1.0 - concentration,
concentration,
}
}
///|
pub fn portfolio_value(summary : PortfolioSummary) -> Array[Double] {
summary.value.copy()
}
///|
pub fn portfolio_metric_names(summary : PortfolioSummary) -> Array[String] {
let result = []
for metric in summary.metrics {
result.push(metric.name)
}
result
}
///|
pub fn portfolio_contributions(summary : PortfolioSummary) -> Array[Double] {
let result = []
for metric in summary.metrics {
result.push(metric.contribution)
}
result
}
///|
pub fn portfolio_risk(summary : PortfolioSummary) -> Double {
mad(summary.value)
}
///|
pub fn portfolio_quality(summary : PortfolioSummary) -> Double {
if summary.value.length() == 0 {
0.0
} else {
robust_signal_quality(summary.value)
}
}
///|
pub fn portfolio_summary_vector(summary : PortfolioSummary) -> Array[Double] {
[
summary.value.length().to_double(),
summary.metrics.length().to_double(),
summary.diversification,
summary.concentration,
portfolio_risk(summary),
portfolio_quality(summary),
]
}
///|
pub fn portfolio_summary_lines(summary : PortfolioSummary) -> Array[String] {
let lines = [
"count=" + summary.value.length().to_string(),
"metrics=" + summary.metrics.length().to_string(),
"diversification=" + summary.diversification.to_string(),
"concentration=" + summary.concentration.to_string(),
"risk=" + portfolio_risk(summary).to_string(),
"quality=" + portfolio_quality(summary).to_string(),
]
for metric in summary.metrics {
lines.push(
metric.name +
"|weight=" +
metric.weight.to_string() +
"|center=" +
metric.center.to_string() +
"|scale=" +
metric.scale.to_string() +
"|contribution=" +
metric.contribution.to_string(),
)
}
lines
}
///|
pub fn portfolio_summary_string(summary : PortfolioSummary) -> String {
portfolio_summary_lines(summary).join("\n")
}
///|
pub fn portfolio_rebalance(
summary : PortfolioSummary,
weights : Array[Double],
) -> Array[Double] {
portfolio_combine([summary.value], weights)
}
///|
pub fn portfolio_compare(
left : PortfolioSummary,
right : PortfolioSummary,
) -> Array[Double] {
[
portfolio_quality(left),
portfolio_quality(right),
portfolio_risk(left),
portfolio_risk(right),
left.diversification,
right.diversification,
]
}
///|
pub fn portfolio_stable(
left : PortfolioSummary,
right : PortfolioSummary,
tolerance : Double,
) -> Bool {
if left.value.length() != right.value.length() {
return false
}
for index = 0; index < left.value.length(); index = index + 1 {
if abs_double(left.value[index] - right.value[index]) > tolerance {
return false
}
}
true
}
///|
pub fn portfolio_batch(
series_sets : Array[Array[Array[Double]]],
weights : Array[Double],
) -> Array[Double] {
let result = []
for series in series_sets {
result.push(portfolio_quality(portfolio_summary([], series, weights)))
}
result
}