跳转至

参数敏感性曲线

参数敏感性曲线 (ParamSensitivity Class)

目标:直观地验证单个参数在整个可行域内扰动时,策略性能的稳定性。

核心原理:

单参数扰动: 从最优参数集开始,每次只选择一个参数,将其值从下限到上限均匀(或线性)地进行步进式变化。

保持其他参数不变: 在评估性能时,所有其他参数都固定在它们的最优值上。

性能评估: 对每个扰动点调用策略的性能评估函数(trade_system_criter)。

曲线解读: 最终生成的性能曲线(通常以文本直方图形式呈现)的形状决定了参数的稳健性:

平坦的曲线: 表明该参数在较大范围内变化时,性能保持稳定,参数鲁棒性高。

尖锐的峰值: 表明策略对该参数的取值非常敏感,可能存在过度拟合风险。

#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <vector>

// 错误码定义(与ParamCorrelation保持一致)
enum PostOptErr {
  POST_OPT_SUCCESS = 0,
  POST_OPT_EMPTY_DATA = 1,
  POST_OPT_NULL_PTR = 2,
  POST_OPT_MEM_ALLOC = 3,
  POST_OPT_INVALID_PARAM = 4
};

/**
 * @brief 性能评估函数类型定义
 * @param params 输入参数集(长度n_params)
 * @param min_trades 最小交易数约束(低于此值返回负性能)
 * @return double 性能值(如总收益、夏普比率等)
 */
typedef double (*PerformanceCriter)(const double* params, int min_trades);

class ParamSensitivity {
 private:
  int n_params;              // 优化参数总数
  int n_int_params;          // 整数参数数量(前n_int_params个参数为整数)
  double* best_params;       // 优化后的最优参数集(长度n_params)
  double* low_bounds;        // 参数下界(长度n_params)
  double* high_bounds;       // 参数上界(长度n_params)
  PerformanceCriter criter;  // 性能评估函数
  int min_trades;            // 最小交易数约束(性能评估时使用)

 public:
  /**
   * @brief 构造函数:初始化参数敏感性分析器
   * @param n_params 优化参数总数
   * @param n_int_params 整数参数数量(前n_int_params个)
   * @param best_params 优化后的最优参数集(长度n_params)
   * @param low_bounds 参数下界(长度n_params)
   * @param high_bounds 参数上界(长度n_params)
   * @param criter 性能评估函数
   * @param min_trades 最小交易数约束
   * @return PostOptErr 错误码
   */
  int init(int n_params, int n_int_params, const double* best_params,
           const double* low_bounds, const double* high_bounds,
           PerformanceCriter criter, int min_trades) {
    // 输入合法性检查
    if (n_params <= 0 || n_int_params < 0 || n_int_params > n_params)
      return POST_OPT_INVALID_PARAM;
    if (!best_params || !low_bounds || !high_bounds || !criter)
      return POST_OPT_NULL_PTR;

    this->n_params = n_params;
    this->n_int_params = n_int_params;
    this->min_trades = min_trades;
    this->criter = criter;

    // 内存分配
    this->best_params = new (std::nothrow) double[n_params]();
    this->low_bounds = new (std::nothrow) double[n_params]();
    this->high_bounds = new (std::nothrow) double[n_params]();

    // 内存分配检查
    if (!this->best_params || !this->low_bounds || !this->high_bounds) {
      if (this->best_params) delete[] this->best_params;
      if (this->low_bounds) delete[] this->low_bounds;
      if (this->high_bounds) delete[] this->high_bounds;
      this->best_params = nullptr;
      this->low_bounds = nullptr;
      this->high_bounds = nullptr;
      return POST_OPT_MEM_ALLOC;
    }

    // 复制输入数据
    memcpy(this->best_params, best_params, n_params * sizeof(double));
    memcpy(this->low_bounds, low_bounds, n_params * sizeof(double));
    memcpy(this->high_bounds, high_bounds, n_params * sizeof(double));

    return POST_OPT_SUCCESS;
  }

  /**
   * @brief 析构函数:释放内存
   */
  ~ParamSensitivity() {
    delete[] best_params;
    delete[] low_bounds;
    delete[] high_bounds;
  }

  /**
   * @brief 生成单个参数的敏感性曲线(文本直方图形式)
   * @param param_idx 目标参数索引(0~n_params-1)
   * @param n_points 扰动点数量(建议≥5,确保曲线平滑)
   * @param n_res 直方图分辨率(最大星号数量)
   * @param log_path 日志文件路径(追加模式写入)
   * @return PostOptErr 错误码
   */
  int generate_curve(int param_idx, int n_points, int n_res,
                     const char* log_path) {
    // 输入合法性检查
    if (param_idx < 0 || param_idx >= n_params) return POST_OPT_INVALID_PARAM;
    if (n_points < 5 || n_res < 10)
      return POST_OPT_INVALID_PARAM;  // 点数过少或分辨率过低
    if (!log_path || !criter) return POST_OPT_NULL_PTR;

    // 打开日志文件(追加模式)
    FILE* fp = fopen(log_path, "at");
    if (!fp) return POST_OPT_NULL_PTR;

    // 分配临时内存:参数缓冲区与性能缓冲区
    double* params = new (std::nothrow) double[n_params]();
    double* perf = new (std::nothrow) double[n_points]();
    if (!params || !perf) {
      delete[] params;
      delete[] perf;
      fclose(fp);
      return POST_OPT_MEM_ALLOC;
    }

    // ---------------------------------------------------------------------
    // 步骤1:初始化参数(仅扰动目标参数,其他参数保持最优值)
    // ---------------------------------------------------------------------
    memcpy(params, best_params, n_params * sizeof(double));
    double param_min = low_bounds[param_idx];
    double param_max = high_bounds[param_idx];
    double step = (param_max - param_min) / (n_points - 1);  // 扰动步长
    double max_perf = -1e60;  // 最大性能值(用于直方图缩放)

    // ---------------------------------------------------------------------
    // 步骤2:遍历所有扰动点,计算性能
    // ---------------------------------------------------------------------
    for (int i = 0; i < n_points; ++i) {
      // 设置当前参数值(区分整数/实数)
      if (param_idx < n_int_params) {
        // 整数参数:四舍五入到最近整数,并确保在[min, max]范围内
        double curr_val = param_min + i * step;
        curr_val = std::round(curr_val);
        curr_val = std::max(param_min, std::min(param_max, curr_val));
        params[param_idx] = curr_val;
      } else {
        // 实数参数:线性插值,直接赋值
        params[param_idx] = param_min + i * step;
      }

      // 评估性能(调用外部性能函数)
      perf[i] = criter(params, min_trades);
      // 更新最大性能值(用于后续直方图缩放)
      if (perf[i] > max_perf) max_perf = perf[i];
    }

    // ---------------------------------------------------------------------
    // 步骤3:生成文本直方图并写入日志
    // ---------------------------------------------------------------------
    fprintf(fp, "\n=== 参数敏感性曲线(参数%d) ===\n", param_idx + 1);
    fprintf(fp, "参数类型: %s\n", (param_idx < n_int_params) ? "整数" : "实数");
    fprintf(fp, "最优值: %.4f\n", best_params[param_idx]);
    fprintf(fp, "扰动范围: [%.4f, %.4f](%d个点)\n", param_min, param_max,
            n_points);
    fprintf(fp, "直方图分辨率: %d个星号(*)对应最大性能%.6f\n", n_res,
            max_perf);
    fprintf(fp, "----------------------------------------\n");

    // 计算直方图缩放比例(性能→星号数量)
    double hist_scale =
        (max_perf > 1e-10) ? (static_cast<double>(n_res) / max_perf) : 1.0;

    // 遍历每个扰动点,打印参数值与直方图
    for (int i = 0; i < n_points; ++i) {
      double curr_val = 0.0;
      // 获取当前参数值(区分整数/实数)
      if (param_idx < n_int_params) {
        curr_val = param_min + i * step;
        curr_val = std::round(curr_val);
        curr_val = std::max(param_min, std::min(param_max, curr_val));
        fprintf(fp, "%8d|", static_cast<int>(curr_val));  // 整数格式输出
      } else {
        curr_val = param_min + i * step;
        fprintf(fp, "%10.3f|", curr_val);  // 实数格式输出(保留3位小数)
      }

      // 计算星号数量(性能×缩放比例,限制在[0, n_res])
      int n_stars = static_cast<int>(perf[i] * hist_scale);
      n_stars = std::max(0, std::min(n_res, n_stars));

      // 打印星号
      for (int j = 0; j < n_stars; ++j) {
        fprintf(fp, "*");
      }
      fprintf(fp, "\n");
    }
    fprintf(fp, "----------------------------------------\n");

    // ---------------------------------------------------------------------
    // 清理临时内存并关闭文件
    // ---------------------------------------------------------------------
    delete[] params;
    delete[] perf;
    fclose(fp);
    return POST_OPT_SUCCESS;
  }

  /**
   * @brief 批量生成所有参数的敏感性曲线
   * @param n_points 每个参数的扰动点数量(建议≥5)
   * @param n_res 直方图分辨率(最大星号数量,建议≥20)
   * @param log_path 日志文件路径(覆盖模式写入)
   * @return PostOptErr 错误码
   */
  int generate_all_curves(int n_points, int n_res, const char* log_path) {
    // 输入合法性检查
    if (!log_path || !criter) return POST_OPT_NULL_PTR;
    if (n_points < 5 || n_res < 10) return POST_OPT_INVALID_PARAM;

    // 先创建新日志文件(覆盖原有内容)
    FILE* fp = fopen(log_path, "wt");
    if (!fp) return POST_OPT_NULL_PTR;
    fprintf(fp, "=== 所有参数敏感性曲线 ===\n");
    fprintf(fp, "参数总数: %d(整数参数: %d,实数参数: %d)\n", n_params,
            n_int_params, n_params - n_int_params);
    fprintf(fp, "每个参数扰动点数量: %d\n", n_points);
    fprintf(fp, "直方图分辨率: %d个星号\n", n_res);
    fprintf(fp, "最小交易数约束: %d\n\n", min_trades);
    fclose(fp);

    // 逐个参数生成敏感性曲线
    for (int i = 0; i < n_params; ++i) {
      int err = generate_curve(i, n_points, n_res, log_path);
      if (err != POST_OPT_SUCCESS) {
        return err;  // 遇到错误时终止并返回错误码
      }
    }

    return POST_OPT_SUCCESS;
  }
};
#include "ParamSensitivity.h"

// 测试用的性能评估函数
double test_performance_function(const double* params, int min_trades) {
  // 简单示例:返回参数的平方和作为性能指标
  double sum = 0.0;
  for (int i = 0; i < 3; i++) {
    sum += params[i] * params[i];
  }
  // 如果交易数不足,返回负值
  if (min_trades < 10) {
    return -1.0;
  }
  return 100.0 - sum;  // 最优值在参数为0时取得
}

// 测试函数
int test_param_sensitivity() {
  // 测试数据
  const int n_params = 3;
  const int n_int_params = 1;
  double best_params[n_params] = {0.0, 0.5, -0.3};
  double low_bounds[n_params] = {-5.0, -2.0, -1.0};
  double high_bounds[n_params] = {5.0, 2.0, 1.0};

  // 创建ParamSensitivity对象
  ParamSensitivity sensitivity;
  int result = sensitivity.init(n_params, n_int_params, best_params, low_bounds,
                                high_bounds, test_performance_function, 15);

  if (result != POST_OPT_SUCCESS) {
    printf("初始化失败,错误码: %d\n", result);
    return result;
  }

  // 测试生成单个参数曲线
  result = sensitivity.generate_curve(0, 10, 20, "sensitivity_test.log");
  if (result != POST_OPT_SUCCESS) {
    printf("生成单个参数曲线失败,错误码: %d\n", result);
    return result;
  }

  // 测试生成所有参数曲线
  result = sensitivity.generate_all_curves(8, 15, "sensitivity_all_test.log");
  if (result != POST_OPT_SUCCESS) {
    printf("生成所有参数曲线失败,错误码: %d\n", result);
    return result;
  }

  printf("所有测试通过\n");
  return POST_OPT_SUCCESS;
}

// 主函数用于运行测试
int main() {
  int result = test_param_sensitivity();
  if (result == POST_OPT_SUCCESS) {
    printf("ParamSensitivity测试成功完成\n");
  } else {
    printf("ParamSensitivity测试失败,错误码: %d\n", result);
  }
  return result;
}