4. 实现仓位管理模块

任务目标

实现一个仓位管理模块,支持开仓、平仓、加仓、减仓操作,并记录仓位方向(多/空)、数量、开仓价格等核心字段。模块以JavaScript类形式提供,可直接在Node.js或浏览器环境中运行验证。

核心实现

// PositionManager.js
class PositionManager {
  constructor() {
    this.positions = new Map(); // key: symbol, value: position object
  }

  // 内部:按symbol获取或初始化仓位
  _getOrCreate(symbol) {
    if (!this.positions.has(symbol)) {
      this.positions.set(symbol, {
        symbol,
        direction: null,   // 'long' | 'short' | null
        quantity: 0,       // 当前净持仓数量(正数=多,负数=空)
        avgPrice: 0,       // 平均开仓价格
        realizedPnl: 0     // 已实现盈亏(简化,不计手续费)
      });
    }
    return this.positions.get(symbol);
  }

  // 开仓:首次建立方向仓位
  open(symbol, direction, quantity, price) {
    if (quantity <= 0 || price <= 0) throw new Error('quantity和price必须为正数');
    const pos = this._getOrCreate(symbol);
    if (pos.quantity !== 0) throw new Error(`已有${pos.direction}仓位,请使用加仓或平仓`);

    pos.direction = direction;
    pos.quantity = direction === 'long' ? quantity : -quantity;
    pos.avgPrice = price;
    return { ...pos };
  }

  // 加仓:同方向增加数量,更新平均价格
  add(symbol, direction, quantity, price) {
    if (quantity <= 0 || price <= 0) throw new Error('quantity和price必须为正数');
    const pos = this._getOrCreate(symbol);
    if (pos.quantity === 0) throw new Error('无仓位,请先开仓');
    if (pos.direction !== direction) throw new Error(`方向不一致:当前${pos.direction},试图加${direction}`);

    const oldQty = Math.abs(pos.quantity);
    const newQty = oldQty + quantity;
    pos.avgPrice = (pos.avgPrice * oldQty + price * quantity) / newQty;
    pos.quantity = direction === 'long' ? newQty : -newQty;
    return { ...pos };
  }

  // 减仓:同方向减少数量,不改变平均价格
  reduce(symbol, direction, quantity, price) {
    if (quantity <= 0 || price <= 0) throw new Error('quantity和price必须为正数');
    const pos = this._getOrCreate(symbol);
    if (pos.quantity === 0) throw new Error('无仓位可减');
    if (pos.direction !== direction) throw new Error(`方向不一致:当前${pos.direction},试图减${direction}`);

    const currentQty = Math.abs(pos.quantity);
    if (quantity > currentQty) throw new Error('减仓数量超过当前持仓');

    // 计算已实现盈亏(简化:不考虑手续费)
    const exitPrice = price;
    const pnlPerUnit = direction === 'long' ? (exitPrice - pos.avgPrice) : (pos.avgPrice - exitPrice);
    pos.realizedPnl += pnlPerUnit * quantity;

    const remainQty = currentQty - quantity;
    if (remainQty === 0) {
      // 全部减完,清空仓位
      pos.direction = null;
      pos.quantity = 0;
      pos.avgPrice = 0;
    } else {
      pos.quantity = direction === 'long' ? remainQty : -remainQty;
    }
    return { ...pos };
  }

  // 平仓:一次性清空仓位
  close(symbol, price) {
    const pos = this._getOrCreate(symbol);
    if (pos.quantity === 0) throw new Error('无仓位可平');

    const direction = pos.direction;
    const qty = Math.abs(pos.quantity);
    const pnlPerUnit = direction === 'long' ? (price - pos.avgPrice) : (pos.avgPrice - price);
    pos.realizedPnl += pnlPerUnit * qty;

    pos.direction = null;
    pos.quantity = 0;
    pos.avgPrice = 0;
    return { ...pos };
  }

  // 查询当前仓位
  getPosition(symbol) {
    return this.positions.get(symbol) || null;
  }
}

// 导出(Node.js环境)
if (typeof module !== 'undefined' && module.exports) {
  module.exports = PositionManager;
}

运行与验证

将上述代码保存为 PositionManager.js,在Node.js中执行以下测试脚本验证功能:

// test.js
const PositionManager = require('./PositionManager');
const pm = new PositionManager();

// 1. 开多仓 2 ETH @ 3000
pm.open('ETH', 'long', 2, 3000);
let pos = pm.getPosition('ETH');
console.assert(pos.quantity === 2 && pos.avgPrice === 3000, '开仓失败');

// 2. 加仓 1 ETH @ 3300 → 平均价 (3000*2+3300*1)/3=3100
pm.add('ETH', 'long', 1, 3300);
pos = pm.getPosition('ETH');
console.assert(pos.quantity === 3 && pos.avgPrice === 3100, '加仓失败');

// 3. 减仓 1 ETH @ 3500 → 已实现盈亏 (3500-3100)*1=400
pm.reduce('ETH', 'long', 1, 3500);
pos = pm.getPosition('ETH');
console.assert(pos.quantity === 2 && pos.realizedPnl === 400, '减仓失败');

// 4. 平仓剩余 2 ETH @ 3600 → 已实现盈亏 400 + (3600-3100)*2=1400
pm.close('ETH', 3600);
pos = pm.getPosition('ETH');
console.assert(pos.quantity === 0 && pos.realizedPnl === 1400, '平仓失败');

// 5. 开空仓验证方向
pm.open('BTC', 'short', 1, 50000);
pos = pm.getPosition('BTC');
console.assert(pos.quantity === -1 && pos.direction === 'short', '开空失败');

console.log('✅ 所有断言通过');

运行命令:node test.js。若输出“✅ 所有断言通过”,说明模块功能正确。

验证要点

  • 方向与数量一致性:多仓数量为正,空仓数量为负;加仓/减仓必须与当前方向一致。
  • 平均价格计算:加仓后按加权平均更新;减仓和平仓不影响剩余仓位的平均价格。
  • 已实现盈亏:减仓和平仓时按退出价格与平均价格的差累计,多仓为退出价-均价,空仓为均价-退出价。
  • 边界条件:无仓位时开仓成功;已有仓位时重复开仓抛错;减仓数量超过持仓抛错;平仓后仓位归零。