跳转至

廉价训练偏差估计

廉价训练偏差估计 (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;
}