Skip to content

Commit 3114fc3

Browse files
committed
Backend/FiatValueEstimation: add CoinDesk API
To increase reliability & accuracy for the BTCUSD exchange rate, let's add a third provider, and rework the code to work with a seq<> of results instead of just two. The place where I found this API is: https://mixedanalytics.com/knowledge-base/top-free-crypto-apis/
1 parent 0e210fe commit 3114fc3

10 files changed

+134
-30
lines changed

src/GWallet.Backend/FiatValueEstimation.fs

Lines changed: 115 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ open System.Net
55

66
open FSharp.Data
77
open Fsdk
8+
open FSharpx.Collections
89

910
open GWallet.Backend.FSharpUtil.UwpHacks
1011

@@ -31,9 +32,45 @@ module FiatValueEstimation =
3132
}
3233
""">
3334

35+
type CoindDeskProvider = JsonProvider<"""
36+
{
37+
"time": {
38+
"updated": "Feb 25, 2024 12:27:26 UTC",
39+
"updatedISO": "2024-02-25T12:27:26+00:00",
40+
"updateduk": "Feb 25, 2024 at 12:27 GMT"
41+
},
42+
"disclaimer":"This data was produced from the CoinDesk Bitcoin Price Index (USD). Non-USD currency data converted using hourly conversion rate from openexchangerates.org",
43+
"chartName":"Bitcoin",
44+
"bpi": {
45+
"USD": {
46+
"code": "USD",
47+
"symbol": "&#36;",
48+
"rate": "51,636.062",
49+
"description": "United States Dollar",
50+
"rate_float": 51636.0621
51+
},
52+
"GBP": {
53+
"code": "GBP",
54+
"symbol": "&pound;",
55+
"rate": "40,725.672",
56+
"description": "British Pound Sterling",
57+
"rate_float": 40725.672
58+
},
59+
"EUR": {
60+
"code":"EUR",
61+
"symbol": "&euro;",
62+
"rate":"47,654.25",
63+
"description": "Euro",
64+
"rate_float": 47654.2504
65+
}
66+
}
67+
}
68+
""">
69+
3470
type PriceProvider =
3571
| CoinCap
3672
| CoinGecko
73+
| CoinDesk
3774

3875
let private QueryOnlineInternal currency (provider: PriceProvider): Async<Option<string*string>> = async {
3976
use webClient = new WebClient()
@@ -48,13 +85,18 @@ module FiatValueEstimation =
4885
| Currency.ETC,_ -> "ethereum-classic"
4986
| Currency.DAI,PriceProvider.CoinCap -> "multi-collateral-dai"
5087
| Currency.DAI,_ -> "dai"
88+
5189
try
5290
let baseUrl =
5391
match provider with
5492
| PriceProvider.CoinCap ->
5593
SPrintF1 "https://api.coincap.io/v2/assets/%s" tickerName
5694
| PriceProvider.CoinGecko ->
5795
SPrintF1 "https://api.coingecko.com/api/v3/simple/price?ids=%s&vs_currencies=usd" tickerName
96+
| PriceProvider.CoinDesk ->
97+
if currency <> Currency.BTC then
98+
failwith "CoinDesk API only provides bitcoin price"
99+
"https://api.coindesk.com/v1/bpi/currentprice.json"
58100
let uri = Uri baseUrl
59101
let task = webClient.DownloadStringTaskAsync uri
60102
let! res = Async.AwaitTask task
@@ -80,6 +122,20 @@ module FiatValueEstimation =
80122
return raise (Exception(SPrintF2 "Could not parse CoinCap's JSON (for %A): %s" currency json, ex))
81123
}
82124

125+
let private QueryCoinDesk(): Async<Option<decimal>> =
126+
async {
127+
let! maybeJson = QueryOnlineInternal Currency.BTC PriceProvider.CoinDesk
128+
match maybeJson with
129+
| None -> return None
130+
| Some (_, json) ->
131+
try
132+
let tickerObj = CoindDeskProvider.Parse json
133+
return Some tickerObj.Bpi.Usd.RateFloat
134+
with
135+
| ex ->
136+
return raise <| Exception (SPrintF1 "Could not parse CoinDesk's JSON: %s" json, ex)
137+
}
138+
83139
let private QueryCoinGecko currency = async {
84140
let! maybeJson = QueryOnlineInternal currency PriceProvider.CoinGecko
85141
match maybeJson with
@@ -97,39 +153,68 @@ module FiatValueEstimation =
97153
return Some usdPrice
98154
}
99155

100-
let private RetrieveOnline currency = async {
101-
let coinGeckoJob = QueryCoinGecko currency
102-
let coinCapJob = QueryCoinCap currency
103-
let bothJobs = FSharpUtil.AsyncExtensions.MixedParallel2 coinGeckoJob coinCapJob
104-
let! maybeUsdPriceFromCoinGecko, maybeUsdPriceFromCoinCap = bothJobs
105-
let result =
106-
match maybeUsdPriceFromCoinGecko, maybeUsdPriceFromCoinCap with
107-
| None, None -> None
108-
| Some usdPriceFromCoinGecko, None ->
109-
Some usdPriceFromCoinGecko
110-
| None, Some usdPriceFromCoinCap ->
111-
Some usdPriceFromCoinCap
112-
| Some usdPriceFromCoinGecko, Some usdPriceFromCoinCap ->
113-
let higher = Math.Max(usdPriceFromCoinGecko, usdPriceFromCoinCap)
114-
let lower = Math.Min(usdPriceFromCoinGecko, usdPriceFromCoinCap)
115-
116-
// example: 100USD vs: 66.666USD (or lower)
117-
let abnormalDifferenceRate = 1.5m
118-
if (higher / lower) > abnormalDifferenceRate then
119-
let err =
120-
SPrintF4 "Alert: difference of USD exchange rate (for %A) between the providers is abnormally high: %M vs %M (H/L > %M)"
121-
currency
122-
usdPriceFromCoinGecko
123-
usdPriceFromCoinCap
124-
abnormalDifferenceRate
156+
let MaybeReportAbnormalDifference (result1: decimal) (result2: decimal) currency =
157+
let higher = Math.Max(result1, result2)
158+
let lower = Math.Min(result1, result2)
159+
160+
// example: 100USD vs: 66.666USD (or lower)
161+
let abnormalDifferenceRate = 1.5m
162+
if (higher / lower) > abnormalDifferenceRate then
163+
let err =
164+
SPrintF4 "Alert: difference of USD exchange rate (for %A) between the providers is abnormally high: %M vs %M (H/L > %M)"
165+
currency
166+
result1
167+
result2
168+
abnormalDifferenceRate
125169
#if DEBUG
126-
failwith err
170+
failwith err
127171
#else
128-
Infrastructure.ReportError err
129-
|> ignore<bool>
172+
Infrastructure.ReportError err
173+
|> ignore<bool>
130174
#endif
131-
let average = (usdPriceFromCoinGecko + usdPriceFromCoinCap) / 2m
132-
Some average
175+
176+
let private Average (results: seq<Option<decimal>>) currency =
177+
let rec averageInternal (nextResults: seq<Option<decimal>>) (resultSoFar: Option<decimal>) (resultCountSoFar: uint32) =
178+
match Seq.tryHeadTail nextResults with
179+
| None ->
180+
match resultSoFar with
181+
| None ->
182+
None
183+
| Some res ->
184+
(res / (decimal (int resultCountSoFar))) |> Some
185+
| Some(head, tail) ->
186+
match head with
187+
| None ->
188+
averageInternal tail resultSoFar resultCountSoFar
189+
| Some res ->
190+
match resultSoFar with
191+
| None ->
192+
if resultCountSoFar <> 0u then
193+
failwith <| SPrintF1 "Got resultSoFar==None but resultCountSoFar>0u: %i" (int resultCountSoFar)
194+
averageInternal tail (Some res) 1u
195+
| Some prevRes ->
196+
let averageSoFar = prevRes / (decimal (int resultCountSoFar))
197+
198+
MaybeReportAbnormalDifference averageSoFar res currency
199+
200+
averageInternal tail (Some (prevRes + res)) (resultCountSoFar + 1u)
201+
averageInternal results None 0u
202+
203+
let private RetrieveOnline currency = async {
204+
let coinGeckoJob = QueryCoinGecko currency
205+
let coinCapJob = QueryCoinCap currency
206+
207+
let multiCurrencyJobs = [ coinGeckoJob; coinCapJob ]
208+
let allJobs =
209+
match currency with
210+
| Currency.BTC ->
211+
let coinDeskJob = QueryCoinDesk()
212+
coinDeskJob :: multiCurrencyJobs
213+
| _ ->
214+
multiCurrencyJobs
215+
216+
let! allResults = Async.Parallel allJobs
217+
let result = Average allResults currency
133218

134219
let realResult =
135220
match result with

src/GWallet.Backend/GWallet.Backend-legacy.fsproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,9 @@
307307
<Reference Include="Xamarin.Essentials">
308308
<HintPath>..\..\packages\DotNetEssentials.1.6.1--date20220823-0234.git-14ad2d3\lib\netstandard2.0\Xamarin.Essentials.dll</HintPath>
309309
</Reference>
310+
<Reference Include="FSharpx.Collections">
311+
<HintPath>..\..\packages\FSharpx.Collections.3.1.0\lib\netstandard2.0\FSharpx.Collections.dll</HintPath>
312+
</Reference>
310313
</ItemGroup>
311314
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
312315
Other similar extension points exist, see Microsoft.Common.targets.

src/GWallet.Backend/GWallet.Backend.fsproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,5 +80,8 @@
8080
<PackageReference Include="HtmlAgilityPack" Version="1.11.24">
8181
<GeneratePathProperty></GeneratePathProperty>
8282
</PackageReference>
83+
<PackageReference Include="FSharpx.Collections" Version="3.1.0">
84+
<GeneratePathProperty></GeneratePathProperty>
85+
</PackageReference>
8386
</ItemGroup>
8487
</Project>

src/GWallet.Backend/packages.config

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<package id="DotNetEssentials" version="1.6.1--date20220823-0234.git-14ad2d3" targetFramework="net471" />
66
<package id="FSharp.Core" version="4.7.0" targetFramework="net461" />
77
<package id="FSharp.Data" version="3.0.0" targetFramework="net46" />
8+
<package id="FSharpx.Collections" version="3.1.0" targetFramework="net471" />
89
<package id="HtmlAgilityPack" version="1.11.24" targetFramework="net471" />
910
<package id="JsonRpcSharp" version="0.98.0--date20230731-1252.git-6788e32" targetFramework="net461" />
1011
<package id="Microsoft.CSharp" version="4.3.0" targetFramework="net471" />

src/GWallet.Frontend.XF.Android/GWallet.Frontend.XF.Android.fsproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,9 @@
255255
<Reference Include="Fsdk">
256256
<HintPath>..\..\packages\Fsdk.0.6.0--date20230530-1155.git-3bb8d08\lib\netstandard2.0\Fsdk.dll</HintPath>
257257
</Reference>
258+
<Reference Include="FSharpx.Collections">
259+
<HintPath>..\..\packages\FSharpx.Collections.3.1.0\lib\netstandard2.0\FSharpx.Collections.dll</HintPath>
260+
</Reference>
258261
</ItemGroup>
259262
<ItemGroup>
260263
<Reference Include="BouncyCastle.Crypto">

src/GWallet.Frontend.XF.Android/packages.config

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
<package id="Fsdk" version="0.6.0--date20230530-1155.git-3bb8d08" targetFramework="monoandroid11.0" />
77
<package id="FSharp.Core" version="4.7.0" targetFramework="monoandroid90" />
88
<package id="FSharp.Data" version="3.0.0" targetFramework="monoandroid90" />
9+
<package id="FSharpx.Collections" version="3.1.0" targetFramework="monoandroid11.0" />
910
<package id="HtmlAgilityPack" version="1.11.24" targetFramework="monoandroid90" />
1011
<package id="JsonRpcSharp" version="0.98.0--date20230731-1252.git-6788e32" targetFramework="monoandroid90" />
1112
<package id="Microsoft.CSharp" version="4.3.0" targetFramework="monoandroid90" />

src/GWallet.Frontend.XF.Gtk/GWallet.Frontend.XF.Gtk.fsproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,9 @@
230230
<Reference Condition="'$(TwoPhaseBuildDueToXBuildUsage)'=='true'" Include="GWallet.Frontend.XF">
231231
<HintPath>..\GWallet.Frontend.XF\bin\$(Configuration)\netstandard2.0\GWallet.Frontend.XF.dll</HintPath>
232232
</Reference>
233+
<Reference Include="FSharpx.Collections">
234+
<HintPath>..\..\packages\FSharpx.Collections.3.1.0\lib\netstandard2.0\FSharpx.Collections.dll</HintPath>
235+
</Reference>
233236
</ItemGroup>
234237
<ItemGroup>
235238
<Folder Include="Properties\" />

src/GWallet.Frontend.XF.Gtk/packages.config

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
<package id="Fsdk" version="0.6.0--date20230530-1155.git-3bb8d08" targetFramework="net471" />
77
<package id="FSharp.Core" version="4.7.0" targetFramework="net461" />
88
<package id="FSharp.Data" version="3.0.0" targetFramework="net461" />
9+
<package id="FSharpx.Collections" version="3.1.0" targetFramework="net471" />
910
<package id="HtmlAgilityPack" version="1.11.24" targetFramework="net471" />
1011
<package id="JsonRpcSharp" version="0.98.0--date20230731-1252.git-6788e32" targetFramework="net471" />
1112
<package id="Microsoft.CSharp" version="4.3.0" targetFramework="net471" />

src/GWallet.Frontend.XF.iOS/GWallet.Frontend.XF.iOS.fsproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,9 @@
293293
<Reference Include="Fsdk">
294294
<HintPath>..\..\packages\Fsdk.0.6.0--date20230530-1155.git-3bb8d08\lib\netstandard2.0\Fsdk.dll</HintPath>
295295
</Reference>
296+
<Reference Include="FSharpx.Collections">
297+
<HintPath>..\packages\FSharpx.Collections.3.1.0\lib\netstandard2.0\FSharpx.Collections.dll</HintPath>
298+
</Reference>
296299
</ItemGroup>
297300
<Import Project="..\..\packages\NETStandard.Library.2.0.3\build\netstandard2.0\NETStandard.Library.targets" Condition="Exists('..\..\packages\NETStandard.Library.2.0.3\build\netstandard2.0\NETStandard.Library.targets')" />
298301
<Import Project="..\..\packages\Xamarin.Forms.5.0.0.2515\build\Xamarin.Forms.targets" Condition="Exists('..\..\packages\Xamarin.Forms.5.0.0.2515\build\Xamarin.Forms.targets')" />

src/GWallet.Frontend.XF.iOS/packages.config

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<package id="Fsdk" version="0.6.0--date20230530-1155.git-3bb8d08" targetFramework="xamarinios10" />
66
<package id="FSharp.Core" version="4.7.0" targetFramework="xamarinios10" />
77
<package id="FSharp.Data" version="3.0.0" targetFramework="xamarinios10" />
8+
<package id="FSharpx.Collections" version="3.1.0" targetFramework="xamarinios10" />
89
<package id="HtmlAgilityPack" version="1.11.24" targetFramework="xamarinios10" />
910
<package id="JsonRpcSharp" version="0.98.0--date20230731-1252.git-6788e32" targetFramework="xamarinios10" />
1011
<package id="Microsoft.CSharp" version="4.3.0" targetFramework="xamarinios10" />

0 commit comments

Comments
 (0)