跳转至

资产成本基础(ACB)计算

FIFO PnL calculator

计算盈亏(PnL)是Post-trade analysis的重要组成部分。

当交易者以买入价 c 购入 n 单位资产,随后以卖出价 s 处置相同数量的资产时,其盈亏可简单表示为 n(s−c)。

但现实交易更复杂,特别是当买入和卖出的数量不完全匹配时。考虑以下交易序列:

  • 时段A:以每股9.50元购入12股
  • 时段B:以每股10.80元增购18股
  • 时段C:以每股11.30元出售22股
  • 时段D:以每股10.95元出售剩余8股

在这种情况下,计算时段C的22股出售所实现的盈亏我们无法直接应用简单公式,因为没有单一的对应买入价格。

金融市场通常采用两种标准方法处理此类情况:FIFO和LIFO。

在FIFO方法下,系统按时间顺序匹配交易:最早购入的资产被视为最先售出。以上述案例为例,时段C售出的22股将被视为包含了时段A的全部12股及时段B的10股。

#include <queue>
#include <string>
#include <vector>

class FIFOPnLCalculator {
 private:
  struct Position {
    double price;
    int quantity;
    Position(double p, int q) : price(p), quantity(q) {}
  };

  std::queue<Position> positions;  // 持仓队列,遵循FIFO原则
  double totalPnL = 0.0;           // 累计盈亏

  // 错误码定义
  enum ErrorCode {
    SUCCESS = 0,
    INVALID_QUANTITY = 1,
    INSUFFICIENT_POSITION = 2
  };

 public:
  // 买入操作
  int buy(double price, int quantity) {
    if (quantity <= 0) {
      return INVALID_QUANTITY;
    }

    positions.push(Position(price, quantity));
    return SUCCESS;
  }

  // 卖出操作
  int sell(double price, int quantity, double* realizedPnL = nullptr) {
    if (quantity <= 0) {
      return INVALID_QUANTITY;
    }

    int remaining = quantity;
    double transactionPnL = 0.0;

    // 按照FIFO原则匹配持仓
    while (remaining > 0 && !positions.empty()) {
      Position& oldest = positions.front();

      if (oldest.quantity <= remaining) {
        // 全部消耗该批次持仓
        double batchPnL = (price - oldest.price) * oldest.quantity;
        transactionPnL += batchPnL;

        remaining -= oldest.quantity;
        positions.pop();
      } else {
        // 部分消耗该批次持仓
        double batchPnL = (price - oldest.price) * remaining;
        transactionPnL += batchPnL;

        oldest.quantity -= remaining;
        remaining = 0;
      }
    }

    // 检查是否有足够的持仓
    if (remaining > 0) {
      return INSUFFICIENT_POSITION;
    }

    totalPnL += transactionPnL;
    if (realizedPnL != nullptr) {
      *realizedPnL = transactionPnL;
    }

    return SUCCESS;
  }

  // 获取当前持仓
  std::vector<std::pair<double, int>> getCurrentPositions() const {
    std::vector<std::pair<double, int>> result;
    auto temp = positions;  // 创建副本
    while (!temp.empty()) {
      const Position& pos = temp.front();
      result.push_back(std::make_pair(pos.price, pos.quantity));
      temp.pop();
    }
    return result;
  }

  // 获取累计盈亏
  double getTotalPnL() const { return totalPnL; }

  // 检查是否有持仓
  bool hasPositions() const { return !positions.empty(); }

  // 获取持仓总量
  int getTotalPositionQuantity() const {
    int total = 0;
    auto temp = positions;
    while (!temp.empty()) {
      total += temp.front().quantity;
      temp.pop();
    }
    return total;
  }
};

ACB calculator

ACB(平均成本基准)中,PnL(盈亏)被分为两个组成部分:RPnL、UPnL

1. 已实现盈亏 (Realized PnL, RPnL)

  • 在平仓交易时计算并累积
  • 计算逻辑:
  • 部分平仓:(price - avg_price) * (-size),其中 -size 是平仓数量
  • 完全平仓:(price - avg_price) * pos,结算全部原仓位盈亏

2. 未实现盈亏 (Unrealized PnL, UPnL)

  • 计算逻辑:price * pos - suma
  • price:当前市场价格
  • pos:当前持仓数量
  • suma:总持仓成本(均价×数量)

3. 总盈亏

  • 计算公式:rpnl + (price * pos - suma)(已实现盈亏+未实现盈亏)

4. 与FIFO方法的区别

采用的是移动平均成本法:

  • 不跟踪每笔买入批次的明细
  • 仅维护总持仓成本(suma)和总持仓量(pos)
  • 平仓时使用平均成本而非最早批次成本计算盈亏
  • 适合实时盈亏风控。
#ifndef _ACB_H_
#define _ACB_H_

#include <cstdlib>

/// 基于交易记录计算未实现盈亏(UPnL)和已实现盈亏(RPnL)的类
/**
 * 该对象是不可变的(immutable):每次交易都会生成一个新的实例,
 * 而不会修改当前对象的状态。
 */
class ACB {
 public:
  /// 构造函数:初始化对象状态
  /**
   * @param init_price 初始开仓价格。如果初始仓位 init_pos 为 0,
   * 此参数将被忽略。
   * @param init_pos 初始持仓数量(可正可负,表示多空方向)。
   * @param init_rpnl 初始已实现盈亏(RPnL),默认为 0.0。
   */
  ACB(double init_price, double init_pos, double init_rpnl = 0.0)
      : suma(init_pos != 0.0 ? init_price * init_pos
                             : 0.0)  // 总持仓成本(均价 × 仓位)
        ,
        pos(init_pos)  // 当前仓位
        ,
        rpnl(init_rpnl)  // 已实现盈亏
  {}

  /// 记录一笔新交易,并返回更新后的新状态(不修改当前对象)
  /**
   * @param price 本次交易的价格(已扣除手续费后的有效价格)
   * @param size 本次交易的数量(正为买入/开多,负为卖出/开空)
   * @return 返回一个包含新状态的 ACB 对象
   *
   * 注意:调用方需自行处理交易手续费,传入的是净价格和净数量。
   */
  ACB operator()(double price, double size) const {
    // 快速路径:当前无持仓
    if (pos == 0.0) {
      return ACB(size * price, size, rpnl);
    }

    // 同方向交易(或交易量为零):增加持仓或维持不变
    if (size == 0.0 || (pos > 0.0) == (size > 0.0)) {
      return ACB(suma + size * price, pos + size, rpnl);
    }

    // 反方向交易:平仓或反向开仓
    double new_pos = pos + size;
    double avg_price = suma / pos;  // 此处 pos != 0,安全

    // 情况1:完全平仓或部分平仓,但未改变持仓方向
    if ((pos > 0.0 && new_pos >= 0.0) || (pos < 0.0 && new_pos <= 0.0)) {
      // 已实现盈亏 = (平仓价格 - 持仓均价) × 平仓数量
      // 平仓数量 = -size(因为 size 与 pos 方向相反)
      double realized = (price - avg_price) * (-size);
      return ACB(avg_price * new_pos, new_pos, rpnl + realized);
    }

    // 情况2:完全平掉原仓位,并反向开新仓
    // 先实现全部原仓位的盈亏,再以新价格建立剩余仓位
    double realized = (price - avg_price) * pos;  // 全部原仓位盈亏
    double remaining_size = new_pos;              // = size + pos,即反向仓位
    return ACB(remaining_size * price, remaining_size, rpnl + realized);
  }

  /// 重置已实现盈亏(RPnL)为 0,保留当前仓位和成本
  ACB resetRPnL() const { return ACB(suma, pos, 0.0); }

  /// 获取当前持仓的平均开仓价格(若无持仓则返回 0)
  double getOpen() const { return pos != 0.0 ? suma / pos : 0.0; }

  /// 获取当前持仓数量
  double getPos() const { return pos; }

  /// 获取累计已实现盈亏(Realized PnL)
  double getRPnL() const { return rpnl; }

  /// 计算当前未实现盈亏(Unrealized PnL)
  /**
   * @param price 当前市场价格
   * @return 未实现盈亏 = 市值 - 持仓成本 = price * pos - suma
   */
  double getUPnL(double price) const { return price * pos - suma; }

  /// 计算当前总权益(Equity = 已实现盈亏 + 未实现盈亏)
  /**
   * @param price 当前市场价格
   * @return 总权益 = rpnl + (price * pos - suma)
   */
  double getEquity(double price) const { return rpnl + (price * pos - suma); }

 private:
  // 私有构造函数,仅供内部使用(避免默认构造后再赋值)
  ACB(double s, double p, double r) : suma(s), pos(p), rpnl(r) {}

  double suma;  // 总持仓成本(即:平均开仓价 × 当前仓位)
  double pos;   // 当前持仓数量(正为多头,负为空头)
  double rpnl;  // 累计已实现盈亏(Realized PnL)
};

#endif