跳转至

交易收益统计分析

交易收益分析的核心目标是基于无偏模拟获取的收益数据,客观评估交易系统的收益质量、风险水平及稳定性,避免因 “收益粒度选择不当” 或 “指标计算偏差” 导致的误判。其核心原理与思想可概括为以下四点:

  1. 收益粒度的标准化转换 不定前瞻期系统的单根 K 线收益转换:针对 “开仓 - 平仓时间不确定” 的交易系统(如突破策略),需将 “开仓到平仓的累计收益” 拆分为 “单根 K 线收益”—— 通过记录每根 K 线的持仓状态(多头 / 空头 / 无持仓),按对数价格差值计算单根 K 线收益(多头 = 下一根 K 线价格 - 当前 K 线价格,空头相反,无持仓为 0)。此转换既消除 “完整交易收益掩盖中间波动” 的信息丢失,又为后续细粒度分析提供基础。 多粒度收益适配不同分析场景:支持将单根 K 线收益进一步转换为三类实用粒度: 仅持仓 K 线收益:排除无持仓的零收益 K 线,聚焦系统 “实际参与交易时的表现”,避免零收益稀释真实收益水平; 完整交易收益:按 “开仓 - 平仓” 周期累计收益,贴合传统交易分析习惯,用于评估单笔交易的盈亏效率; 固定块收益:将连续 N 根 K 线收益累加(如 10 根 K 线为一个块),平滑短期随机波动,用于观察系统在中期时间维度的稳定性。
  2. 基于细粒度收益的核心指标计算 拒绝 “完整交易收益依赖”:强调基于 “单根 K 线收益” 或 “仅持仓 K 线收益” 计算指标,避免因完整交易收益 “数据量少、掩盖中间风险” 导致的误判。例如,盈利因子(盈利总和 / 亏损总和)若基于完整交易计算,可能因 “少数大盈利交易” 夸大性能,而基于细粒度收益计算则能反映真实盈亏结构。 核心指标体系: 平均收益:反映单位时间(或单位持仓 K 线)的预期收益,需用 “总收益 / 数据量” 计算,避免因数据量差异导致的不可比; 收益标准差:无偏估计(除以 n-1),衡量收益波动风险,标准差越小说明系统收益越稳定; 盈利因子:需区分 “盈利收益总和” 与 “亏损收益绝对值总和”,避免将零收益计入,且通过极小值(如 1e-60)防护分母为零的情况; 胜率:盈利次数 / 总收益次数,反映收益的稳定性,需结合盈利因子综合评估(高胜率低盈利因子可能因 “小赢大输” 仍无实际价值)。
  3. 收益数据的合法性校验 数据时序与完整性校验:分析前需校验 K 线时间戳是否严格递增(避免数据乱序导致的收益计算错误)、收益序列是否非空(至少 1 个有效收益点)、固定块大小是否为正整数(避免无效粒度划分),确保输入数据的合法性,避免因数据质量问题导致的分析偏差。 参数合理性校验:针对依赖参数的系统(如移动平均突破策略),需校验核心参数(如移动平均回溯期)是否为正,避免因参数非法导致的信号生成错误,确保收益数据的源头可靠。
  4. 风险与收益的平衡评估逻辑 拒绝 “单一指标依赖”:强调通过 “平均收益 + 标准差 + 盈利因子 + 胜率” 的组合指标评估系统,例如:高平均收益但高标准差的系统可能风险过高,高胜率但低盈利因子的系统可能 “小赢大输”,需综合判断风险与收益的性价比。 聚焦 “泛化能力验证”:所有收益分析需基于 “无偏模拟获取的样本外收益”,而非样本内收益。样本内收益仅用于参数优化,样本外收益才是评估系统未来表现的核心依据,避免因 “拟合样本内噪声” 导致的性能高估。
#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstdint>
#include <functional>
#include <iostream>
#include <numeric>
#include <stdexcept>
#include <vector>

// 错误码定义(收益分析的异常场景)
enum class TradeAnalysisErrCode : int32_t {
  SUCCESS = 0,              // 成功
  INSUFFICIENT_BARS = 1,    // K线数量不足(需至少2根计算收益)
  TIMESTAMP_ORDER_ERR = 2,  // K线时间戳非递增(数据乱序)
  INVALID_BLOCK_SIZE = 3,   // 固定块大小非法(≤0)
  EMPTY_RETURN_SEQ = 4,     // 收益序列为空(无法统计分析)
  INVALID_MA_PARAM = 5      // 移动平均参数非法(回溯期≤0)
};

// 对数价格K线数据结构(收益计算的尺度一致性要求)
struct LogPriceBar {
  double log_price;  // 对数价格(用于收益计算,避免价格绝对值偏差)
  int timestamp;     // 时间戳(确保数据按时间顺序处理,避免未来泄露)
};

// 交易信号类型定义(标记当前持仓状态,用于收益计算)
enum class TradeSignal {
  NO_POSITION,  // 无持仓(收益为0)
  LONG,         // 多头持仓(收益=下一根K线对数价格 - 当前K线对数价格)
  SHORT         // 空头持仓(收益=当前K线对数价格 - 下一根K线对数价格)
};

// 单根K线收益结果(记录收益值与对应持仓状态,用于后续粒度转换)
struct BarReturn {
  double return_val;     // 单根K线对数收益
  TradeSignal position;  // 该K线的持仓状态(无持仓/多头/空头)
};

// 1. 不定周期交易系统转单根K线收益工具
class DynamicToBarReturnConverter {
 public:
  /**
   * @brief 将不定周期系统的收益转换为单根K线粒度收益
   * @param price_bars 输入K线数据(对数价格+时间戳)
   * @param signal_generator 交易信号生成函数(输入K线索引,输出交易信号)
   * @param out_bar_returns 输出:单根K线收益列表(含持仓状态)
   * @return TradeAnalysisErrCode 错误码
   */
  static TradeAnalysisErrCode convert(
      const std::vector<LogPriceBar>& price_bars,
      const std::function<TradeSignal(size_t bar_idx)>& signal_generator,
      std::vector<BarReturn>& out_bar_returns) {
    // 校验K线数量(至少2根才能计算“当前-下一根”的收益)
    if (price_bars.size() < 2) {
      return TradeAnalysisErrCode::INSUFFICIENT_BARS;
    }

    // 校验时间戳递增(确保数据时序正确,避免未来泄露)
    for (size_t i = 1; i < price_bars.size(); ++i) {
      if (price_bars[i].timestamp <= price_bars[i - 1].timestamp) {
        return TradeAnalysisErrCode::TIMESTAMP_ORDER_ERR;
      }
    }

    const size_t bar_count = price_bars.size();
    out_bar_returns.clear();
    out_bar_returns.resize(bar_count - 1);               // 收益数量 = K线数 - 1
    TradeSignal current_pos = TradeSignal::NO_POSITION;  // 初始无持仓

    // 批量处理每根K线,生成对应收益
    for (size_t i = 0; i < bar_count - 1; ++i) {
      // 步骤1:根据外部信号更新当前持仓(无信号时维持原持仓)
      TradeSignal signal = signal_generator(i);
      if (signal != TradeSignal::NO_POSITION) {
        current_pos = signal;
      }

      // 步骤2:计算单根K线对数收益(基于当前持仓状态)
      double ret = 0.0;
      if (current_pos == TradeSignal::LONG) {
        ret = price_bars[i + 1].log_price - price_bars[i].log_price;
      } else if (current_pos == TradeSignal::SHORT) {
        ret = price_bars[i].log_price - price_bars[i + 1].log_price;
      }

      // 步骤3:保存收益与持仓状态
      out_bar_returns[i] = {ret, current_pos};
    }

    return TradeAnalysisErrCode::SUCCESS;
  }
};

// 2. 收益粒度转换工具(转换为不同分析粒度,支持多样化评估)
class ReturnGranularityConverter {
 public:
  /**
   * @brief 收益粒度类型(三种核心分析粒度)
   */
  enum class GranularityType {
    POSITION_ONLY,     // 仅保留有持仓的K线收益(排除无持仓的零收益)
    COMPLETED_TRADES,  // 完整交易收益(开仓到平仓的累计收益)
    FIXED_BLOCKS       // 固定K线块收益(指定数量K线的累计收益,平滑短期波动)
  };

  /**
   * @brief 转换收益粒度
   * @param bar_returns 输入:单根K线收益列表(含持仓状态)
   * @param granularity 目标粒度类型
   * @param out_returns 输出:转换后的收益序列
   * @param block_size 固定块大小(仅GranularityType::FIXED_BLOCKS时有效)
   * @return TradeAnalysisErrCode 错误码
   */
  static TradeAnalysisErrCode convert(const std::vector<BarReturn>& bar_returns,
                                      GranularityType granularity,
                                      std::vector<double>& out_returns,
                                      size_t block_size = 10) {
    out_returns.clear();

    // 根据目标粒度调用对应转换函数
    switch (granularity) {
      case GranularityType::POSITION_ONLY:
        return toPositionOnly(bar_returns, out_returns);
      case GranularityType::COMPLETED_TRADES:
        return toCompletedTrades(bar_returns, out_returns);
      case GranularityType::FIXED_BLOCKS:
        return toFixedBlocks(bar_returns, block_size, out_returns);
      default:
        return TradeAnalysisErrCode::SUCCESS;
    }
  }

 private:
  /**
   * @brief 转换为“仅持仓K线收益”(排除无持仓的零收益K线)
   * @param bar_returns 输入:单根K线收益列表
   * @param out_returns 输出:仅持仓K线收益序列
   * @return TradeAnalysisErrCode 错误码
   */
  static TradeAnalysisErrCode toPositionOnly(
      const std::vector<BarReturn>& bar_returns,
      std::vector<double>& out_returns) {
    // 预分配内存,提升性能(最坏情况:所有K线均有持仓)
    out_returns.reserve(bar_returns.size());
    for (const auto& br : bar_returns) {
      if (br.position != TradeSignal::NO_POSITION) {
        out_returns.push_back(br.return_val);
      }
    }
    return TradeAnalysisErrCode::SUCCESS;
  }

  /**
   * @brief 转换为“完整交易收益”(开仓到平仓的累计收益)
   * @param bar_returns 输入:单根K线收益列表
   * @param out_returns 输出:完整交易收益序列
   * @return TradeAnalysisErrCode 错误码
   */
  static TradeAnalysisErrCode toCompletedTrades(
      const std::vector<BarReturn>& bar_returns,
      std::vector<double>& out_returns) {
    double trade_profit = 0.0;  // 当前交易的累计收益
    bool in_trade = false;      // 是否处于持仓状态

    for (const auto& br : bar_returns) {
      if (br.position != TradeSignal::NO_POSITION) {
        // 累加持仓期间的收益(基于对数价格差值累计)
        trade_profit += br.return_val;
        in_trade = true;
      } else if (in_trade) {
        // 持仓结束,记录完整交易收益并重置
        out_returns.push_back(trade_profit);
        trade_profit = 0.0;
        in_trade = false;
      }
    }

    // 处理最后一根K线仍持仓的情况(强制平仓,避免收益遗漏)
    if (in_trade) {
      out_returns.push_back(trade_profit);
    }
    return TradeAnalysisErrCode::SUCCESS;
  }

  /**
   * @brief 转换为“固定K线块收益”(指定数量K线的累计收益)
   * @param bar_returns 输入:单根K线收益列表
   * @param block_size 固定块大小(每块包含的K线数量)
   * @param out_returns 输出:固定块收益序列
   * @return TradeAnalysisErrCode 错误码
   */
  static TradeAnalysisErrCode toFixedBlocks(
      const std::vector<BarReturn>& bar_returns, size_t block_size,
      std::vector<double>& out_returns) {
    // 校验固定块大小(需为正整数,确保粒度有效)
    if (block_size <= 0) {
      return TradeAnalysisErrCode::INVALID_BLOCK_SIZE;
    }

    double block_profit = 0.0;  // 当前块的累计收益
    size_t count = 0;           // 当前块的K线计数

    for (const auto& br : bar_returns) {
      // 累加当前块的收益(包含无持仓的零收益)
      block_profit += br.return_val;
      ++count;

      // 每累积指定数量K线,生成一个块收益
      if (count == block_size) {
        out_returns.push_back(block_profit);
        block_profit = 0.0;
        count = 0;
      }
    }

    return TradeAnalysisErrCode::SUCCESS;
  }
};

// 3. 交易收益统计分析工具(计算核心评估指标)
class TradeReturnAnalyzer {
 public:
  /**
   * @brief 统计结果结构体(核心评估指标)
   */
  struct Stats {
    double mean;           // 平均收益(对数价格收益的均值,反映收益能力)
    double std_dev;        // 收益标准差(风险度量,反映收益波动程度)
    double profit_factor;  // 盈利因子(盈利总和/亏损总和,反映收益质量)
    double win_rate;       // 胜率(盈利次数/总次数,反映收益稳定性)
    size_t total_count;    // 总数据量(统计样本数,确保结果可靠性)
  };

  /**
   * @brief 分析收益序列,计算核心统计指标
   * @param returns 输入:收益序列(对数收益)
   * @param out_stats 输出:统计结果
   * @return TradeAnalysisErrCode 错误码
   */
  static TradeAnalysisErrCode analyze(const std::vector<double>& returns,
                                      Stats& out_stats) {
    // 校验收益序列非空(需足够样本保证统计有效性)
    if (returns.empty()) {
      return TradeAnalysisErrCode::EMPTY_RETURN_SEQ;
    }

    out_stats.total_count = returns.size();

    // 步骤1:计算平均收益(总对数收益 / 总样本数)
    double sum_ret = std::accumulate(returns.begin(), returns.end(), 0.0);
    out_stats.mean = sum_ret / out_stats.total_count;

    // 步骤2:计算收益标准差(无偏估计,分母为n-1,反映风险)
    double sum_sq_diff = 0.0;
    for (double ret : returns) {
      sum_sq_diff += (ret - out_stats.mean) * (ret - out_stats.mean);
    }
    out_stats.std_dev =
        (out_stats.total_count > 1)
            ? std::sqrt(sum_sq_diff / (out_stats.total_count - 1))
            : 0.0;

    // 步骤3:计算盈利因子与胜率(基于对数收益评估收益质量)
    double win_sum = 0.0;   // 盈利总和(正收益的累加)
    double lose_sum = 0.0;  // 亏损总和(负收益的绝对值累加)
    size_t win_count = 0;   // 盈利次数

    for (double ret : returns) {
      if (ret > 0) {
        win_sum += ret;
        ++win_count;
      } else if (ret < 0) {
        lose_sum += std::abs(ret);
      }
    }

    // 避免除以零(添加极小值防护,处理无亏损场景)
    lose_sum = std::max(lose_sum, 1e-60);
    out_stats.profit_factor = win_sum / lose_sum;
    out_stats.win_rate = static_cast<double>(win_count) / out_stats.total_count;

    return TradeAnalysisErrCode::SUCCESS;
  }
};

// 4. 移动平均突破信号生成器(配套工具,生成交易信号)
class MABreakoutSignalGenerator {
 public:
  /**
   * @brief 构造函数:初始化移动平均参数与突破阈值
   * @param ma_lookback 移动平均回溯期(计算均线的K线数量)
   * @param breakout_thresh 突破阈值(如0.01表示对数价格突破均线1%)
   */
  MABreakoutSignalGenerator(size_t ma_lookback, double breakout_thresh)
      : ma_lookback_(ma_lookback), breakout_thresh_(breakout_thresh) {}

  /**
   * @brief 校验参数合法性(移动平均回溯期需为正)
   * @return TradeAnalysisErrCode 错误码
   */
  TradeAnalysisErrCode validateParams() const {
    if (ma_lookback_ <= 0) {
      return TradeAnalysisErrCode::INVALID_MA_PARAM;
    }
    return TradeAnalysisErrCode::SUCCESS;
  }

  /**
   * @brief 生成交易信号(基于对数价格突破均线阈值)
   * @param price_bars 输入K线数据(按时间戳排序)
   * @param bar_idx 当前K线索引(用于计算均线)
   * @return TradeSignal 交易信号(无持仓/多头)
   */
  TradeSignal generate(const std::vector<LogPriceBar>& price_bars,
                       size_t bar_idx) const {
    // 前N-1根K线无法计算完整均线,返回无持仓
    if (bar_idx < ma_lookback_ - 1) {
      return TradeSignal::NO_POSITION;
    }

    // 计算移动平均(对数价格均值,确保与价格尺度一致)
    double ma_sum = 0.0;
    for (size_t i = 0; i < ma_lookback_; ++i) {
      ma_sum += price_bars[bar_idx - i].log_price;
    }
    double ma = ma_sum / ma_lookback_;
    double current_log_price = price_bars[bar_idx].log_price;

    // 生成突破信号(对数价格突破均线阈值开多,跌破均线平仓)
    if (current_log_price > ma * (1.0 + breakout_thresh_)) {
      return TradeSignal::LONG;  // 突破阈值,开多
    } else if (current_log_price < ma) {
      return TradeSignal::NO_POSITION;  // 跌破均线,平仓
    } else {
      // 价格在均线与阈值之间,维持原持仓(外部需记录历史持仓)
      return TradeSignal::NO_POSITION;
    }
  }

 private:
  size_t ma_lookback_;      // 移动平均回溯期
  double breakout_thresh_;  // 突破阈值(相对均线的百分比)
};
#include "TradeAnalysis.h"

void testDynamicToBarReturnConverter() {
  std::cout << "Testing DynamicToBarReturnConverter...\n";

  // Test case 1: Normal case
  std::vector<LogPriceBar> bars = {
      {1.0, 100}, {1.1, 200}, {1.2, 300}, {1.3, 400}};

  std::function<TradeSignal(size_t)> signalGen = [](size_t idx) -> TradeSignal {
    return idx == 0 ? TradeSignal::LONG : TradeSignal::NO_POSITION;
  };

  std::vector<BarReturn> returns;
  TradeAnalysisErrCode result =
      DynamicToBarReturnConverter::convert(bars, signalGen, returns);

  assert(result == TradeAnalysisErrCode::SUCCESS);
  assert(returns.size() == 3);
  assert(std::abs(returns[0].return_val - 0.1) < 1e-10);
  assert(returns[0].position == TradeSignal::LONG);
  std::cout << " Test case 1 passed\n";

  // Test case 2: Insufficient bars
  std::vector<LogPriceBar> insufficientBars = {{1.0, 100}};
  result = DynamicToBarReturnConverter::convert(insufficientBars, signalGen,
                                                returns);
  assert(result == TradeAnalysisErrCode::INSUFFICIENT_BARS);
  std::cout << " Test case 2 passed\n";

  // Test case 3: Timestamp order error
  std::vector<LogPriceBar> unorderedBars = {{1.0, 200}, {1.1, 100}};
  result =
      DynamicToBarReturnConverter::convert(unorderedBars, signalGen, returns);
  assert(result == TradeAnalysisErrCode::TIMESTAMP_ORDER_ERR);
  std::cout << " Test case 3 passed\n";
}

void testReturnGranularityConverter() {
  std::cout << "Testing ReturnGranularityConverter...\n";

  // Setup test data
  std::vector<BarReturn> barReturns = {
      {0.05, TradeSignal::LONG},       {0.03, TradeSignal::LONG},
      {0.0, TradeSignal::NO_POSITION}, {-0.02, TradeSignal::SHORT},
      {-0.01, TradeSignal::SHORT},     {0.0, TradeSignal::NO_POSITION},
      {0.04, TradeSignal::LONG},       {0.02, TradeSignal::LONG}};

  // Test POSITION_ONLY conversion
  std::vector<double> positionOnlyReturns;
  TradeAnalysisErrCode result = ReturnGranularityConverter::convert(
      barReturns, ReturnGranularityConverter::GranularityType::POSITION_ONLY,
      positionOnlyReturns);

  assert(result == TradeAnalysisErrCode::SUCCESS);
  assert(positionOnlyReturns.size() == 6);
  assert(std::abs(positionOnlyReturns[0] - 0.05) < 1e-10);
  assert(std::abs(positionOnlyReturns[3] - (-0.02)) < 1e-10);
  std::cout << " POSITION_ONLY test passed\n";

  // Test COMPLETED_TRADES conversion
  std::vector<double> completedTradesReturns;
  result = ReturnGranularityConverter::convert(
      barReturns, ReturnGranularityConverter::GranularityType::COMPLETED_TRADES,
      completedTradesReturns);

  assert(result == TradeAnalysisErrCode::SUCCESS);
  assert(completedTradesReturns.size() == 3);
  assert(std::abs(completedTradesReturns[0] - 0.08) < 1e-10);  // 0.05 + 0.03
  assert(std::abs(completedTradesReturns[1] - (-0.03)) <
         1e-10);                                               // -0.02 + -0.01
  assert(std::abs(completedTradesReturns[2] - 0.06) < 1e-10);  // 0.04 + 0.02
  std::cout << " COMPLETED_TRADES test passed\n";

  // Test FIXED_BLOCKS conversion
  std::vector<double> fixedBlocksReturns;
  result = ReturnGranularityConverter::convert(
      barReturns, ReturnGranularityConverter::GranularityType::FIXED_BLOCKS,
      fixedBlocksReturns, 3);

  assert(result == TradeAnalysisErrCode::SUCCESS);
  assert(fixedBlocksReturns.size() == 2);
  assert(std::abs(fixedBlocksReturns[0] - 0.08) < 1e-10);  // 0.05 + 0.03 + 0.0
  assert(std::abs(fixedBlocksReturns[1] - (-0.03)) <
         1e-10);  // -0.02 + -0.01 + 0.0
  std::cout << " FIXED_BLOCKS test passed\n";

  // Test invalid block size
  result = ReturnGranularityConverter::convert(
      barReturns, ReturnGranularityConverter::GranularityType::FIXED_BLOCKS,
      fixedBlocksReturns, 0);

  assert(result == TradeAnalysisErrCode::INVALID_BLOCK_SIZE);
  std::cout << " Invalid block size test passed\n";
}

void testTradeReturnAnalyzer() {
  std::cout << "Testing TradeReturnAnalyzer...\n";

  // Test normal case
  std::vector<double> returns = {0.05, -0.02, 0.03, -0.01, 0.04};
  TradeReturnAnalyzer::Stats stats;
  TradeAnalysisErrCode result = TradeReturnAnalyzer::analyze(returns, stats);

  assert(result == TradeAnalysisErrCode::SUCCESS);
  assert(stats.total_count == 5);
  assert(std::abs(stats.mean - 0.018) <
         1e-10);  // (0.05 - 0.02 + 0.03 - 0.01 + 0.04) / 5
  std::cout << " Normal case test passed\n";

  // Test empty returns
  std::vector<double> emptyReturns;
  result = TradeReturnAnalyzer::analyze(emptyReturns, stats);
  assert(result == TradeAnalysisErrCode::EMPTY_RETURN_SEQ);
  std::cout << " Empty returns test passed\n";
}

void testMABreakoutSignalGenerator() {
  std::cout << "Testing MABreakoutSignalGenerator...\n";

  // Test parameter validation
  MABreakoutSignalGenerator generator(0, 0.01);
  TradeAnalysisErrCode result = generator.validateParams();
  assert(result == TradeAnalysisErrCode::INVALID_MA_PARAM);

  MABreakoutSignalGenerator validGenerator(3, 0.01);
  result = validGenerator.validateParams();
  assert(result == TradeAnalysisErrCode::SUCCESS);
  std::cout << " Parameter validation test passed\n";

  // Test signal generation
  std::vector<LogPriceBar> bars = {
      {1.0, 100},
      {1.05, 200},
      {1.1, 300},  // MA = 1.05
      {1.2, 400}   // 1.2 > 1.05 * 1.01, should generate LONG signal
  };

  TradeSignal signal = validGenerator.generate(bars, 0);
  assert(signal == TradeSignal::NO_POSITION);  // Not enough data

  signal = validGenerator.generate(bars, 2);
  assert(signal == TradeSignal::NO_POSITION);  // Price not突破 threshold

  signal = validGenerator.generate(bars, 3);
  assert(signal == TradeSignal::LONG);  // Price突破 threshold
  std::cout << " Signal generation test passed\n";
}

int main() {
  std::cout << "Starting tests...\n";

  testDynamicToBarReturnConverter();
  testReturnGranularityConverter();
  testTradeReturnAnalyzer();
  testMABreakoutSignalGenerator();

  std::cout << "All tests passed!\n";
  return 0;
}