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相同的结果。在这种情况下,实际上可能并非如此,您需要每个受支持的排列。
我不知道"双重货币"是什么意思……我只会忽略它。
尝试:
看起来很有前途! :)
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 /浮点运算。 .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优雅解决的问题。