如何最好地使用C#/ LINQ计算衍生货币汇率转换?

How best to calculate derived currency rate conversions using C#/LINQ?

1
2
3
4
5
6
7
8
9
10
11
12
13
 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}
          // ...
 };

给出一个庞大但不完整的汇率列表,其中所有货币至少出现一次(作为目标货币或基础货币):我将使用哪种算法来得出未直接列出的汇率的汇率?

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

1
2
3
4
 public double Rate(string baseCode, string targetCode, double currency)
 {
      return ...
 }

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

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


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);
        }
    }
}

现在使用递归遍历该图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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))


仅列出所有转换为单一货币的列表,然后将其用于任何转换会更简单吗?像这样(以美元为基础货币):

1
2
3
4
5
6
7
8
9
10
11
12
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相同的结果。在这种情况下,实际上可能并非如此,您需要每个受支持的排列。


我不知道"双重货币"是什么意思……我只会忽略它。

尝试:List<List<FxRate>> res = Rates("EUR","CHF");产生{EUR-USD, USD-CHF}
看起来很有前途! :)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
    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 => r.Sum(convertion => convertion.Rate));

获得更便宜的转换


有趣的问题!

首先,请避免使用double /浮点运算。 .NET Decimal类型应该足够了,并且可以提供更好的精度!鉴于得出的Fx速率的计算需要一系列操作的事实,这种提高的精度可能尤其重要。

另一句话是,引入一个更简单/更简短的汇率列表可能是不被允许的,因此目标始终是相同的[真实或虚拟]货币。我在这里假设我们应该使用列出的速率(如果可用)。

因此,找出派生费率应成为[简化的]网络解决方案,从而

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);
        }
    }
}

此实用程序功能将生成单独的交叉速率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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;
}

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

另一方面,如果您知道从任何一种货币到任何其他货币都有一条路径,并且列表中的任何两种货币之间只有一种可能的转换(即,如果存在USD> EUR和USD> CHF ,则EUR> CHF不存在,或者您可以忽略它),您可以简单地生成诸如双向链表和遍历之类的内容。同样,这不是可以通过LINQ优雅解决的问题。