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
//! Multi-thread batch rebalancing over the account axis (roadmap 3.2).
//!
//! The persona-C workload is "one factor model, many accounts": every account
//! shares the risk model but carries its own constraints, budget, and
//! turnover anchor, and every account rolls through the same trading dates.
//! Accounts are independent — no state is shared between them — so the batch
//! axis parallelizes embarrassingly: [`solve_batch`] runs one
//! [`PortfolioSequence`](crate::PortfolioSequence) per account, exactly as if
//! the caller had looped over [`solve_sequence`](crate::solve_sequence).
//!
//! Threading is feature-gated: with the non-default `rayon` cargo feature the
//! accounts are distributed over rayon's global thread pool
//! (`RAYON_NUM_THREADS` or a caller-installed pool control the width);
//! without it the same function runs the accounts serially. Results are
//! identical either way — each account's iterate path never depends on the
//! other accounts or on scheduling — so the feature changes wall-clock time,
//! never answers.
//!
//! Failures stay per-account: one account with invalid step data reports its
//! error while every other account still returns its solutions. Within an
//! account the sequence semantics are unchanged (an invalid step aborts that
//! account; an unconverged date does not).
//!
//! ```
//! use ledge_core::{
//! solve_batch, BatchAccount, FactorCovariance, Matrix, PortfolioProblem, RebalanceStep,
//! };
//!
//! let problem = PortfolioProblem::new(
//! Matrix::new(3, 1, vec![1.0, -0.5, 0.25])?,
//! FactorCovariance::Diagonal(vec![0.1]),
//! vec![0.2, 0.3, 0.25],
//! vec![0.08, 0.04, 0.06],
//! )?;
//! let accounts = vec![
//! BatchAccount {
//! problem: problem.clone(),
//! steps: vec![RebalanceStep::default()],
//! chain_previous_weights: false,
//! },
//! BatchAccount {
//! problem,
//! steps: vec![RebalanceStep {
//! budget: Some(0.9),
//! ..RebalanceStep::default()
//! }],
//! chain_previous_weights: false,
//! },
//! ];
//! let results = solve_batch(&accounts, None);
//! assert_eq!(results.len(), 2);
//! for account in &results {
//! assert_eq!(account.as_ref().unwrap().len(), 1);
//! }
//! # Ok::<(), ledge_core::PortfolioError>(())
//! ```
use crate::;
/// One account in a batch: its own portfolio problem plus its per-date steps.
///
/// The problem carries the account's structure and first date's data; `steps`
/// are applied in order exactly like
/// [`solve_sequence`](crate::solve_sequence), so pass
/// [`RebalanceStep::default()`] first to solve the base data as-is.
/// Per-account outcome of [`solve_batch`]: all step solutions in order, or
/// the error that stopped that account.
pub type AccountResult = ;
/// Solves every account's rolling sequence, in parallel over the account
/// axis when the `rayon` cargo feature is enabled (roadmap 3.2).
///
/// Each account gets its own workspace (equilibration and factorizations
/// built once per account, warm starts chained across its dates), so the
/// batch does the same numerical work as calling
/// [`solve_sequence`](crate::solve_sequence) per account — results are
/// bit-identical to that loop regardless of the feature or thread count.
/// One entry is returned per account, in input order; a failed account never
/// affects the others.