廉价训练偏差估计¶
廉价训练偏差估计 (StocBias Class)
目标:在不进行额外的样本外(OOS)回测的情况下,快速评估策略在样本内(IS)的平均性能与其在模拟样本外的平均性能之间的偏差。这是过度拟合的量化指标。
核心原理:
留一法思想 (Leave-One-Out): 利用优化初始种群中每个参数组合的单 Bar 级收益数据。
模拟 OOS: 将回测期内的每一个 Bar 都视为一个独立的 OOS 样本。
最优 IS 搜索: 对于任一 Bar $ t $,计算所有初始参数组合在排除 Bar $ t $ 后的总收益(即 IS 收益)。
更新最优: 在所有参数组合中,找到使 IS 收益最大的那个组合,并记录其对应的 IS 收益($ IS_best[t] $)和 Bar $ t $ 的收益($ OOS[t] $)。
计算偏差:
$ \text{Bias} = \text{Mean}(IS_best) - \text{Mean}(OOS) $
如果偏差显著为正,则表明策略对训练数据(IS)过度敏感,存在过度拟合风险。
#include <cassert>
#include <cmath>
#include <iostream>
#include <vector>
// 错误码定义
enum class StocBiasErr {
SUCCESS = 0,
INVALID_SIZE = 1, // 收益数量≤0
NOT_COLLECTING = 2, // 未开启数据收集
MEMORY_FAILED = 3 // 内存分配失败
};
class StocBias {
public:
/**
* @brief 构造函数:初始化训练偏差估计器
* @param n_returns 收益数量(与市场数据 bars 对应)
*/
explicit StocBias(int n_returns)
: n_returns_(n_returns),
collecting_(false),
got_first_case_(false),
returns_(nullptr),
IS_best_(nullptr),
OOS_(nullptr) {
if (n_returns_ <= 0) {
// 设置错误状态
n_returns_ = 0;
return;
}
// 内存分配
returns_ = new (std::nothrow) double[n_returns_]{};
IS_best_ = new (std::nothrow) double[n_returns_]{};
OOS_ = new (std::nothrow) double[n_returns_]{};
if (!returns_ || !IS_best_ || !OOS_) {
// 内存分配失败,清理已分配的内存
delete[] returns_;
delete[] IS_best_;
delete[] OOS_;
returns_ = nullptr;
IS_best_ = nullptr;
OOS_ = nullptr;
n_returns_ = 0;
}
}
~StocBias() {
delete[] returns_;
delete[] IS_best_;
delete[] OOS_;
}
/**
* @brief 开启/关闭数据收集
* @param collect true=开启,false=关闭
*/
void collect(bool collect) {
collecting_ = collect;
if (!collect) got_first_case_ = false; // 关闭时重置状态
}
/**
* @brief 获取收益数组指针(供外部写入当前参数集的bar-by-bar收益)
* @return double* 收益数组(长度n_returns_)
*/
double* expose_returns() { return returns_; }
/**
* @brief 处理当前参数集的收益,更新IS/OOS最优记录
* @return StocBiasErr 错误码
*/
StocBiasErr process() {
if (!collecting_) return StocBiasErr::NOT_COLLECTING;
if (n_returns_ <= 0) return StocBiasErr::INVALID_SIZE;
if (!returns_ || !IS_best_ || !OOS_) return StocBiasErr::MEMORY_FAILED;
// 1. 计算当前参数集的总收益
double total = 0.0;
for (int i = 0; i < n_returns_; ++i) {
total += returns_[i];
}
// 2. 初始化或更新IS_best与OOS
if (!got_first_case_) {
// 首次处理:IS_best[i] = 总收益 - returns[i](IS收益=排除第i个bar)
// OOS[i] = returns[i](OOS收益=第i个bar)
for (int i = 0; i < n_returns_; ++i) {
IS_best_[i] = total - returns_[i];
OOS_[i] = returns_[i];
}
got_first_case_ = true;
} else {
// 后续处理:更新IS_best[i](仅当当前IS收益更优)
for (int i = 0; i < n_returns_; ++i) {
double current_IS = total - returns_[i];
if (current_IS > IS_best_[i]) {
IS_best_[i] = current_IS;
OOS_[i] = returns_[i];
}
}
}
return StocBiasErr::SUCCESS;
}
/**
* @brief 计算训练偏差
* @param IS_return
* 输出:平均IS收益(每个IS_best[i]是n_returns_-1个bar的收益和)
* @param OOS_return 输出:平均OOS收益
* @param bias 输出:训练偏差(IS_return - OOS_return)
* @return StocBiasErr 错误码
*/
StocBiasErr compute(double* IS_return, double* OOS_return, double* bias) {
if (!got_first_case_) {
if (IS_return) *IS_return = 0.0;
if (OOS_return) *OOS_return = 0.0;
if (bias) *bias = 0.0;
return StocBiasErr::NOT_COLLECTING;
}
if (!IS_return || !OOS_return || !bias) {
return StocBiasErr::INVALID_SIZE;
}
if (n_returns_ <= 0) return StocBiasErr::INVALID_SIZE;
if (!returns_ || !IS_best_ || !OOS_) return StocBiasErr::MEMORY_FAILED;
// 1. 累加所有IS_best与OOS
double sum_IS = 0.0, sum_OOS = 0.0;
for (int i = 0; i < n_returns_; ++i) {
sum_IS += IS_best_[i];
sum_OOS += OOS_[i];
}
// 2. 计算平均收益(IS_best[i]是n_returns_-1个bar的和,需归一化)
*IS_return = sum_IS / (n_returns_ - 1);
*OOS_return = sum_OOS / n_returns_;
*bias = *IS_return - *OOS_return;
return StocBiasErr::SUCCESS;
}
private:
int n_returns_; // 收益数量(bar数量)
bool collecting_; // 是否开启数据收集
bool got_first_case_; // 是否处理过第一个参数集
double* returns_; // 当前参数集的bar-by-bar收益
double* IS_best_; // 每个bar作为OOS时的最优IS收益
double* OOS_; // 每个bar作为OOS时的收益
};
#include "StocBias.h"
// 测试函数
void test_basic_functionality() {
std::cout << "Testing basic functionality..." << std::endl;
StocBias sb(5);
sb.collect(true);
// 模拟第一个参数集的收益
double* returns = sb.expose_returns();
returns[0] = 0.01;
returns[1] = 0.02;
returns[2] = -0.01;
returns[3] = 0.03;
returns[4] = -0.02;
StocBiasErr err = sb.process();
assert(err == StocBiasErr::SUCCESS);
// 模拟第二个参数集的收益(更好的)
returns[0] = 0.02;
returns[1] = 0.03;
returns[2] = 0.00;
returns[3] = 0.04;
returns[4] = -0.01;
err = sb.process();
assert(err == StocBiasErr::SUCCESS);
// 计算偏差
double IS_return, OOS_return, bias;
err = sb.compute(&IS_return, &OOS_return, &bias);
assert(err == StocBiasErr::SUCCESS);
std::cout << "IS Return: " << IS_return << std::endl;
std::cout << "OOS Return: " << OOS_return << std::endl;
std::cout << "Bias: " << bias << std::endl;
std::cout << "Basic functionality test passed!" << std::endl;
}
void test_not_collecting() {
std::cout << "Testing not collecting state..." << std::endl;
StocBias sb(3);
// 不调用 collect(true),保持默认的 collecting_=false
double* returns = sb.expose_returns();
returns[0] = 0.01;
returns[1] = 0.02;
returns[2] = 0.03;
StocBiasErr err = sb.process();
assert(err == StocBiasErr::NOT_COLLECTING);
double IS_return, OOS_return, bias;
err = sb.compute(&IS_return, &OOS_return, &bias);
assert(err == StocBiasErr::NOT_COLLECTING);
assert(IS_return == 0.0);
assert(OOS_return == 0.0);
assert(bias == 0.0);
std::cout << "Not collecting test passed!" << std::endl;
}
void test_invalid_size() {
std::cout << "Testing invalid size..." << std::endl;
StocBias sb(-1); // 无效大小
double* returns = sb.expose_returns();
assert(returns == nullptr);
StocBiasErr err = sb.process();
assert(err == StocBiasErr::INVALID_SIZE);
std::cout << "Invalid size test passed!" << std::endl;
}
int main() {
std::cout << "Running StocBias tests with exceptions disabled..."
<< std::endl;
test_invalid_size();
test_not_collecting();
test_basic_functionality();
std::cout << "All tests passed!" << std::endl;
return 0;
}