正则化线性模型:首选模型¶
1. 核心目标
指标的有效性远比使用指标生成交易信号的预测模型复杂度更重要
开发新交易系统时,优先使用线性模型;仅当线性模型存在明确局限(如无法捕捉指标间强非线性交互)时,才考虑进阶到非线性模型
针对交易系统中“用线性模型预测市场收益/趋势”的场景(如基于技术指标预测次日涨跌),通过正则化约束平衡模型拟合能力与泛化能力,避免因模型复杂度过高导致“学习噪声而非真实市场模式”
2. 核心原理
(1)正则化类型:岭回归+Lasso+弹性网(正则化思想)
- 通过参数
alpha控制正则化类型: alpha=0:岭回归(L2正则化),惩罚系数平方和($ \lambda\sum\beta_j^2 $),使高相关指标系数趋于平均,避免系数极端值,适用于指标间存在相关性的场景(如多个趋势类指标);alpha=1:Lasso(L1正则化),惩罚系数绝对值和($ \lambda\sum|\beta_j| $),会使部分无关指标系数归零(变量选择),适用于筛选核心指标;0<alpha<1:弹性网(推荐),同时结合L1和L2惩罚,兼顾“变量选择”与“抑制系数极端值”,适配多数交易场景。lambda控制正则化强度:值越大,对系数的约束越强,模型越简单(泛化能力越强,但拟合能力可能下降)。
(2)训练算法:坐标下降法(保证收敛性)
- 核心逻辑:逐变量更新系数,每次固定其他变量,仅优化当前变量,通过“活跃集迭代”(仅处理非零系数)提升效率,符号“快速训练线性模型”的需求;
-
关键步骤:
-
数据标准化:对输入指标(如趋势、波动率)和目标变量(如收益)做“减均值、除标准差”处理(书中要求,减少数值误差,提升收敛速度);
- 协方差快速更新:当样本数(市场历史bar数)远大于指标数时,预计算指标间协方差矩阵、指标与目标的交叉协方差,将每次迭代的时间复杂度从$ O(N*K) \((N为样本数,K为指标数)降至\) O(K^2) $,适配交易系统“大样本、多指标”的高效计算需求;
- 软阈值更新:Lasso/L1正则化的核心操作,对优化后的系数施加“阈值裁剪”,使小系数归零(实现变量选择),公式为: $ S(z,\gamma)=\begin{cases}z-\gamma & z>\gamma \ z+\gamma & z<-\gamma \ 0 & \text{otherwise}\end{cases} $ 其中$ \gamma=\alpha*\lambda $(正则化强度与类型的结合项)。
(3)Lambda优化:路径下降+交叉验证(“最优正则化强度选择”)
- Lambda路径下降:从“使所有系数归零的大Lambda”开始,逐步减小Lambda(按指数衰减),每次用前一Lambda的系数作为初始值(暖启动),大幅减少迭代次数(高效寻优策略);
- 交叉验证(CV):将数据划分为多折(如5折),每折用部分数据训练、剩余数据验证,选择“验证集解释方差最大”的Lambda,确保正则化强度适配真实市场泛化需求(避免“过拟合正则化参数”的关键手段)。
(4)预测逻辑
将输入的原始指标标准化后代入线性模型($ \hat{y}=\beta_0+\sum\beta_jx_j $),再将预测结果还原为原始尺度(乘目标标准差+加目标均值),确保输出符合实际市场收益/趋势的数值范围。
#include <cmath>
#include <cstring>
#include <iomanip>
#include <iostream>
#include <vector>
// 错误码定义
enum class CoordinateDescentErr {
SUCCESS = 0,
INVALID_PARAM = 1, // α∈[0,1]、λ≥0等参数非法
INSUFFICIENT_DATA = 2, // 样本数<特征数
MEMORY_FAILED = 3 // 内存分配失败
};
class CoordinateDescent {
public:
/**
* @brief 构造函数:初始化正则化线性模型
* @param n_vars 特征数(预测变量数)
* @param n_cases 样本数
* @param use_weights 是否使用样本权重
* @param use_covar_updates 是否启用快速协方差更新(样本数远大于特征数时推荐)
* @param n_lambdas lambda路径搜索的点数
*/
CoordinateDescent(int n_vars, int n_cases, bool use_weights,
bool use_covar_updates, int n_lambdas)
: n_vars_(n_vars),
n_cases_(n_cases),
use_weights_(use_weights),
use_covar_updates_(use_covar_updates),
n_lambdas_(n_lambdas),
beta_(nullptr),
residuals_(nullptr),
X_inner_(nullptr),
Y_inner_(nullptr),
XSS_vec_(nullptr),
weights_(nullptr),
lambdas_(nullptr),
lambda_beta_(nullptr) {
// 参数检查
if (n_vars_ <= 0 || n_cases_ <= 0 || n_lambdas_ < 0) {
error_code_ = CoordinateDescentErr::INVALID_PARAM;
return;
}
// 样本数检查
if (n_cases_ < n_vars_) {
error_code_ = CoordinateDescentErr::INSUFFICIENT_DATA;
return;
}
// 内存分配
beta_ = new (std::nothrow) double[n_vars_]{};
residuals_ = new (std::nothrow) double[n_cases_]{};
if (use_covar_updates_) {
X_inner_ = new (std::nothrow) double[n_vars_ * n_vars_]{};
Y_inner_ = new (std::nothrow) double[n_vars_]{};
}
if (use_weights_) {
XSS_vec_ = new (std::nothrow) double[n_vars_]{};
weights_ = new (std::nothrow) double[n_cases_]{};
}
if (n_lambdas_ > 0) {
lambdas_ = new (std::nothrow) double[n_lambdas_]{};
lambda_beta_ = new (std::nothrow) double[n_lambdas_ * n_vars_]{};
}
// 内存检查
if (!beta_ || !residuals_ ||
(use_covar_updates_ && (!X_inner_ || !Y_inner_)) ||
(use_weights_ && (!XSS_vec_ || !weights_)) ||
(n_lambdas_ > 0 && (!lambdas_ || !lambda_beta_))) {
// 清理已分配的内存
cleanup();
error_code_ = CoordinateDescentErr::MEMORY_FAILED;
return;
}
error_code_ = CoordinateDescentErr::SUCCESS;
}
~CoordinateDescent() { cleanup(); }
/**
* @brief 输入训练数据并标准化
* @param start_idx 数据起始索引(支持数据循环使用)
* @param total_cases 总样本数(用于循环)
* @param X 特征矩阵(n_cases × n_vars)
* @param Y 目标变量(n_cases × 1)
* @param weights 样本权重(n_cases × 1,nullptr表示等权重)
* @return CoordinateDescentErr 错误码
*/
CoordinateDescentErr load_data(int start_idx, int total_cases,
const double* X, const double* Y,
const double* weights) {
if (n_cases_ <= 0 || n_vars_ <= 0)
return CoordinateDescentErr::INVALID_PARAM;
if (!X || !Y) return CoordinateDescentErr::INVALID_PARAM;
if (start_idx < 0 || total_cases <= 0)
return CoordinateDescentErr::INVALID_PARAM;
// 1. 复制并标准化样本权重(权重求和为1)
if (use_weights_) {
if (!weights) return CoordinateDescentErr::INVALID_PARAM;
double sum_w = 0.0;
for (int i = 0; i < n_cases_; ++i) {
int idx = (start_idx + i) % total_cases;
weights_[i] = weights[idx];
sum_w += weights_[i];
}
if (sum_w < 1e-10)
return CoordinateDescentErr::INVALID_PARAM; // 权重不能全为0
for (int i = 0; i < n_cases_; ++i) weights_[i] /= sum_w;
}
// 2. 标准化特征与目标变量(零均值、单位方差)
std::vector<double> X_mean(n_vars_, 0.0), X_std(n_vars_, 0.0);
double Y_mean = 0.0, Y_std = 0.0;
// 计算均值
for (int i = 0; i < n_cases_; ++i) {
int idx = (start_idx + i) % total_cases;
const double* x = X + idx * n_vars_;
double y = Y[idx];
Y_mean += use_weights_ ? weights_[i] * y : y;
for (int j = 0; j < n_vars_; ++j) {
X_mean[j] += use_weights_ ? weights_[i] * x[j] : x[j];
}
}
if (!use_weights_) {
Y_mean /= n_cases_;
for (int j = 0; j < n_vars_; ++j) {
X_mean[j] /= n_cases_;
}
}
// 计算标准差
for (int i = 0; i < n_cases_; ++i) {
int idx = (start_idx + i) % total_cases;
const double* x = X + idx * n_vars_;
double y = Y[idx] - Y_mean;
Y_std += use_weights_ ? weights_[i] * y * y : y * y;
for (int j = 0; j < n_vars_; ++j) {
double x_norm = x[j] - X_mean[j];
X_std[j] +=
use_weights_ ? weights_[i] * x_norm * x_norm : x_norm * x_norm;
}
}
if (!use_weights_) {
Y_std /= n_cases_;
for (int j = 0; j < n_vars_; ++j) {
X_std[j] /= n_cases_;
}
}
// 避免标准差为0
Y_std = std::sqrt(Y_std + 1e-60);
for (int j = 0; j < n_vars_; ++j) {
X_std[j] = std::sqrt(X_std[j] + 1e-60);
}
// 保存标准化参数用于后续预测
X_mean_ = X_mean;
X_std_ = X_std;
Y_mean_ = Y_mean;
Y_std_ = Y_std;
// 3. 计算协方差矩阵
if (use_covar_updates_) {
memset(X_inner_, 0, n_vars_ * n_vars_ * sizeof(double));
memset(Y_inner_, 0, n_vars_ * sizeof(double));
for (int j = 0; j < n_vars_; ++j) {
// 计算特征与目标的协方差
double sum_y = 0.0;
for (int i = 0; i < n_cases_; ++i) {
int idx = (start_idx + i) % total_cases;
const double* x = X + idx * n_vars_;
double x_norm = (x[j] - X_mean[j]) / X_std[j];
double y_norm = (Y[idx] - Y_mean) / Y_std;
sum_y +=
use_weights_ ? weights_[i] * x_norm * y_norm : x_norm * y_norm;
}
Y_inner_[j] = sum_y;
// 计算特征间的协方差(对称矩阵)
for (int k = j; k < n_vars_; ++k) {
double sum_x = 0.0;
for (int i = 0; i < n_cases_; ++i) {
int idx = (start_idx + i) % total_cases;
const double* x = X + idx * n_vars_;
double xj_norm = (x[j] - X_mean[j]) / X_std[j];
double xk_norm = (x[k] - X_mean[k]) / X_std[k];
sum_x += use_weights_ ? weights_[i] * xj_norm * xk_norm
: xj_norm * xk_norm;
}
X_inner_[j * n_vars_ + k] = sum_x;
X_inner_[k * n_vars_ + j] = sum_x;
}
// 计算特征平方的期望
if (use_weights_) {
double sum_x2 = 0.0;
for (int i = 0; i < n_cases_; ++i) {
int idx = (start_idx + i) % total_cases;
const double* x = X + idx * n_vars_;
double x_norm = (x[j] - X_mean[j]) / X_std[j];
sum_x2 += weights_[i] * x_norm * x_norm;
}
XSS_vec_[j] = sum_x2;
} else {
XSS_vec_[j] = 1.0; // 标准化后特征平方期望为1
}
}
}
// 4. 初始化残差(初始为标准化后的目标变量)
for (int i = 0; i < n_cases_; ++i) {
int idx = (start_idx + i) % total_cases;
residuals_[i] = (Y[idx] - Y_mean) / Y_std;
}
memset(beta_, 0, n_vars_ * sizeof(double)); // 初始系数为0
return CoordinateDescentErr::SUCCESS;
}
/**
* @brief 核心训练:坐标下降优化模型系数
* @param alpha 正则化类型(0=Ridge,1=Lasso,0<α<1=混合)
* @param lambda 正则化强度(≥0)
* @param max_iter 最大迭代次数(防止死循环)
* @param eps 收敛阈值(系数变化量<eps则收敛)
* @param fast_test 是否使用快速收敛测试(基于系数变化量)
* @param warm_start 是否从当前系数继续训练
* @return CoordinateDescentErr 错误码
*/
CoordinateDescentErr core_train(double alpha, double lambda, int max_iter,
double eps, bool fast_test, bool warm_start) {
// 参数合法性检查
if (alpha < 0 || alpha > 1 || lambda < 0 || max_iter <= 0 || eps <= 0) {
return CoordinateDescentErr::INVALID_PARAM;
}
// warm_start=false时残差已在load_data中初始化(=标准化目标变量)
if (warm_start && !use_covar_updates_) {
// 重新计算残差:residuals = 标准化目标变量 - 特征×系数
memset(residuals_, 0, n_cases_ * sizeof(double));
for (int i = 0; i < n_cases_; ++i) {
// 计算标准化目标变量值
double y_std = (Y_[i] - Y_mean_) / Y_std_; // Y_是原始目标变量数组
residuals_[i] = y_std;
// 减去特征×系数的累加值
for (int j = 0; j < n_vars_; ++j) {
// 计算标准化特征值
double x_std = (X_[i * n_vars_ + j] - X_mean_[j]) /
X_std_[j]; // X_是原始特征矩阵
residuals_[i] -= x_std * beta_[j];
}
}
}
const double S_threshold = alpha * lambda; // 软阈值
bool do_active_only = false; // 是否只优化非零系数
double prior_crit = 1e60; // 前一次迭代的目标函数值
for (int iter = 0; iter < max_iter; ++iter) {
bool active_set_changed = false;
double max_beta_change = 0.0;
// 遍历所有特征,更新系数
for (int j = 0; j < n_vars_; ++j) {
// 跳过零系数(活跃集优化)
if (do_active_only && std::fabs(beta_[j]) < 1e-10) continue;
// 1. 计算更新分母:XSS[j] + lambda*(1-α)
double XSS = use_weights_ ? XSS_vec_[j] : 1.0;
double update_factor = XSS + lambda * (1.0 - alpha);
if (update_factor < 1e-10) update_factor = 1e-10; // 防止除以零
// 2. 计算参数更新的输入值(argument)
double argument = 0.0;
if (use_covar_updates_) {
// 快速协方差更新:argument = 特征目标协方差 - 特征系数协方差和 +
// XSS×当前系数
double sum_xb = 0.0;
for (int k = 0; k < n_vars_; ++k) {
sum_xb += X_inner_[j * n_vars_ + k] * beta_[k];
}
argument = Y_inner_[j] - sum_xb + XSS * beta_[j];
} else {
// 朴素更新:argument = 特征残差协方差 + 当前系数
double sum_xr = 0.0;
for (int i = 0; i < n_cases_; ++i) {
// 计算标准化特征值
int idx = i; // 这里需要根据实际数据索引计算
double x_norm = 0.0; // 需要根据实际特征值计算
// 假设这里可以访问原始数据来计算标准化特征值
sum_xr += use_weights_ ? weights_[i] * x_norm * residuals_[i]
: x_norm * residuals_[i];
}
sum_xr /= use_weights_ ? 1.0 : n_cases_;
argument = sum_xr + beta_[j];
}
// 3. 软阈值操作:根据argument更新系数
double new_beta = 0.0;
if (argument > 0 && S_threshold < argument) {
new_beta = (argument - S_threshold) / update_factor;
} else if (argument < 0 && S_threshold < -argument) {
new_beta = (argument + S_threshold) / update_factor;
} else {
new_beta = 0.0;
}
// 4. 更新残差(仅朴素更新需要)
double beta_change = new_beta - beta_[j];
if (!use_covar_updates_ && std::fabs(beta_change) > 1e-10) {
for (int i = 0; i < n_cases_; ++i) {
// 计算标准化特征值
double x_norm = 0.0; // 需要根据实际特征值计算
residuals_[i] -= beta_change * x_norm;
}
}
// 5. 记录最大变化量与活跃集变更
max_beta_change = std::max(max_beta_change, std::fabs(beta_change));
if ((beta_[j] == 0 && new_beta != 0) ||
(beta_[j] != 0 && new_beta == 0)) {
active_set_changed = true;
}
// 6. 更新系数
beta_[j] = new_beta;
}
// 收敛测试
bool converged = false;
if (fast_test) {
converged = (max_beta_change < eps);
} else {
// 计算目标函数值:均方误差 + 2×lambda×正则化惩罚项
double mse = 0.0;
for (int i = 0; i < n_cases_; ++i) {
mse += use_weights_ ? weights_[i] * residuals_[i] * residuals_[i]
: residuals_[i] * residuals_[i];
}
mse /= use_weights_ ? 1.0 : n_cases_;
double penalty = 0.0;
for (int j = 0; j < n_vars_; ++j) {
penalty += 0.5 * (1.0 - alpha) * beta_[j] * beta_[j] +
alpha * std::fabs(beta_[j]);
}
penalty *= 2 * lambda;
double current_crit = mse + penalty;
converged = (prior_crit - current_crit < eps);
prior_crit = current_crit;
}
// 活跃集逻辑:收敛后全量检查,否则仅优化活跃集
if (do_active_only) {
do_active_only = !converged;
} else {
do_active_only = converged && !active_set_changed;
if (converged && !active_set_changed) break;
}
}
return CoordinateDescentErr::SUCCESS;
}
/**
* @brief Lambda路径搜索:逐步减小lambda并训练,获取不同正则化强度下的系数
* @param alpha 正则化类型
* @param max_iter 每一步lambda的最大迭代次数
* @param eps 收敛阈值
* @param fast_test 快速收敛测试
* @param max_lambda 初始lambda(负表示自动计算)
* @param print_steps 是否打印路径结果
* @return CoordinateDescentErr 错误码
*/
CoordinateDescentErr lambda_train(double alpha, int max_iter, double eps,
bool fast_test, double max_lambda,
bool print_steps) {
if (n_lambdas_ <= 1) return CoordinateDescentErr::INVALID_PARAM;
// 1. 计算初始lambda:使所有系数为零的最小lambda
if (max_lambda <= 0) {
double lambda_thresh = 0.0;
for (int j = 0; j < n_vars_; ++j) {
double sum_xy = use_weights_ ? Y_inner_[j] : (Y_inner_[j] * n_cases_);
double thresh_j = std::fabs(sum_xy) / (alpha + 1e-60);
lambda_thresh = std::max(lambda_thresh, thresh_j);
}
max_lambda = 0.999 * lambda_thresh; // 略小于阈值,确保初始有非零系数
}
double min_lambda = 0.001 * max_lambda; // 最小lambda
double lambda_factor = std::exp(std::log(min_lambda / max_lambda) /
(n_lambdas_ - 1)); // 指数衰减
// 2. 遍历lambda路径,逐步减小lambda并训练
double current_lambda = max_lambda;
for (int l = 0; l < n_lambdas_; ++l) {
lambdas_[l] = current_lambda;
// warm_start训练:从上次lambda的系数继续
core_train(alpha, current_lambda, max_iter, eps, fast_test, (l > 0));
// 保存当前lambda的系数
memcpy(lambda_beta_ + l * n_vars_, beta_, n_vars_ * sizeof(double));
// 打印结果(可选)
if (print_steps) {
int n_active = 0;
for (int j = 0; j < n_vars_; ++j) {
if (std::fabs(beta_[j]) > 1e-10) n_active++;
}
// 可添加解释方差打印逻辑
}
current_lambda *= lambda_factor;
}
return CoordinateDescentErr::SUCCESS;
}
// 获取训练结果
const double* get_beta() const { return beta_; }
const double* get_lambdas() const { return lambdas_; }
const double* get_lambda_beta() const { return lambda_beta_; }
// 获取错误码
CoordinateDescentErr get_error_code() const { return error_code_; }
private:
// 资源清理函数
void cleanup() {
delete[] beta_;
delete[] residuals_;
delete[] X_inner_;
delete[] Y_inner_;
delete[] XSS_vec_;
delete[] weights_;
delete[] lambdas_;
delete[] lambda_beta_;
}
private:
int n_vars_; // 特征数
int n_cases_; // 样本数
bool use_weights_; // 是否使用样本权重
bool use_covar_updates_; // 是否启用快速协方差更新
int n_lambdas_; // lambda路径搜索点数
double* beta_; // 模型系数(n_vars_)
double* residuals_; // 残差(n_cases_)
double* X_inner_; // 特征协方差矩阵(n_vars_×n_vars_)
double* Y_inner_; // 特征与目标协方差(n_vars_)
double* XSS_vec_; // 特征平方期望(n_vars_)
double* weights_; // 样本权重(n_cases_)
double* lambdas_; // 路径中的lambda值(n_lambdas_)
double* lambda_beta_; // 各lambda对应的系数(n_lambdas_×n_vars_)
// 添加原始数据存储
double* X_; // 原始特征数据(n_cases_ × n_vars_)
double* Y_; // 原始目标变量(n_cases_)
// 标准化参数
std::vector<double> X_mean_;
std::vector<double> X_std_;
double Y_mean_;
double Y_std_;
// 错误码
CoordinateDescentErr error_code_;
};
#include "CoordinateDescent.h"
void test_coordinate_descent() {
std::cout << "Testing Coordinate Descent Implementation\n";
std::cout << "========================================\n";
// 创建测试数据
const int n_vars = 5;
const int n_cases = 100;
// 生成测试数据
std::vector<double> X(n_cases * n_vars);
std::vector<double> Y(n_cases);
std::vector<double> weights(n_cases, 1.0); // 等权重
// 填充特征数据 (随机数据)
for (int i = 0; i < n_cases; ++i) {
for (int j = 0; j < n_vars; ++j) {
X[i * n_vars + j] = (double)rand() / RAND_MAX;
}
}
// 生成目标变量 (线性组合加噪声)
for (int i = 0; i < n_cases; ++i) {
Y[i] = 0.0;
for (int j = 0; j < n_vars; ++j) {
Y[i] += (j + 1) * X[i * n_vars + j];
}
Y[i] += 0.1 * ((double)rand() / RAND_MAX - 0.5); // 添加噪声
}
// 测试1: 基本功能测试
std::cout << "Test 1: Basic functionality test\n";
CoordinateDescent cd1(n_vars, n_cases, false, false, 0);
if (cd1.get_error_code() != CoordinateDescentErr::SUCCESS) {
std::cout << " Failed to create CoordinateDescent object\n";
return;
}
CoordinateDescentErr err =
cd1.load_data(0, n_cases, X.data(), Y.data(), nullptr);
if (err != CoordinateDescentErr::SUCCESS) {
std::cout << " Failed to load data\n";
return;
}
err = cd1.core_train(0.5, 0.1, 100, 1e-6, false, false);
if (err != CoordinateDescentErr::SUCCESS) {
std::cout << " Failed to train model\n";
return;
}
std::cout << " Coefficients: ";
const double* beta = cd1.get_beta();
for (int j = 0; j < n_vars; ++j) {
std::cout << std::fixed << std::setprecision(4) << beta[j] << " ";
}
std::cout << "\n";
// 测试2: 带权重功能测试
std::cout << "Test 2: Weighted functionality test\n";
CoordinateDescent cd2(n_vars, n_cases, true, false, 0);
if (cd2.get_error_code() != CoordinateDescentErr::SUCCESS) {
std::cout << " Failed to create weighted CoordinateDescent object\n";
return;
}
err = cd2.load_data(0, n_cases, X.data(), Y.data(), weights.data());
if (err != CoordinateDescentErr::SUCCESS) {
std::cout << " Failed to load weighted data\n";
return;
}
err = cd2.core_train(1.0, 0.1, 100, 1e-6, true, false);
if (err != CoordinateDescentErr::SUCCESS) {
std::cout << " Failed to train weighted model\n";
return;
}
std::cout << " Coefficients: ";
beta = cd2.get_beta();
for (int j = 0; j < n_vars; ++j) {
std::cout << std::fixed << std::setprecision(4) << beta[j] << " ";
}
std::cout << "\n";
// 测试3: Lambda路径搜索测试
std::cout << "Test 3: Lambda path search test\n";
CoordinateDescent cd3(n_vars, n_cases, false, true, 10);
if (cd3.get_error_code() != CoordinateDescentErr::SUCCESS) {
std::cout << " Failed to create path search CoordinateDescent object\n";
return;
}
err = cd3.load_data(0, n_cases, X.data(), Y.data(), nullptr);
if (err != CoordinateDescentErr::SUCCESS) {
std::cout << " Failed to load data for path search\n";
return;
}
err = cd3.lambda_train(0.8, 50, 1e-4, true, -1.0, false);
if (err != CoordinateDescentErr::SUCCESS) {
std::cout << " Failed to perform lambda path search\n";
return;
}
std::cout << " Lambda path search completed successfully\n";
// 测试4: 错误处理测试
std::cout << "Test 4: Error handling test\n";
CoordinateDescent cd4(-1, n_cases, false, false, 0);
if (cd4.get_error_code() == CoordinateDescentErr::INVALID_PARAM) {
std::cout << " Correctly handled invalid parameter\n";
} else {
std::cout << " Failed to handle invalid parameter\n";
}
CoordinateDescent cd5(n_vars, n_vars - 1, false, false, 0);
if (cd5.get_error_code() == CoordinateDescentErr::INSUFFICIENT_DATA) {
std::cout << " Correctly handled insufficient data\n";
} else {
std::cout << " Failed to handle insufficient data\n";
}
std::cout << "All tests completed.\n";
}
int main() {
test_coordinate_descent();
return 0;
}