@@ -5,6 +5,7 @@ open System.Net
5
5
6
6
open FSharp.Data
7
7
open Fsdk
8
+ open FSharpx.Collections
8
9
9
10
open GWallet.Backend .FSharpUtil .UwpHacks
10
11
@@ -31,9 +32,45 @@ module FiatValueEstimation =
31
32
}
32
33
""" >
33
34
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
+
34
70
type PriceProvider =
35
71
| CoinCap
36
72
| CoinGecko
73
+ | CoinDesk
37
74
38
75
let private QueryOnlineInternal currency ( provider : PriceProvider ): Async < Option < string * string >> = async {
39
76
use webClient = new WebClient()
@@ -48,13 +85,18 @@ module FiatValueEstimation =
48
85
| Currency.ETC,_ -> " ethereum-classic"
49
86
| Currency.DAI, PriceProvider.CoinCap -> " multi-collateral-dai"
50
87
| Currency.DAI,_ -> " dai"
88
+
51
89
try
52
90
let baseUrl =
53
91
match provider with
54
92
| PriceProvider.CoinCap ->
55
93
SPrintF1 " https://api.coincap.io/v2/assets/%s " tickerName
56
94
| PriceProvider.CoinGecko ->
57
95
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"
58
100
let uri = Uri baseUrl
59
101
let task = webClient.DownloadStringTaskAsync uri
60
102
let! res = Async.AwaitTask task
@@ -80,6 +122,20 @@ module FiatValueEstimation =
80
122
return raise ( Exception( SPrintF2 " Could not parse CoinCap's JSON (for %A ): %s " currency json, ex))
81
123
}
82
124
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
+
83
139
let private QueryCoinGecko currency = async {
84
140
let! maybeJson = QueryOnlineInternal currency PriceProvider.CoinGecko
85
141
match maybeJson with
@@ -97,39 +153,68 @@ module FiatValueEstimation =
97
153
return Some usdPrice
98
154
}
99
155
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
125
169
#if DEBUG
126
- failwith err
170
+ failwith err
127
171
#else
128
- Infrastructure.ReportError err
129
- |> ignore< bool>
172
+ Infrastructure.ReportError err
173
+ |> ignore< bool>
130
174
#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
133
218
134
219
let realResult =
135
220
match result with
0 commit comments