120 std::span<const double> returns,
121 std::span<
const std::array<double,3>> exposures,
122 const Config& cfg)
noexcept
124 const std::size_t n = returns.size();
125 if (n == 0 || n != exposures.size()) {
128 if (cfg.max_iterations <= 0 || cfg.learning_rate <= 0.0) {
133 std::vector<double> w(n, 1.0 /
static_cast<double>(n));
135 auto clamp_and_normalise = [&]() {
138 wi = std::clamp(wi, cfg.min_weight, cfg.max_weight);
141 const double total = std::accumulate(w.begin(), w.end(), 0.0);
143 std::fill(w.begin(), w.end(), 1.0 /
static_cast<double>(n));
145 for (
auto& wi : w) wi /= total;
149 clamp_and_normalise();
152 _portfolio_momentum(w, returns, exposures));
155 bool converged =
false;
157 for (
int iter = 0; iter < cfg.max_iterations; ++iter) {
163 constexpr double DELTA = 1e-6;
164 std::vector<double> grad(n, 0.0);
166 for (std::size_t i = 0; i < n; ++i) {
170 _portfolio_momentum(w, returns, exposures));
173 _portfolio_momentum(w, returns, exposures));
176 grad[i] = (m2_fwd - m2_bwd) / (2.0 * DELTA);
180 for (std::size_t i = 0; i < n; ++i) {
181 w[i] += cfg.learning_rate * grad[i];
184 clamp_and_normalise();
187 _portfolio_momentum(w, returns, exposures));
189 if (std::abs(new_m2 - prev_m2) < cfg.tolerance) {