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