跳转至

三角套利

假设有三个交易对(交易对指两种资产按照一定比例互相交易的市场)。

第一个为A-B,交换比例为1:10(即1个A可以兑换为10个B,10个B可以兑换为1个A,下同);

第二个为B-C,交换比例为1:100;

第三个为A-C,交换比例为1:1000。

假设我们手中有1个A资产,当Price(A,C)变为1:1100时,我们可以拿1个A通过第三个交易对,交换为1100个C,再通过第二个交易对将1100个C交换为11个B,再通过第一个交易对将11个B交换为1.1个A,从而实现0.1个A的盈利。

问题1: 当你收到的是Price(A,C)变为1:900时,是否可以盈利?如何操作?

问题2: 写一个函数,当监听到三个交易对中,任意一个交易对的交换比例发生变化时,判断是否有盈利机会,需如何操作。

问题3: 假设市场上有5万个类似的交易对,如何找到所有可以进行上述交易全部回环?

问题4: 设计一个数据结构,存储找到的所有回环,并写一个函数,当监听到5万个交易对中,任意一个交易对的交换比例发生变化时,使用该数据结构快速找到最盈利的那一条回环。

问题5: 如果我们用来交换的不是固定的1个A,而是0-1000个A,但由于冲击成本的存在,A的输入越大,收益率越低,但绝对收益可能在一定区间保持上升,如何设计算法去找到每条回环乃至所有回环的最大盈利。

统计套利策略(配对交易)

  1. 资产对筛选(代码逻辑): 计算候选资产对的价格差平方和$ \sum_{t=1}^{T}\left(S_{i,t}-S_{j,t}\right)^{2} $,选择最小值对应的资产对(如同一行业股票);
  2. 偏离判断(代码公式): 计算价格差的均值$ E[\Delta S_t]=\frac{1}{T}\sum_{t=1}^T \Delta S_t \(、标准差\) \sigma[\Delta S_t]=\sqrt{\frac{1}{T-1}\sum_{t=1}^T (\Delta S_t-E[\Delta S_t])^2} $;
  3. 交易触发: 若 $ \Delta S_\tau > E[\Delta S_\tau] + 2\sigma[\Delta S_\tau] $,代码执行“卖i买j”; 若 $ \Delta S_\tau < E[\Delta S_\tau] - 2\sigma[\Delta S_\tau] $,执行“买i卖j”。

问题1解答

当Price(A,C)变为1:900时,可以盈利,操作路径为A→B→C→A,具体步骤如下:

  1. 1个A兑换10个B(利用A-B的1:10比例);
  2. 10个B兑换1000个C(利用B-C的1:100比例,10×100=1000);
  3. 1000个C兑换约1.111个A(利用A-C的1:900比例,1000/900≈1.111)。

最终盈利约0.111个A,存在套利机会。

问题2:套利检测函数实现

将资产间的兑换比例建模为有向图的汇率矩阵,通过检测“循环路径的汇率乘积是否大于1”判断套利机会。对于3个资产,需检测两种核心循环路径:A→B→C→A和A→C→B→A。

#include <cmath>
#include <iostream>
#include <string>
#include <vector>

enum class Asset { A, B, C };

// 资产转字符串辅助函数
std::string assetStr(Asset a) {
  switch (a) {
    case Asset::A:
      return "A";
    case Asset::B:
      return "B";
    case Asset::C:
      return "C";
    default:
      return "Unknown";
  }
}

class ArbitrageChecker {
 private:
  // 汇率矩阵:rate[i][j]表示1单位资产i可兑换的资产j数量
  double rate[3][3] = {1, 0, 0, 0, 1, 0, 0, 0, 1};  // 初始化为单位矩阵

 public:
  // 初始化默认汇率
  ArbitrageChecker() {
    setRate(Asset::A, Asset::B, 1, 10);    // A-B:1:10
    setRate(Asset::B, Asset::C, 1, 100);   // B-C:1:100
    setRate(Asset::A, Asset::C, 1, 1000);  // A-C:1:1000
  }

  // 更新交易对汇率(a单位x兑换b单位y)
  void setRate(Asset x, Asset y, int a, int b) {
    int xi = static_cast<int>(x);
    int yi = static_cast<int>(y);
    rate[xi][yi] = static_cast<double>(b) / a;  // 1x换b/a y
    rate[yi][xi] = static_cast<double>(a) / b;  // 1y换a/b x
  }

  // 检测套利机会并返回操作步骤
  std::vector<std::string> check() {
    const double EPS = 1e-9;
    std::vector<std::string> steps;

    // 检测路径1:A→B→C→A
    double prod1 = rate[0][1] * rate[1][2] * rate[2][0];
    if (prod1 > 1.0 + EPS) {
      steps.emplace_back("套利路径: A→B→C→A");
      steps.emplace_back("盈利倍数: " + std::to_string(prod1 - 1.0));
      steps.emplace_back("操作:");
      steps.emplace_back("1. 1A → " + std::to_string(rate[0][1]) + "B");
      steps.emplace_back("2. " + std::to_string(rate[0][1]) + "B → " +
                         std::to_string(rate[0][1] * rate[1][2]) + "C");
      steps.emplace_back("3. " + std::to_string(rate[0][1] * rate[1][2]) +
                         "C → " + std::to_string(prod1) + "A");
      return steps;
    }

    // 检测路径2:A→C→B→A
    double prod2 = rate[0][2] * rate[2][1] * rate[1][0];
    if (prod2 > 1.0 + EPS) {
      steps.emplace_back("套利路径: A→C→B→A");
      steps.emplace_back("盈利倍数: " + std::to_string(prod2 - 1.0));
      steps.emplace_back("操作:");
      steps.emplace_back("1. 1A → " + std::to_string(rate[0][2]) + "C");
      steps.emplace_back("2. " + std::to_string(rate[0][2]) + "C → " +
                         std::to_string(rate[0][2] * rate[2][1]) + "B");
      steps.emplace_back("3. " + std::to_string(rate[0][2] * rate[2][1]) +
                         "B → " + std::to_string(prod2) + "A");
      return steps;
    }

    steps.emplace_back("无套利机会");
    return steps;
  }
};

// 测试问题1场景
int main() {
  ArbitrageChecker checker;
  checker.setRate(Asset::A, Asset::C, 1, 900);  // 更新A-C为1:900
  auto res = checker.check();
  for (const auto& s : res) std::cout << s << "\n";
  return 0;
}

问题3:海量交易对的回环检测

将交易对建模为有向图(节点为资产,边权重为汇率的对数),将“套利回环”转化为“正权环”检测问题(乘积>1等价于对数和>0)。由于长回环实际套利可能性低,限制回环长度(如≤5步),使用改良的贝尔曼-福特算法高效检测:

  1. 图建模:资产为节点,边X→Y权重为ln(rate[X][Y]);
  2. 短环检测:对每个节点,运行贝尔曼-福特算法检测≤5步的正权环(松弛操作次数限制为5);
  3. 回环收集:记录所有检测到的正权环,去重后即为有效回环。
#include <algorithm>
#include <cmath>
#include <iostream>
#include <limits>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>

using namespace std;

// 资产用整数ID表示
using AssetId = int;

// 有向边:from -> to,权重为ln(汇率)(方便将乘积转为求和)
struct Edge {
  AssetId from;
  AssetId to;
  double weight;  // ln(rate),即1单位from可兑换rate单位to,weight=ln(rate)
};

// 回环结构:存储路径和盈利倍数(汇率乘积)
struct Loop {
  vector<AssetId> path;
  double profit_multiplier;  // 路径汇率乘积(>1表示盈利)

  // 用于去重的哈希函数
  size_t hash() const {
    size_t h = 0;
    // 标准化路径:以最小资产ID为起点,顺时针排列
    auto min_it = min_element(path.begin(),
                              path.end() - 1);  // 排除最后一个(与第一个相同)
    int shift = min_it - path.begin();
    for (int i = 0; i < path.size() - 1; ++i) {
      AssetId id = path[(shift + i) % (path.size() - 1)];
      h ^= hash<AssetId>()(id) + 0x9e3779b9 + (h << 6) + (h >> 2);
    }
    return h;
  }

  // 用于去重的相等判断
  bool operator==(const Loop& other) const {
    if (path.size() != other.path.size()) return false;
    // 检查是否为同一环的不同起点
    auto find_pos = [&](AssetId id) {
      for (int i = 0; i < other.path.size() - 1; ++i) {
        if (other.path[i] == id) return i;
      }
      return -1;
    };
    int pos = find_pos(path[0]);
    if (pos == -1) return false;
    for (int i = 0; i < path.size() - 1; ++i) {
      if (path[i] != other.path[(pos + i) % (other.path.size() - 1)]) {
        return false;
      }
    }
    return true;
  }
};

// 为Loop提供哈希支持
namespace std {
template <>
struct hash<Loop> {
  size_t operator()(const Loop& l) const { return l.hash(); }
};
}  // namespace std

class ArbitrageLoopDetector {
 private:
  vector<Edge> edges;
  unordered_set<AssetId> all_assets;
  const int MAX_LOOP_LENGTH = 5;  // 最大回环长度(实际套利中长环无意义)
  const double EPS = 1e-8;        // 浮点数精度

 public:
  // 添加交易对:a单位from兑换b单位to(即1单位from可兑换b/a单位to)
  void addTradingPair(AssetId from, AssetId to, int a, int b) {
    double rate = static_cast<double>(b) / a;
    edges.push_back({from, to, log(rate)});                        // 正向边权重
    edges.push_back({to, from, log(static_cast<double>(a) / b)});  // 反向边权重
    all_assets.insert(from);
    all_assets.insert(to);
  }

  // 检测所有盈利回环
  unordered_set<Loop> findAllLoops() {
    unordered_set<Loop> loops;

    // 对每个资产作为起点检测回环
    for (AssetId start : all_assets) {
      // 距离数组:dist[k][v]表示从start出发,经过k步到达v的最大权重和
      vector<unordered_map<AssetId, double>> dist(MAX_LOOP_LENGTH + 1);
      // 前驱数组:prev[k][v] = 到达v的前一个节点(用于回溯路径)
      vector<unordered_map<AssetId, AssetId>> prev(MAX_LOOP_LENGTH + 1);

      // 初始化:0步到达start,权重和为0
      dist[0][start] = 0.0;

      // 松弛操作:最多MAX_LOOP_LENGTH步
      for (int k = 1; k <= MAX_LOOP_LENGTH; ++k) {
        // 复制上一步的距离(不操作的情况)
        dist[k] = dist[k - 1];
        prev[k] = prev[k - 1];

        // 遍历所有边,尝试松弛
        for (const Edge& e : edges) {
          AssetId u = e.from;
          AssetId v = e.to;
          // 如果上一步能到达u,尝试更新v
          if (dist[k - 1].count(u)) {
            double new_weight = dist[k - 1][u] + e.weight;
            // 找到更优路径(权重和更大)
            if (!dist[k].count(v) || new_weight > dist[k][v] + EPS) {
              dist[k][v] = new_weight;
              prev[k][v] = u;
            }
          }
        }

        // 检查是否形成回环(从start出发,k步回到start,且权重和>0)
        if (dist[k].count(start) && dist[k][start] > EPS) {
          // 回溯路径
          vector<AssetId> path;
          AssetId current = start;
          int steps = k;
          while (steps > 0) {
            path.push_back(current);
            current = prev[steps][current];
            steps--;
          }
          path.push_back(start);              // 闭合回环
          reverse(path.begin(), path.end());  // 恢复正确顺序

          // 计算盈利倍数(汇率乘积 = exp(权重和))
          double profit = exp(dist[k][start]);
          loops.insert({path, profit});
        }
      }
    }

    return loops;
  }
};

// 测试示例
int main() {
  ArbitrageLoopDetector detector;

  // 添加问题1中的3个交易对
  detector.addTradingPair(0, 1, 1, 10);   // A(0)-B(1):1:10
  detector.addTradingPair(1, 2, 1, 100);  // B(1)-C(2):1:100
  detector.addTradingPair(0, 2, 1, 900);  // A(0)-C(2):1:900(问题1场景)

  // 检测回环
  auto loops = detector.findAllLoops();

  // 输出结果
  cout << "找到" << loops.size() << "个盈利回环:" << endl;
  for (const Loop& loop : loops) {
    cout << "路径:";
    for (AssetId id : loop.path) {
      cout << (char)('A' + id) << " → ";
    }
    cout << "\b\b\b " << endl;  // 移除最后一个" → "
    cout << "盈利倍数:" << loop.profit_multiplier << "(盈利:"
         << (loop.profit_multiplier - 1) << ")" << endl;
    cout << "------------------------" << endl;
  }

  return 0;
}

问题4:高效回环存储与最大盈利查询

数据结构

  1. 回环结构体(Loop):
struct Loop {
  std::vector<Asset> path;                     // 回环路径(如[A,B,C,A])
  double profit;                               // 当前盈利倍数
  std::vector<std::pair<Asset, Asset>> edges;  // 依赖的交易对
};
  1. 索引结构:

  2. edge_to_loops:哈希表(键为交易对(X,Y),值为依赖该交易对的回环列表);

  3. profit_set:有序集合(按profit降序,存储(profit, Loop*),支持快速查询最大值)。

更新与查询流程

  1. 交易对更新时:

  2. 遍历edge_to_loops中该交易对对应的所有回环;

  3. 重新计算回环的profit,并在profit_set中更新(删除旧值,插入新值)。

  4. 查询最大盈利:直接返回profit_set的首个元素(O(1)复杂度)。

#include <algorithm>
#include <cmath>
#include <iostream>
#include <limits>
#include <set>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>

// 基础类型定义
using AssetId = int;  // 资产ID(整数表示,支持扩展)
using TradingPair = std::pair<AssetId, AssetId>;  // 有向交易对(X→Y,区分方向)

// 资产ID转字符串
std::string assetToStr(AssetId id) { return std::string(1, 'A' + id); }

// 交易对转字符串
std::string pairToStr(const TradingPair& pair) {
  return assetToStr(pair.first) + "→" + assetToStr(pair.second);
}

// 回环结构体:存储回环完整信息及依赖关系
struct Loop {
  std::vector<AssetId> path;       // 回环路径(如[A,B,C,A],首尾需相同)
  double profit_multiplier;        // 盈利倍数(汇率乘积,>1表示盈利)
  std::vector<TradingPair> edges;  // 依赖的有向交易对(与path一一对应)

  // 构造函数:传入路径,自动生成依赖的交易对
  Loop(std::vector<AssetId> p) : path(std::move(p)) {
    // 生成交易对(如路径[0,1,2,0]→[(0,1),(1,2),(2,0)])
    for (size_t i = 0; i < path.size() - 1; ++i) {
      edges.emplace_back(path[i], path[i + 1]);
    }
    profit_multiplier = 0.0;  // 初始值,后续通过汇率计算
  }

  // 打印回环信息(辅助调试与结果展示)
  void print() const {
    std::cout << "回环路径:";
    for (size_t i = 0; i < path.size(); ++i) {
      if (i > 0) std::cout << "→";
      std::cout << assetToStr(path[i]);
    }
    std::cout << "\n依赖交易对:";
    for (size_t i = 0; i < edges.size(); ++i) {
      if (i > 0) std::cout << ", ";
      std::cout << pairToStr(edges[i]);
    }
    std::cout << "\n当前盈利倍数:" << profit_multiplier << "(盈利:"
              << (profit_multiplier - 1) * 100 << "%)\n";
  }
};

// 核心索引类:维护回环双索引,支持交易对更新与最大盈利查询
class ArbitrageIndex {
 private:
  // 1. 交易对→当前汇率(键:有向交易对,值:1单位from兑换to的数量)
  std::unordered_map<TradingPair, double> rate_map;
  // 2. 所有回环对象(管理内存,避免野指针)
  std::vector<Loop> all_loops;
  // 3. 交易对→依赖该交易对的回环指针(更新交易对时快速定位回环)
  std::unordered_map<TradingPair, std::vector<Loop*>> edge_to_loops;
  // 4. 盈利有序集合(按盈利倍数降序,支持O(1)查最大、O(logn)增删)
  using ProfitEntry = std::pair<double, Loop*>;
  std::multiset<ProfitEntry, std::greater<ProfitEntry>> profit_set;

  const double EPS = 1e-8;

  // 计算单个回环的当前盈利倍数(基于最新汇率)
  double calculateLoopProfit(const Loop& loop) const {
    double product = 1.0;
    for (const auto& pair : loop.edges) {
      // 若交易对未定义,返回0(标记为无效回环)
      auto it = rate_map.find(pair);
      if (it == rate_map.end()) return 0.0;
      product *= it->second;  // 汇率乘积(如A→B→C→A:rate1*rate2*rate3)
    }
    return product;
  }

  // 从profit_set中删除回环的旧盈利记录(匹配盈利倍数+回环指针)
  void removeFromProfitSet(Loop* loop) {
    auto it = profit_set.begin();
    while (it != profit_set.end()) {
      if (std::abs(it->first - loop->profit_multiplier) < EPS &&
          it->second == loop) {
        profit_set.erase(it);
        return;
      }
      ++it;
    }
  }

 public:
  ~ArbitrageIndex() = default;

  // 添加/更新有向交易对:a单位from兑换b单位to → 1单位from换b/a单位to
  void updateTradingPair(AssetId from, AssetId to, int a, int b) {
    TradingPair pair = {from, to};
    double new_rate = static_cast<double>(b) / a;

    // 情况1:首次添加交易对(仅更新汇率表,无回环依赖)
    if (rate_map.find(pair) == rate_map.end()) {
      rate_map[pair] = new_rate;
      std::cout << "[更新交易对] " << pairToStr(pair) << ":1→" << new_rate
                << "\n";
      return;
    }

    // 情况2:交易对已存在(更新汇率+联动更新依赖回环)
    rate_map[pair] = new_rate;
    std::cout << "[更新交易对] " << pairToStr(pair) << ":1→" << new_rate
              << "(触发回环盈利更新)\n";

    // 找到依赖该交易对的所有回环,重新计算盈利
    auto loop_it = edge_to_loops.find(pair);
    if (loop_it == edge_to_loops.end()) return;  // 无依赖回环,直接返回

    for (Loop* loop : loop_it->second) {
      removeFromProfitSet(loop);                       // 删除旧盈利记录
      double new_profit = calculateLoopProfit(*loop);  // 计算新盈利
      loop->profit_multiplier = new_profit;            // 更新回环盈利
      profit_set.emplace(new_profit, loop);            // 插入新盈利记录
      std::cout << " - 回环更新:";
      loop->print();
    }
  }

  // 添加新回环:需传入合法路径(首尾相同、长度≥3)
  void addLoop(const std::vector<AssetId>& path) {
    // 验证路径合法性(排除[A,B,A]等无效路径)
    if (path.size() < 3 || path.front() != path.back()) {
      std::cerr << "[错误] 无效回环路径(需首尾相同且长度≥3)\n";
      return;
    }

    // 1. 存储回环对象并获取指针
    all_loops.emplace_back(path);
    Loop* new_loop = &all_loops.back();

    // 2. 计算初始盈利并维护双索引
    double init_profit = calculateLoopProfit(*new_loop);
    new_loop->profit_multiplier = init_profit;

    // 3. 关联交易对与回环(更新edge_to_loops)
    for (const auto& pair : new_loop->edges) {
      edge_to_loops[pair].push_back(new_loop);
    }

    // 4. 加入盈利有序集合
    profit_set.emplace(init_profit, new_loop);

    std::cout << "[添加回环成功]:\n";
    new_loop->print();
  }

  // 查询当前最大盈利回环(返回nullptr表示无有效盈利回环)
  const Loop* getMaxProfitLoop() const {
    if (profit_set.empty()) {
      std::cout << "[查询结果] 无有效回环\n";
      return nullptr;
    }

    // 取盈利最大的记录(profit_set按降序排列,首元素即最大)
    const auto& max_entry = *profit_set.begin();
    if (max_entry.first <= 1.0 + EPS) {
      std::cout << "[查询结果] 无盈利回环(最大盈利倍数:" << max_entry.first
                << " ≤ 1)\n";
      return nullptr;
    }

    std::cout << "[查询结果] 最大盈利回环:\n";
    max_entry.second->print();
    return max_entry.second;
  }

  // 打印所有回环当前状态(辅助调试)
  void printAllLoops() const {
    if (all_loops.empty()) {
      std::cout << "[打印所有回环] 无回环\n";
      return;
    }
    std::cout << "[打印所有回环] 共" << all_loops.size() << "个回环:\n";
    for (const auto& loop : all_loops) {
      loop.print();
      std::cout << "------------------------\n";
    }
  }
};

// 模拟交易对更新、回环添加与最大盈利查询
int main() {
  // 1. 初始化索引对象
  ArbitrageIndex arbitrage_index;
  std::cout << "=== 初始化完成 ===" << "\n\n";

  // 2. 添加3个资产的基础交易对(A=0, B=1, C=2)
  std::cout << "=== 步骤1:添加基础交易对 ===" << "\n";
  arbitrage_index.updateTradingPair(0, 1, 1, 10);      // A→B:1A换10B
  arbitrage_index.updateTradingPair(1, 2, 1, 100);     // B→C:1B换100C
  arbitrage_index.updateTradingPair(2, 0, 1, 0.0012);  // C→A:1C换0.0012A
  std::cout << "\n";

  // 3. 添加2个核心回环(A→B→C→A 和 A→C→B→A)
  std::cout << "=== 步骤2:添加回环 ===" << "\n";
  arbitrage_index.addLoop({0, 1, 2, 0});  // 回环1:A→B→C→A
  arbitrage_index.addLoop({0, 2, 1, 0});  // 回环2:A→C→B→A
  std::cout << "\n";

  // 4. 查询初始最大盈利回环
  std::cout << "=== 步骤3:查询初始最大盈利回环 ===" << "\n";
  arbitrage_index.getMaxProfitLoop();
  std::cout << "\n";

  // 5. 更新交易对汇率(模拟市场波动,创造套利)
  std::cout << "=== 步骤4:更新C→A汇率(1C换0.0013A) ===" << "\n";
  arbitrage_index.updateTradingPair(2, 0, 1, 0.0013);
  std::cout << "\n";

  // 6. 再次查询最大盈利回环(此时回环1应盈利)
  std::cout << "=== 步骤5:查询波动后的最大盈利回环 ===" << "\n";
  arbitrage_index.getMaxProfitLoop();
  std::cout << "\n";

  // 7. 打印所有回环状态
  std::cout << "=== 步骤6:打印所有回环当前状态 ===" << "\n";
  arbitrage_index.printAllLoops();

  return 0;
}

// 支持更多资产:只需扩展AssetId(如 3=D、4=E),无需修改核心逻辑。
// 支持更长回环:添加回环时只需传入更长的合法路径(如
// [A,B,C,D,A]),索引会自动适配。
// 性能优化:若资产和回环数量极大,profit_set使用boost::container::flat_set提升缓存效率,
// 或对edge_to_loops使用哈希表分段存储。

问题5:冲击成本下的最大盈利算法

冲击成本导致收益率随投入量递增而递减,但绝对收益可能呈单峰分布(先增后减)。通过三分法在投入区间[0,1000]内寻找最优投入量:

  1. 冲击成本建模:定义函数f(X→Y, x)表示投入x单位X可获得的Y数量(边际递减);
  2. 绝对收益函数:对回环X1→X2→...→Xk→X1,投入a单位X1的绝对收益为:
R(a) = f(Xk→X1, f(Xk-1→Xk, ...f(X1→X2, a)...)) - a
  1. 三分法优化:由于R(a)为单峰函数,通过迭代缩小区间找到最大值点。
// 冲击成本函数(基础汇率*投入量/(1+冲击系数*投入量))
double冲击成本(Asset from, Asset to, double x, double base_rate) {
  const double k = 0.001;  // 冲击系数
  return base_rate * x / (1 + k * x);
}

// 计算回环绝对收益
double calcProfit(const Loop& loop, double a,
                  const std::vector<double>& base_rates) {
  double current = a;
  for (size_t i = 0; i < loop.path.size() - 1; ++i) {
    current = 冲击成本(loop.path[i], loop.path[i + 1], current, base_rates[i]);
  }
  return current - a;
}

// 三分法寻找最大盈利
double findMaxProfit(const Loop& loop, const std::vector<double>& base_rates) {
  double left = 0.0, right = 1000.0;
  for (int i = 0; i < 100; ++i) {  // 迭代精度控制
    if (right - left < 1e-6) break;
    double m1 = left + (right - left) / 3;
    double m2 = right - (right - left) / 3;
    double p1 = calcProfit(loop, m1, base_rates);
    double p2 = calcProfit(loop, m2, base_rates);
    if (p1 < p2)
      left = m1;
    else
      right = m2;
  }
  return calcProfit(loop, (left + right) / 2, base_rates);
}