@@ -5,6 +5,7 @@ open System.Net
55
66open FSharp.Data
77open Fsdk
8+ open FSharpx.Collections
89
910open 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": "$",
48+ "rate": "51,636.062",
49+ "description": "United States Dollar",
50+ "rate_float": 51636.0621
51+ },
52+ "GBP": {
53+ "code": "GBP",
54+ "symbol": "£",
55+ "rate": "40,725.672",
56+ "description": "British Pound Sterling",
57+ "rate_float": 40725.672
58+ },
59+ "EUR": {
60+ "code":"EUR",
61+ "symbol": "€",
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.5 m
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.5 m
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) / 2 m
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 <> 0 u then
193+ failwith <| SPrintF1 " Got resultSoFar==None but resultCountSoFar>0u: %i " ( int resultCountSoFar)
194+ averageInternal tail ( Some res) 1 u
195+ | Some prevRes ->
196+ let averageSoFar = prevRes / ( decimal ( int resultCountSoFar))
197+
198+ MaybeReportAbnormalDifference averageSoFar res currency
199+
200+ averageInternal tail ( Some ( prevRes + res)) ( resultCountSoFar + 1 u)
201+ averageInternal results None 0 u
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
0 commit comments