【问题标题】:How best to calculate derived currency rate conversions using C#/LINQ?如何最好地使用 C#/LINQ 计算派生货币汇率转换?
【发布时间】:2011-01-26 13:04:51
【问题描述】:
 class FxRate {
     string Base { get; set; }
     string Target  { get; set; }
     double Rate { get; set; }
 }

 private IList<FxRate> rates = new List<FxRate> {
          new FxRate {Base = "EUR", Target = "USD", Rate = 1.3668},
          new FxRate {Base = "GBP", Target = "USD", Rate = 1.5039},
          new FxRate {Base = "USD", Target = "CHF", Rate = 1.0694},
          new FxRate {Base = "CHF", Target = "SEK", Rate = 8.12}
          // ...
 };

鉴于所有货币至少出现一次(作为目标货币或基础货币)的大量但不完整的汇率列表:我将使用什么算法来得出未直接列出的交易所的汇率?

我正在寻找以下形式的通用算法:

 public double Rate(string baseCode, string targetCode, double currency)
 {
      return ...
 }

在上面的示例中,派生汇率将是 GBP->CHF 或 EUR->SEK(这需要使用 EUR->USD、USD->CHF、CHF->SEK 的转换)

虽然我知道如何手动进行转换,但我正在寻找一种简洁的方法(可能使用 LINQ)来执行这些派生转换,可能涉及多个货币跃点,但最好的方法是什么?

【问题讨论】:

  • Base -&gt; Target 转换是否意味着Target -&gt; Base1 / Rate 的转换?

标签: c# linq currency


【解决方案1】:

有趣的问题!

首先,远离双精度/浮点运算。 .NET Decimal type 应该足够了,并且可以提供更好的精度!考虑到导出 Fx 汇率的计算需要一系列操作,这种改进的精度可能特别重要。

另一句话是,引入更简单/更短的汇率列表可能是禁止的,因为目标始终是相同的 [真实或虚构] 货币。我在这里假设我们应该在可用时使用列出的费率。

所以计算派生费率应该成为一种[简化的]网络解决方案,其中

  • 给定基础货币和目标货币,我们确定所有最短路径(从基础到目标),给定列表中的权威(非衍生)汇率。 (我们可以希望最短路径在所有情况下都是 2,但考虑到非常深奥的货币,情况可能并非如此)。
  • 对于这些最短路径中的每一个(我认为同时考虑更长的路径会很可笑),我们执行简单的算术转换,然后...
  • 希望确认这些导出的汇率都在名义上的转换误差范围内,因此取这些汇率的平均值
  • 提高警惕...或者只是通过制作圆形路径并在差速器中倾斜来赚很多钱;-)

【讨论】:

    【解决方案2】:

    最直接的算法可能就像Dijkstra 的最短路径或您使用该列表生成的图表上的东西。由于您事先不知道路径将有多长,因此这并不是一个可以通过 LINQ 查询优雅地解决的问题。 (并不是说不可能,只是可能不是你应该追求的。)

    另一方面,如果您知道从任何一种货币到任何另一种货币都有一条路径,并且列表中的任何两种货币之间只有一种可能的转换(即,如果存在美元 > 欧元和美元 > 瑞士法郎, 那么 EUR > CHF 不存在或者你可以忽略它),你可以简单地生成类似双向链表的东西并遍历。不过,这又不是可以通过 LINQ 优雅地解决的问题。

    【讨论】:

      【解决方案3】:

      将所有转换为单一货币的列表然后用于任何转换不是更简单吗?类似于(以美元为基础货币):

      var conversionsToUSD = new Dictionary<string, decimal>();
      
      public decimal Rate ( string baseCode, string targetCode )
      {
          if ( targetCode == "USD" )
              return conversionsToUSD[baseCode];
      
          if ( baseCode == "USD" )
              return 1 / conversionsToUSD[targetCode];
      
          return conversionsToUSD[baseCode] / conversionsToUSD[targetCode]
      }
      

      现在,这假设代数是完全可以交流的。即,如果我转换为 EUR->USD->GBP,我将得到与从 EUR->GBP 转换相同的结果。实际情况可能并非如此,在这种情况下,您需要每个支持的排列。

      【讨论】:

      • 这并不能完全回答如何以编程方式将他的任意转换列表转换为美元转换列表的问题,但如果他可以强制输入全部相关,这可能是最好的解决方案换成一种货币,所以+1。 :)
      【解决方案4】:

      我不知道那个“双重货币”是干什么用的……我就忽略它。

      尝试:List&lt;List&lt;FxRate&gt;&gt; res = Rates("EUR", "CHF"); 产生 {EUR-USD, USD-CHF}
      看起来很有希望! :)

          public class FxRate
          {
              public string Base { get; set; }
              public string Target { get; set; }
              public double Rate { get; set; }
          }
      
          private List<FxRate> rates = new List<FxRate>
                                          {
                                              new FxRate {Base = "EUR", Target = "USD", Rate = 1.3668},
                                              new FxRate {Base = "GBP", Target = "USD", Rate = 1.5039},
                                              new FxRate {Base = "USD", Target = "CHF", Rate = 1.0694},
                                              new FxRate {Base = "CHF", Target = "SEK", Rate = 8.12}
                                              // ...
                                          };
      
          public List<List<FxRate>> Rates(string baseCode, string targetCode)
          {
              return Rates(baseCode, targetCode, rates.ToArray());
          }
          public List<List<FxRate>> Rates(string baseCode, string targetCode, FxRate[] toSee)
          {
              List<List<FxRate>> results = new List<List<FxRate>>();
      
              List<FxRate> possible = toSee.Where(r => r.Base == baseCode).ToList();
              List<FxRate> hits = possible.Where(p => p.Target == targetCode).ToList();
              if (hits.Count > 0)
              {
                  possible.RemoveAll(hits.Contains);
                  results.AddRange(hits.Select(hit => new List<FxRate> { hit }));
              }
      
              FxRate[] newToSee = toSee.Where( item => !possible.Contains(item)).ToArray();
              foreach (FxRate posRate in possible)
              {
                  List<List<FxRate>> otherConversions = Rates(posRate.Target, targetCode, newToSee);
                  FxRate rate = posRate;
                  otherConversions.ForEach(result => result.Insert(0, rate));
                  results.AddRange(otherConversions);
              }
              return results;
          }
      

      评论?

      PS:你可以通过double minConvertion = res.Min(r =&gt; r.Sum(convertion =&gt; convertion.Rate));获得更便宜的转换

      【讨论】:

      • 等等,这不是r.Sum(conv =&gt; conv.Rate)... 你需要乘以 - 我的错。但是转换路线代码确实有效。 :) 然后您可以使用 foreach (或扩展方法 double GetRate(this List&lt;FxRate&gt; route){...} )来获取每条路线的价值,并选择更便宜的路线。 (尽管如您所说,如果转换是 a->b = 1/(b->a),那么对于给定的开始和结束货币对,所有路线都应该具有相同的成本。)
      • 此外,此解决方案没有考虑到您可以从 EUR->US 导出 US->EUR,因为最初这不是问题所在。我真的没有时间不解决这个问题,并进行测试以确保它有效 - 但您可以通过在列表中插入两个转换(A-> B 和 B-> A)来解决这个问题......“限制”费率。
      【解决方案5】:

      首先构建所有货币的图表:

      private Dictionary<string, List<string>> _graph
      public void ConstructGraph()
      {
          if (_graph == null) {
              _graph = new Dictionary<string, List<string>>();
              foreach (var rate in rates) {
                  if (!_graph.ContainsKey(rate.Base))
                      _graph[rate.Base] = new List<string>();
                  if (!_graph.ContainsKey(rate.Target))
                      _graph[rate.Target] = new List<string>();
      
                  _graph[rate.Base].Add(rate.Target);
                  _graph[rate.Target].Add(rate.Base);
              }
          }
      }
      

      现在使用递归遍历该图:

      public double Rate(string baseCode, string targetCode)
      {
          if (_graph[baseCode].Contains(targetCode)) {
              // found the target code
              return GetKnownRate(baseCode, targetCode);
          }
          else {
              foreach (var code in _graph[baseCode]) {
                  // determine if code can be converted to targetCode
                  double rate = Rate(code, targetCode);
                  if (rate != 0) // if it can than combine with returned rate
                      return rate * GetKnownRate(baseCode, code);
              }
          }
      
          return 0; // baseCode cannot be converted to the targetCode
      }
      public double GetKnownRate(string baseCode, string targetCode) 
      {
          var rate = rates.SingleOrDefault(fr => fr.Base == baseCode && fr.Target == targetCode);
          var rate_i rates.SingleOrDefault(fr => fr.Base == targetCode && fr.Target == baseCode));
          if (rate == null)
              return 1 / rate_i.Rate
          return rate.Rate;
      }
      

      免责声明:这是未经测试的。此外,我确信这不是解决问题的最有效方法(我认为是 O(n)),但我相信它会起作用。您可以添加许多东西来提高性能(例如,保存每个新的组合速率计算最终会将其变成有效的 O(1))

      【讨论】:

        【解决方案6】:

        全部生成并缓存它们。给定初始集合,此函数将通过在迭代时简单扩展初始列表来生成所有现有的对(在同一列表内)而无需图形或递归。

        public static void CrossRates(List<FxRate> rates)
        {
            for (int i = 0; i < rates.Count; i++)
            {
                FxRate rate = rates[i];
                for (int j = i + 1; j < rates.Count; j++)
                {
                    FxRate rate2 = rates[j];
                    FxRate cross = CanCross(rate, rate2);
                    if (cross != null)
                        if (rates.FirstOrDefault(r => r.Ccy1.Equals(cross.Ccy1) && r.Ccy2.Equals(cross.Ccy2)) == null)
                            rates.Add(cross);
                }
            }
        }
        

        此效用函数将生成单独的交叉率。

        public static FxRate CanCross(FxRate r1, FxRate r2)
        {
            FxRate nr = null;
        
            if (r1.Ccy1.Equals(r2.Ccy1) && r1.Ccy2.Equals(r2.Ccy2) ||
                r1.Ccy1.Equals(r2.Ccy2) && r1.Ccy2.Equals(r2.Ccy1)
                ) return null; // Same with same.
        
            if (r1.Ccy1.Equals(r2.Ccy1))
            { // a/b / a/c = c/b
                nr = new FxRate()
                {
                    Ccy1 = r2.Ccy2,
                    Ccy2 = r1.Ccy2,
                    Rate = r1.Rate / r2.Rate
                };
            }
            else if (r1.Ccy1.Equals(r2.Ccy2))
            {
                // a/b * c/a = c/b
                nr = new FxRate()
                {
                    Ccy1 = r2.Ccy1,
                    Ccy2 = r1.Ccy2,
                    Rate = r2.Rate * r1.Rate
                };
            }
            else if (r1.Ccy2.Equals(r2.Ccy2))
            {
                // a/c / b/c = a/b
                nr = new FxRate()
                {
                    Ccy1 = r1.Ccy1,
                    Ccy2 = r2.Ccy1,
                    Rate = r1.Rate / r2.Rate
                };
            }
            else if (r1.Ccy2.Equals(r2.Ccy1))
            {
                // a/c * c/b = a/b
                nr = new FxRate()
                {
                    Ccy1 = r1.Ccy1,
                    Ccy2 = r2.Ccy2,
                    Rate = r1.Rate * r2.Rate
                };
            }
            return nr;
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2018-03-18
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-12-30
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多