currency-rate 1.7.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/lib/adapter.rb +53 -24
  4. data/lib/adapters/crypto/binance_adapter.rb +1 -1
  5. data/lib/adapters/crypto/bitfinex_adapter.rb +1 -1
  6. data/lib/adapters/crypto/bitpay_adapter.rb +1 -1
  7. data/lib/adapters/crypto/bitstamp_adapter.rb +1 -1
  8. data/lib/adapters/crypto/coin_market_cap_adapter.rb +2 -4
  9. data/lib/adapters/crypto/coinbase_adapter.rb +3 -3
  10. data/lib/adapters/crypto/exmo_adapter.rb +1 -1
  11. data/lib/adapters/crypto/hit_BTC_adapter.rb +64 -0
  12. data/lib/adapters/crypto/huobi_adapter.rb +1 -1
  13. data/lib/adapters/crypto/kraken_adapter.rb +1 -1
  14. data/lib/adapters/crypto/localbitcoins_adapter.rb +1 -1
  15. data/lib/adapters/crypto/okcoin_adapter.rb +4 -6
  16. data/lib/adapters/crypto/paxful_adapter.rb +1 -1
  17. data/lib/adapters/crypto/poloniex_adapter.rb +1 -1
  18. data/lib/adapters/crypto/yadio_adapter.rb +1 -1
  19. data/lib/adapters/fiat/bonbast_adapter.rb +3 -5
  20. data/lib/adapters/fiat/coinmonitor_adapter.rb +4 -5
  21. data/lib/adapters/fiat/currency_layer_adapter.rb +2 -3
  22. data/lib/adapters/fiat/fixer_adapter.rb +3 -4
  23. data/lib/adapters/fiat/forge_adapter.rb +15 -6
  24. data/lib/adapters/fiat/free_forex_adapter.rb +7 -3
  25. data/lib/container.rb +203 -0
  26. data/lib/currency_rate.rb +19 -63
  27. data/lib/currency_rate/version.rb +1 -1
  28. data/lib/storage/file_storage.rb +14 -9
  29. data/lib/utils/string_extensions.rb +19 -0
  30. metadata +6 -11
  31. data/api_keys.yml.sample +0 -4
  32. data/lib/adapters/crypto/btc_china_adapter.rb +0 -11
  33. data/lib/adapters/crypto/btc_e_adapter.rb +0 -18
  34. data/lib/adapters/crypto/hit_btc_adapter.rb +0 -61
  35. data/lib/adapters/fiat/yahoo_adapter.rb +0 -35
  36. data/lib/configuration.rb +0 -28
  37. data/lib/fetcher.rb +0 -80
  38. data/lib/synchronizer.rb +0 -54
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: db91dd121b519bd89adba8d9576c0746792e5a9e40206347284ed4fc9acc3089
4
- data.tar.gz: ae3c3b6b260e4e202d25e602fc215ff7f2f55837a8b0e355da6d8e3c565961b6
3
+ metadata.gz: e33cf57e2389c7faea732bc6a2c1005d1373daa26622d77d07058a157cb408b4
4
+ data.tar.gz: ecdcdf02e7478deb612bebe5fed48c3d46d99916f9f25b4e0666bcf88ed4c1f9
5
5
  SHA512:
6
- metadata.gz: 6dbceacbf49cfb2d1f06174e443748ae6fced938b1ab2504e2a213d437afad3bf0cb822cd08041675e69fb55b9abc7e108c7433ed0c9e19621c9a17ab9f1da0c
7
- data.tar.gz: 7fa1c6d94d9c34f9fd3ce672e044b24ee30ca081bda69bd19354902bd8df902bb6f5e073aac0525714f30d6d01bad1c7bb2cf21c835ebe278d457277e3760588
6
+ metadata.gz: d6d71b129e93052a97d6e27749531c16818e19eeb1f7b44b6889d67683543094eea7c9cf59f7dc6dae01e1e6e0f570c0709c06d3c786b7c7d275f1b4e60c6e65
7
+ data.tar.gz: 21cd15e77e63eb8df45f4b52904b7c9ac3fb0e0a93838a3373c31165af5a7d115c6982b2e8cdf869898ed24dbe07008bcc817b9464ca0510717cf5a92e359844
data/.gitignore CHANGED
@@ -17,4 +17,4 @@ pkg
17
17
  .byebug_history
18
18
 
19
19
  # Private API keys for adapters
20
- api_keys.yml
20
+ spec/api_keys.yml
@@ -4,28 +4,41 @@ module CurrencyRate
4
4
 
5
5
  SUPPORTED_CURRENCIES = []
6
6
  FETCH_URL = nil
7
- API_KEY_PARAM = nil
8
7
 
9
- def name
10
- self.class.name.gsub /^.*::/, ""
8
+ attr_reader :rates
9
+ attr_accessor :container
10
+ attr_accessor :api_key
11
+
12
+ def name(format=:snake_case)
13
+ @camel_case_name ||= self.class.name.gsub(/^.*::/, "").sub("Adapter", "")
14
+ @snake_case_name ||= @camel_case_name.to_snake_case
15
+ format == :camel_case ? @camel_case_name : @snake_case_name
16
+ end
17
+
18
+ def get_rate(from, to)
19
+ @rates = @container.storage.read(self.name) if @rates.nil? && @container.storage.exists?(self.name)
20
+ return BigDecimal(rates[to]) if ANCHOR_CURRENCY == from && rates[to]
21
+ return BigDecimal(1 / rates[from]) if ANCHOR_CURRENCY == to && rates[from]
22
+ return BigDecimal(rates[to] / rates[from]) if rates[from] && rates[to]
23
+ nil
11
24
  end
12
25
 
13
26
  def fetch_rates
14
27
  begin
15
- normalize exchange_data
28
+ @rates = normalize(exchange_data)
16
29
  rescue StandardError => e
17
- CurrencyRate.logger.error("Error in #{self.name}#fetch_rates")
18
- CurrencyRate.logger.error(e)
30
+ @container.log(:error, e)
19
31
  nil
20
32
  end
21
33
  end
22
34
 
23
35
  def normalize(data)
24
- if data.nil?
25
- CurrencyRate.logger.warn("#{self.name}#normalize: data is nil")
36
+ @rates = if data.nil?
37
+ @container.log(:warn, "#{self.name}#normalize: data is nil")
26
38
  return nil
39
+ else
40
+ parse_raw_data(data)
27
41
  end
28
- true
29
42
  end
30
43
 
31
44
  def exchange_data
@@ -37,34 +50,50 @@ module CurrencyRate
37
50
  result[name] = request url
38
51
  end
39
52
  else
40
- request self.class::FETCH_URL
53
+ result = request self.class::FETCH_URL
41
54
  end
42
55
  rescue StandardError => e
43
- CurrencyRate.logger.error("Error in #{self.name}#exchange_data")
44
- CurrencyRate.logger.error(e)
56
+ @container.log(:error, e)
45
57
  nil
46
58
  end
47
59
  end
48
60
 
49
- def request(url)
50
- fetch_url = url
51
- if self.class::API_KEY_PARAM
52
- api_key = CurrencyRate.configuration.api_keys[self.name]
61
+ def url_with_api_key(url)
62
+ if url.include?("__API_KEY__")
53
63
  if api_key.nil?
54
- CurrencyRate.logger.error("API key for #{self.name} not defined")
64
+ @container.log(:error, "API key for #{self.name} is not set")
55
65
  return nil
66
+ else
67
+ url.sub("__API_KEY__", api_key.strip)
56
68
  end
57
- param_symbol = fetch_url.split("/").last.include?("?") ? "&" : "?"
58
- fetch_url << "#{param_symbol}#{self.class::API_KEY_PARAM}=#{api_key}" if api_key
69
+ else
70
+ url
71
+ end
72
+ end
73
+
74
+ def request(url)
75
+ unless url = url_with_api_key(url)
76
+ return nil
59
77
  end
60
- http_client = HTTP.timeout(connect: CurrencyRate.configuration.connect_timeout, read: CurrencyRate.configuration.read_timeout)
61
- JSON.parse(
62
- http_client
78
+ http_client = HTTP.timeout(connect: @container.connect_timeout, read: @container.read_timeout)
79
+ http_client
63
80
  .headers("Accept" => "application/json; version=1")
64
81
  .headers("Content-Type" => "text/plain")
65
- .get(fetch_url)
82
+ .get(url)
66
83
  .to_s
67
- )
84
+ end
85
+
86
+ def parse_raw_data(data)
87
+ if data.kind_of?(Hash)
88
+ data.each { |k,v| data[k] = parse_raw_data(v) }
89
+ data
90
+ else
91
+ begin
92
+ return JSON.parse(data)
93
+ rescue JSON::ParserError
94
+ return data
95
+ end
96
+ end
68
97
  end
69
98
 
70
99
  end
@@ -20,7 +20,7 @@ module CurrencyRate
20
20
  }
21
21
 
22
22
  def normalize(data)
23
- return nil unless super
23
+ return nil unless data = super
24
24
  binance_result = data["Binance"].reduce({ "anchor" => ANCHOR_CURRENCY }) do |result, hash|
25
25
  if hash["symbol"].index(ANCHOR_CURRENCY) == 0
26
26
  result[hash["symbol"].sub(ANCHOR_CURRENCY, "")] = BigDecimal(hash["price"].to_s)
@@ -21,7 +21,7 @@ module CurrencyRate
21
21
  FETCH_URL = "https://api.bitfinex.com/v2/tickers?symbols=ALL"
22
22
 
23
23
  def normalize(data)
24
- return nil unless super
24
+ return nil unless data = super
25
25
  data.reduce({ "anchor" => ANCHOR_CURRENCY }) do |result, pair_info|
26
26
  pair_name = pair_info[0].sub("t", "")
27
27
  key = pair_name.sub(ANCHOR_CURRENCY, "")
@@ -18,7 +18,7 @@ module CurrencyRate
18
18
  FETCH_URL = "https://bitpay.com/api/rates"
19
19
 
20
20
  def normalize(data)
21
- return nil unless super
21
+ return nil unless data = super
22
22
  data.reject { |rate| rate["code"] == ANCHOR_CURRENCY }.reduce({ "anchor" => ANCHOR_CURRENCY }) do |result, rate|
23
23
  result["#{rate['code'].upcase}"] = BigDecimal(rate["rate"].to_s)
24
24
  result
@@ -10,7 +10,7 @@ module CurrencyRate
10
10
  FETCH_URL["USD"] = "https://www.bitstamp.net/api/ticker/"
11
11
 
12
12
  def normalize(data)
13
- return nil unless super
13
+ return nil unless data = super
14
14
  data.reduce({ "anchor" => ANCHOR_CURRENCY }) do |result, (key, value)|
15
15
  if key == "USD"
16
16
  result[key] = BigDecimal(value["last"].to_s)
@@ -12,12 +12,10 @@ module CurrencyRate
12
12
  )
13
13
 
14
14
  ANCHOR_CURRENCY = "BTC"
15
-
16
- FETCH_URL = "https://pro-api.coinmarketcap.com/v1/cryptocurrency/listings/latest"
17
- API_KEY_PARAM = "CMC_PRO_API_KEY"
15
+ FETCH_URL = "https://pro-api.coinmarketcap.com/v1/cryptocurrency/listings/latest?CMC_PRO_API_KEY=__API_KEY__"
18
16
 
19
17
  def normalize(data)
20
- return nil unless super
18
+ return nil unless data = super
21
19
  data["data"].each_with_object({ "anchor" => ANCHOR_CURRENCY }) do |payload, result|
22
20
  if payload["symbol"] == ANCHOR_CURRENCY
23
21
  result["USD"] = BigDecimal(payload["quote"]["USD"]["price"].to_s)
@@ -18,9 +18,9 @@ module CurrencyRate
18
18
 
19
19
  FETCH_URL = "https://api.coinbase.com/v2/exchange-rates?currency=#{ANCHOR_CURRENCY}"
20
20
 
21
- def normalize(response)
22
- return nil unless super
23
- response["data"]["rates"].reduce({ "anchor" => ANCHOR_CURRENCY }) do |result, (currency, rate)|
21
+ def normalize(data)
22
+ return nil unless data = super
23
+ data["data"]["rates"].reduce({ "anchor" => ANCHOR_CURRENCY }) do |result, (currency, rate)|
24
24
  result[currency] = BigDecimal(rate.to_s)
25
25
  result
26
26
  end
@@ -12,7 +12,7 @@ module CurrencyRate
12
12
  FETCH_URL = "https://api.exmo.com/v1/ticker/"
13
13
 
14
14
  def normalize(data)
15
- return nil unless super
15
+ return nil unless data = super
16
16
  data.reduce({ "anchor" => ANCHOR_CURRENCY }) do |result, (key, value)|
17
17
  if key.split("_")[0] == ANCHOR_CURRENCY
18
18
  result[key.sub("#{self.class::ANCHOR_CURRENCY}_", "")] = BigDecimal(value["avg"].to_s)
@@ -0,0 +1,64 @@
1
+ module CurrencyRate
2
+ class HitBTCAdapter < Adapter
3
+ SUPPORTED_CURRENCIES = %w(
4
+ ZRC EDG IHT AEON SOC HBAR ZRX OPT APPC DRGN PTOY
5
+ XDN OKB CHSB NCT GUSD GET FUN EXP EMRX REV GHOST
6
+ BMH SNC DTR ERD SCL HMQ ACT ETC QTUM MTX SBTC
7
+ KIND SMT BTB SWFTC 1ST UTT AXPR NMR EVX IOTA XPRM
8
+ STMX SALT DGB NTK AMM ALGO ORMEUS BDG BQX EKO FYP
9
+ IPX HOT MG BTS ADX CRPT WAXP POA PLBT SHIP HTML
10
+ BOX GNO UBT BTT ZEN VEO POA20 BCPT SRN XPR ETHBNT
11
+ MANA QKC MLN FLP SOLO TRUE VSYS JST GNT BOS PHB
12
+ ZEC ESH SWM NANO VIBE HVN SOLVE ELEC LRC AGI LNC
13
+ WAVES WTC ONT STRAT GNX NEU BCN XPNT ECA ARDR KIN
14
+ LSK USE IOTX CRO IDH LINK OAX CPT NGC XNS KEY TKY
15
+ HSR TNT SMART TRST DCR WINGS GT MKR ONE DOGE ARN
16
+ ACAT BMC RAISE EXM TIME REX FDZ HT MTH SCC BET
17
+ DENT IDRT IPL ZAP CMCT TDP XAUR MTL NEBL SUSDT BAT
18
+ STEEM CUR BYTZ PRO LOOM USD DRG DICE ADK COMP DRT
19
+ XTZ WETH EURS CHZ NEO NPLC XCON LEVL PAX AIM PART
20
+ PRE ERK HEDG FET PAXG DAG AVA CUTE NEXO DAY PITCH
21
+ MITX NXT POWR PLR CVCOIN TUSD MYST DLT REM RLC DNA
22
+ FOTA SBD ELF TEL C20 PNT CND UTK ASI CVC ETP ETH
23
+ ZIL ARPA INK NPXS LEO MESH NIM DATX FXT PBT GST
24
+ BSV GAS CBC MCO SENT GBX XRC POE SUR LOC WIKI PPT
25
+ CVT APM LEND NUT DOV KMD AYA LUN XEM RVN BCD XMR
26
+ NWC USG CLO NLC2 BBTC BERRY ART GRIN VITAE XBP OMG
27
+ MDA KRL BCH POLY PLA BANCA ENJ TRIGX UUU PASS ANT
28
+ LAMB BIZZ RFR AMB ROOBEE BST LCC RCN MIN BUSD DIT
29
+ PPC AE IQ BNK CENNZ SUB OCN DGD VRA STX AERGO HGT
30
+ TRAD IGNIS REP DAPP DNT CDT YCC SNGLS ICX PKT COV
31
+ PAY ABYSS BLZ DAV TKN ERT SPC SEELE XZC MAID AUTO
32
+ REN DATA AUC TV LAVA KAVA DAI DAPS ADA COCOS MITH
33
+ SETH NRG PHX DASH VLX CHAT VET EOSDT ZSC KNC DGTX
34
+ CEL SHORTUSD IOST BNB PBTT XMC EMC VIB BNT STORJ
35
+ ATOM LCX SC FTT BTM XLM TRX CELR BRD DBIX ETN SNT
36
+ MOF HEX XUC PLU FACE TNC SIG PXG BDP BTX TAU DCT
37
+ YOYOW SYBC SWT MAN GLEEC EOS XVG NAV CURE XRP KICK
38
+ BRDG LTC USDC MATIC FTX BTG PMA
39
+ ).freeze
40
+
41
+ ANCHOR_CURRENCY = "BTC".freeze
42
+
43
+ FETCH_URL = "https://api.hitbtc.com/api/2/public/ticker".freeze
44
+
45
+ def normalize(data)
46
+ return nil unless data = super
47
+
48
+ data.each_with_object({ "anchor" => ANCHOR_CURRENCY }) do |pair_info, result|
49
+ pair_name = pair_info["symbol"]
50
+ next unless pair_name.include?(ANCHOR_CURRENCY)
51
+ next unless pair_info["last"]
52
+
53
+ key = pair_name.sub(ANCHOR_CURRENCY, "")
54
+
55
+ result[key] =
56
+ if pair_name.index(ANCHOR_CURRENCY) == 0
57
+ BigDecimal(pair_info["last"])
58
+ else
59
+ 1 / BigDecimal(pair_info["last"])
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -5,7 +5,7 @@ module CurrencyRate
5
5
  ANCHOR_CURRENCY = "BTC"
6
6
 
7
7
  def normalize(data)
8
- return nil unless super
8
+ return nil unless data = super
9
9
 
10
10
  data["data"].each_with_object({ "anchor" => ANCHOR_CURRENCY }) do |pair_info, result|
11
11
  pair_name = pair_info["symbol"].upcase
@@ -24,7 +24,7 @@ module CurrencyRate
24
24
  FETCH_URL = "https://api.kraken.com/0/public/Ticker?pair=#{ %w(ADAXBT BCHXBT BSVXBT DASHXBT EOSXBT GNOXBT QTUMXBT XTZXBT XETCXXBT XETHXXBT XLTCXXBT XREPXXBT XXLMXXBT XXMRXXBT XXRPXXBT XZECXXBT XXBTZUSD XBTDAI XBTUSDC).join(",") }"
25
25
 
26
26
  def normalize(data)
27
- return nil unless super
27
+ return nil unless data = super
28
28
  data["result"].reduce({ "anchor" => ANCHOR_CURRENCY }) do |result, (pair, value)|
29
29
  key = ta(pair.sub(ta(ANCHOR_CURRENCY), ""))
30
30
 
@@ -14,7 +14,7 @@ module CurrencyRate
14
14
  FETCH_URL = 'https://localbitcoins.com/bitcoinaverage/ticker-all-currencies/'
15
15
 
16
16
  def normalize(data)
17
- return nil unless super
17
+ return nil unless data = super
18
18
  data.reduce({ "anchor" => ANCHOR_CURRENCY }) do |result, (fiat, value)|
19
19
  result["#{fiat.upcase}"] = BigDecimal(value["rates"]["last"].to_s)
20
20
  result
@@ -1,16 +1,14 @@
1
1
  module CurrencyRate
2
2
  class OkcoinAdapter < Adapter
3
3
  FETCH_URL = {
4
- 'LTC_USD' => 'https://www.okcoin.com/api/v1/ticker.do?symbol=ltc_usd',
5
- 'BTC_USD' => 'https://www.okcoin.com/api/v1/ticker.do?symbol=btc_usd',
6
- 'LTC_CNY' => 'https://www.okcoin.cn/api/ticker.do?symbol=ltc_cny',
7
- 'BTC_CNY' => 'https://www.okcoin.cn/api/ticker.do?symbol=btc_cny'
4
+ 'LTC_USD' => 'https://www.okcoin.com/api/spot/v3/instruments/LTC-USD/ticker',
5
+ 'BTC_USD' => 'https://www.okcoin.com/api/spot/v3/instruments/BTC-USD/ticker',
8
6
  }
9
7
 
10
8
  def normalize(data)
11
- return nil unless super
9
+ return nil unless data = super
12
10
  data.reduce({}) do |result, (pair, value)|
13
- result[pair] = BigDecimal(value["ticker"]["last"].to_s)
11
+ result[pair] = BigDecimal(value["last"].to_s)
14
12
  result
15
13
  end
16
14
  end
@@ -7,7 +7,7 @@ module CurrencyRate
7
7
  FETCH_URL = "https://paxful.com/api/currency/btc"
8
8
 
9
9
  def normalize(data)
10
- return nil unless super
10
+ return nil unless data = super
11
11
 
12
12
  {
13
13
  "anchor" => ANCHOR_CURRENCY,
@@ -14,7 +14,7 @@ module CurrencyRate
14
14
  FETCH_URL = "https://poloniex.com/public?command=returnTicker".freeze
15
15
 
16
16
  def normalize(data)
17
- return nil unless super
17
+ return nil unless data = super
18
18
 
19
19
  data.each_with_object({ "anchor" => ANCHOR_CURRENCY }) do |(pair_name, pair_info), result|
20
20
  next unless pair_name.include?(ANCHOR_CURRENCY)
@@ -7,7 +7,7 @@ module CurrencyRate
7
7
  FETCH_URL = "https://api.yadio.io/rate/BTC"
8
8
 
9
9
  def normalize(data)
10
- return nil unless super
10
+ return nil unless data = super
11
11
 
12
12
  {
13
13
  "anchor" => ANCHOR_CURRENCY,
@@ -12,13 +12,11 @@ module CurrencyRate
12
12
  end
13
13
 
14
14
  def request(url)
15
-
16
- http_client = HTTP.timeout(connect:
17
- CurrencyRate.configuration.connect_timeout, read:
18
- CurrencyRate.configuration.read_timeout
15
+ http_client = HTTP.timeout(
16
+ connect: @container.connect_timeout,
17
+ read: @container.read_timeout
19
18
  )
20
19
  http_client.get(url).to_s
21
-
22
20
  end
23
21
 
24
22
  end
@@ -3,12 +3,11 @@ module CurrencyRate
3
3
  # No need to use it for fetching, just additional information about supported currencies
4
4
  SUPPORTED_CURRENCIES = %w(ARS)
5
5
  ANCHOR_CURRENCY = "USD"
6
- FETCH_URL = "https://ar.coinmonitor.info/api/dolar_ar/"
6
+ FETCH_URL = "https://ar.coinmonitor.info/data_ar.json"
7
7
 
8
8
  def normalize(data)
9
- return nil unless super
10
-
11
- { "anchor" => ANCHOR_CURRENCY }.merge({ SUPPORTED_CURRENCIES.first => BigDecimal(data["DOL_blue"].to_s) })
9
+ return nil unless data = super
10
+ { "anchor" => ANCHOR_CURRENCY }.merge({ SUPPORTED_CURRENCIES.first => BigDecimal(data["BTC_avr_ars"].to_s) })
12
11
  end
13
12
  end
14
- end
13
+ end
@@ -17,11 +17,10 @@ module CurrencyRate
17
17
  )
18
18
 
19
19
  ANCHOR_CURRENCY = "USD"
20
- FETCH_URL = "http://www.apilayer.net/api/live"
21
- API_KEY_PARAM = "access_key"
20
+ FETCH_URL = "http://apilayer.net/api/live?access_key=__API_KEY__"
22
21
 
23
22
  def normalize(data)
24
- return nil unless super
23
+ return nil unless data = super
25
24
  rates = { "anchor" => self.class::ANCHOR_CURRENCY }
26
25
  data["quotes"].each do |key, value|
27
26
  rates[key.sub(self.class::ANCHOR_CURRENCY, "")] = BigDecimal(value.to_s)
@@ -2,13 +2,12 @@ module CurrencyRate
2
2
  class FixerAdapter < Adapter
3
3
  # EUR is the only currency available as a base on free plan
4
4
  ANCHOR_CURRENCY = "EUR"
5
- FETCH_URL = "http://data.fixer.io/latest"
6
- API_KEY_PARAM = "access_key"
5
+ FETCH_URL = "http://data.fixer.io/api/latest?access_key=__API_KEY__&base=#{ANCHOR_CURRENCY}"
7
6
 
8
7
  def normalize(data)
9
- return nil unless super
8
+ return nil unless data = super
10
9
  rates = { "anchor" => ANCHOR_CURRENCY }
11
- data["rates"].each do |k, v|
10
+ data["rates"].each do |k,v|
12
11
  rates[k] = BigDecimal(v.to_s)
13
12
  end
14
13
  rates
@@ -1,22 +1,31 @@
1
1
  module CurrencyRate
2
2
  class ForgeAdapter < Adapter
3
3
  SUPPORTED_CURRENCIES = %w(
4
- JPY CHF CAD SEK NOK MXN ZAR TRY CNH EUR GBP AUD NZD XAU XAG
4
+ AED AFN ALL AMD ANG AOA ARE ARS AUD AUN AWG BAM BBD BDT BGN BHD BIF BMD
5
+ BND BOB BRI BRL BSD BTN BWP BYN BZD CAD CDF CHF CLF CLP CLY CNH CNY COP
6
+ CRC CUP CVE CYP CZK DJF DKK DOE DOP DZD EGP ETB EUR FJD FRN GBP GEL GHS
7
+ GMD GNF GTQ GYD HKD HNL HRK HTG HUF IDR ILS INR IQD IRR ISK JMD JOD JPY
8
+ KES KHR KMF KRU KRW KWD KYD KZT LAK LBP LFX LKR LRD LSL LTL LYD M5P MAD
9
+ MAL MDL MGA MKD MMK MOP MRU MTL MUR MVR MWK MXN MYR MZN NAD NBL NGN NIO
10
+ NOK NPR NSO NZD OMR OSO PAB PEN PGK PHP PKR PLN PYG QAR RON RSD RUB RWF
11
+ SAR SBD SCR SDG SEK SGD SHP SLL SOS SRD STN SVC SZL THB TJS TMT TND TOP
12
+ TRY TTD TWD TZS UAH UGX USD UYU UZS VES VND VRL VRN XAG XAGK XAU XAUK XCD
13
+ XDR XOF XPD XPDK XPF XPT XPTK YER ZAR ZMW ZWD
5
14
  )
6
15
 
7
16
  ANCHOR_CURRENCY = "USD"
8
- FETCH_URL = "https://forex.1forge.com/1.0.2/quotes?pairs=" + SUPPORTED_CURRENCIES.map { |c| "#{ANCHOR_CURRENCY}#{c}" }.join(",")
9
- API_KEY_PARAM = "api_key"
17
+ currency_pairs_request = SUPPORTED_CURRENCIES.map { |c| "#{ANCHOR_CURRENCY}/#{c}" }.join(",")
18
+ FETCH_URL = "https://api.1forge.com/quotes?pairs=#{currency_pairs_request}&api_key=__API_KEY__"
10
19
 
11
20
  def normalize(data)
12
- return nil unless super
21
+ return nil unless data = super
13
22
  rates = { "anchor" => self.class::ANCHOR_CURRENCY }
14
23
  data.each do |rate|
15
24
  if rate["error"]
16
- CurrencyRate.logger.error("Forge exchange returned error")
25
+ @container.logger.error("Forge exchange returned error")
17
26
  return nil
18
27
  end
19
- rates[rate["symbol"].sub(self.class::ANCHOR_CURRENCY, "")] = BigDecimal(rate["price"].to_s)
28
+ rates[rate["s"].sub("#{self.class::ANCHOR_CURRENCY}/", "")] = BigDecimal(rate["p"].to_s) if rate["p"]
20
29
  end
21
30
  rates
22
31
  end
@@ -1,4 +1,8 @@
1
1
  module CurrencyRate
2
+
3
+ # Beware, freeforexapi.com doesn't normally work and you need to place
4
+ # a small banner on your website so they'd list your websites IP address.
5
+ # More info here: http://freeforexapi.com/Home/Api
2
6
  class FreeForexAdapter < Adapter
3
7
  SUPPORTED_CURRENCIES = %w(
4
8
  AED AFN ALL AMD ANG AOA ARS ATS AUD AWG AZM AZN BAM BBD BDT
@@ -18,10 +22,10 @@ module CurrencyRate
18
22
  )
19
23
 
20
24
  ANCHOR_CURRENCY = "USD"
21
- FETCH_URL = "https://www.freeforexapi.com/api/live?pairs=" + SUPPORTED_CURRENCIES.map{|cur| "USD#{cur}"}.join(",")
25
+ FETCH_URL = "https://www.freeforexapi.com/api/live?pairs=" + SUPPORTED_CURRENCIES.map { |c| "USD#{c}"}.join(",")
22
26
 
23
27
  def normalize(data)
24
- return nil unless super
28
+ return nil unless data = super
25
29
  rates = { "anchor" => self.class::ANCHOR_CURRENCY }
26
30
  data["rates"].each do |pair, payload|
27
31
  rates[pair.sub(self.class::ANCHOR_CURRENCY, "")] = BigDecimal(payload["rate"].to_s)
@@ -29,4 +33,4 @@ module CurrencyRate
29
33
  rates
30
34
  end
31
35
  end
32
- end
36
+ end
@@ -0,0 +1,203 @@
1
+ class CurrencyRate::Container
2
+
3
+ attr_accessor :api_keys
4
+ attr_accessor :logger
5
+ attr_accessor :adapters
6
+ attr_accessor :connect_timeout
7
+ attr_accessor :read_timeout
8
+ attr_accessor :storage
9
+ attr_accessor :logger_callbacks
10
+ attr_accessor :limit_adapters_for_pairs
11
+
12
+ def initialize(
13
+ api_keys: {},
14
+ adapter_type: nil,
15
+ adapter_names: nil,
16
+ limit_adapters_for_pairs: {},
17
+ storage_settings: CurrencyRate.default_config[:storage_settings],
18
+ connect_timeout: CurrencyRate.default_config[:connect_timeout],
19
+ read_timeout: CurrencyRate.default_config[:read_timeout],
20
+ logger_settings: CurrencyRate.default_config[:logger_settings],
21
+ logger_callbacks: CurrencyRate.default_config[:logger_callbacks]
22
+ )
23
+
24
+ logger_settigns = CurrencyRate.default_config[:logger_settings].merge(logger_settings)
25
+ storage_settigns = CurrencyRate.default_config[:storage_settings].merge(storage_settings)
26
+
27
+ method(__method__).parameters.map do |_, name|
28
+ value = binding.local_variable_get(name)
29
+ instance_variable_set("@#{name}", value)
30
+ end
31
+
32
+ _logger_callbacks = {}
33
+ @logger_callbacks.each do |k,v|
34
+ _logger_callbacks[Logger::Severity.const_get(k.to_s.upcase)] = v
35
+ end
36
+ @logger_callbacks = _logger_callbacks
37
+
38
+ _limit_adapters_for_pairs = {}
39
+ @limit_adapters_for_pairs.each do |k,v|
40
+ _limit_adapters_for_pairs[k] = CurrencyRate.const_get(v.to_s.to_camel_case + "Adapter").instance
41
+ end
42
+ @limit_adapters_for_pairs = _limit_adapters_for_pairs
43
+
44
+ @storage = CurrencyRate.const_get(storage_settigns[:type].to_s.to_camel_case + "Storage").new(
45
+ path: storage_settings[:path],
46
+ container: self,
47
+ serializer: storage_settings[:serializer]
48
+ )
49
+
50
+ load_adapters!(names: adapter_names, type: adapter_type)
51
+ end
52
+
53
+ def method_missing(m, *args, &block)
54
+ if m.to_s.end_with? "_adapters"
55
+ self.send(:adapters, m[0..-10])
56
+ else
57
+ super
58
+ end
59
+ end
60
+
61
+ def logger
62
+ return @logger if @logger
63
+ @logger = Logger.new(@logger_settings[:device])
64
+ @logger.progname = "CurrencyRate"
65
+ @logger.level = @logger_settings[:level]
66
+ @logger.formatter = @logger_settings[:formatter] if @logger_settings[:formatter]
67
+ @logger
68
+ end
69
+
70
+ # This method doesn't make any requests. It merely reads normalized data
71
+ # from the selected storage. Contrary to what one might assume,
72
+ # it doesn't call CurrencyRate::Container.sync! if storage for a particular
73
+ # adapter doesn't exist - that's because those are two separate tasks
74
+ # and you might not want to make that external http(s) request even if
75
+ # the rates in storage are not found.
76
+ #
77
+ # It also uses a 3 different strategies to calculate rates before they are returned:
78
+ #
79
+ # 1. If only one adapter is specified and/or available, it will return the rate for the
80
+ # pair using the data from the storage for this particular adapter.
81
+ #
82
+ # 2. If two or more adapters are specified and available, it will return the average rate
83
+ # based on the the rates from all of those adapters.
84
+ #
85
+ # 3. If `strategy: :majority` flag is set and an odd number of adapters (say, 3)
86
+ # is passed and/or available it will look at the rates from all, separate them
87
+ # into two groups based on how close their rates are and return the average of the rates
88
+ # that are closest in the group that is the largest.
89
+ #
90
+ # It will also check @limit_adapters_for_pairs hash for the request pair
91
+ # and if the key exists it will exclude adapters that are not listed in the array
92
+ # under that key.
93
+ def get_rate(from, to, use_adapters=@adapter_names, strategy: :average)
94
+ adapters_for_pair = @limit_adapters_for_pairs["#{from}/#{to}"]
95
+ _adapters = adapters.select { |a| use_adapters.include?(a.name(:camel_case)) }
96
+ _adapters = _adapters.select { |a| adapters_for_pair.include?(a.name(:camel_case)) } if adapters_for_pair
97
+
98
+ warning_loggers = []
99
+ rates = _adapters.map do |a|
100
+ rate = [a.name(:camel_case), a.get_rate(from, to)]
101
+ warning_loggers.push -> { self.log(:warn, "No rate for pair #{from}/#{to} found in #{a.name(:camel_case)} adapter") if rate[1].nil? }
102
+ rate
103
+ end.select { |r| !r[1].nil? }
104
+
105
+ if rates.empty?
106
+ self.log(:error,
107
+ "No rate for pair #{from}/#{to} is found in any of the available adapters " +
108
+ "(#{_adapters.map { |a| a.name(:camel_case) }.join(", ")})"
109
+ )
110
+ else
111
+ warning_loggers.each { |wl| wl.call }
112
+ end
113
+
114
+ if rates.size == 1
115
+ rates[0][1]
116
+ else
117
+ if strategy == :majority && rates.size.odd?
118
+ rates.sort! { |r1, r2| r1[1] <=> r2[1] }
119
+
120
+ largest_discrepancy_rate_index = 0
121
+ last_discrepancy = 0
122
+
123
+ rates.each_with_index do |r,i|
124
+ if i > 0
125
+ discrepancy = r[1] - rates[i-1][1]
126
+ if discrepancy > last_discrepancy
127
+ last_discrepancy = discrepancy
128
+ largest_discrepancy_rate_index = i
129
+ end
130
+ end
131
+ end
132
+
133
+ rates_group_1 = rates[0...largest_discrepancy_rate_index]
134
+ rates_group_2 = rates[largest_discrepancy_rate_index..rates.size-1]
135
+ rates = [rates_group_1, rates_group_2].max { |g1,g2| g1.size <=> g2.size }
136
+ end
137
+ rates.inject(BigDecimal(0)) { |sum, i| sum + i[1] } / BigDecimal(rates.size)
138
+ end
139
+
140
+ end
141
+
142
+ def sync!
143
+ successfull = []
144
+ failed = []
145
+ @adapters.each do |adapter|
146
+ adapter_name = adapter.class.to_s.sub("CurrencyRate::", "")
147
+ begin
148
+ rates = adapter.fetch_rates
149
+ unless rates
150
+ self.log(:warn, "Trying to sync rates for #{adapter_name}, rates not found, but http(s) request did not return any error.")
151
+ failed << adapter_name
152
+ next
153
+ end
154
+ @storage.write(adapter_name.to_snake_case.sub("_adapter", ""), rates)
155
+ successfull << adapter_name
156
+ rescue StandardError => e
157
+ failed << adapter_name
158
+ self.log(:error, e)
159
+ next
160
+ end
161
+ end
162
+ [successfull, failed]
163
+ end
164
+
165
+ def log(level, message)
166
+ severity = Logger::Severity.const_get(level.to_s.upcase)
167
+ logger.send(level, message)
168
+ if @logger_callbacks[severity]
169
+ @logger_callbacks[severity].call(level, message)
170
+ else
171
+ @logger_callbacks.keys.sort.each do |k|
172
+ @logger_callbacks[k].call(level, message) && break if k < severity
173
+ end
174
+ end
175
+ end
176
+
177
+ private
178
+
179
+ def load_adapters!(names: nil, type: :all)
180
+ if names
181
+ names = [names] if names.kind_of?(String)
182
+ @adapters = names.map do |name|
183
+ CurrencyRate.const_get(name + "Adapter").instance
184
+ end
185
+ else
186
+ crypto_adapter_files = Dir[File.join CurrencyRate.root, "lib/adapters/crypto/*"]
187
+ fiat_adapter_files = Dir[File.join CurrencyRate.root, "lib/adapters/fiat/*"]
188
+ adapter_files = case type
189
+ when :crypto then crypto_adapter_files
190
+ when :fiat then fiat_adapter_files
191
+ else crypto_adapter_files + fiat_adapter_files
192
+ end
193
+ @adapters = adapter_files.map do |file|
194
+ CurrencyRate.const_get(File.basename(file, ".rb").to_camel_case).instance
195
+ end
196
+ end
197
+ @adapters.each { |a| a.container = self; a.api_key = @api_keys[a.name] }
198
+ @adapter_names = @adapters.map { |a| a.name(:camel_case) }
199
+ @adapters
200
+ end
201
+
202
+
203
+ end
@@ -5,81 +5,37 @@ require "json"
5
5
  require "http"
6
6
 
7
7
  require_relative "exceptions"
8
- require_relative "configuration"
9
8
  require_relative "adapter"
10
- require_relative "fetcher"
11
- require_relative "synchronizer"
9
+ require_relative "container"
10
+ require_relative "utils/string_extensions"
12
11
 
13
12
  Dir["#{File.expand_path __dir__}/adapters/**/*.rb"].each { |f| require f }
14
- Dir["#{File.expand_path __dir__}/storage/**/*.rb"].each { |f| require f }
13
+ Dir["#{File.expand_path __dir__}/storage/**/*.rb"].each { |f| require f }
15
14
 
16
15
  module CurrencyRate
16
+
17
+ @@default_config = {
18
+ connect_timeout: 10,
19
+ read_timeout: 10,
20
+ logger_callbacks: {},
21
+ logger_settings: { device: $stdout, level: :info, formatter: nil },
22
+ storage_settings: { path: __dir__, serializer: CurrencyRate::Storage::YAMLSerializer }
23
+ }
24
+
17
25
  class << self
18
- attr_writer :configuration
19
- end
20
26
 
21
- def self.method_missing(m, *args, &block)
22
- if m.to_s.end_with? "_adapters"
23
- self.send(:adapters, m[0..-10])
24
- else
25
- super
27
+ def default_config
28
+ @@default_config
26
29
  end
27
- end
28
30
 
29
- def self.adapters(type)
30
- Dir[File.join self.root, "lib/adapters/#{type}/*"].map do |file|
31
- File.basename(file, ".rb").split('_')[0...-1].map {|w| w.capitalize}.join
31
+ def update_default_config(config_hash)
32
+ @@default_config.merge!(config_hash)
32
33
  end
33
- end
34
-
35
- def self.configuration
36
- @configuration ||= Configuration.new
37
- end
38
-
39
- def self.configure
40
- yield(configuration)
41
- end
42
-
43
- def self.fetcher
44
- @fetcher ||= Fetcher.new(fiat_exchanges: configuration.fiat_adapters,
45
- crypto_exchanges: configuration.crypto_adapters,
46
- limit_sources_for_fiat_currencies: configuration.limit_sources_for_fiat_currencies)
47
- end
48
-
49
- def self.fetch_crypto(exchange, from, to)
50
- fetcher.fetch_crypto(exchange, from, to)
51
- end
52
-
53
- def self.fetch_fiat(from, to)
54
- fetcher.fetch_fiat(from, to)
55
- end
56
-
57
- def self.logger
58
- return @logger if @logger
59
- @logger = Logger.new(configuration.logger[:device])
60
- @logger.progname = "CurrencyRate"
61
- @logger.level = configuration.logger[:level]
62
- @logger.formatter = configuration.logger[:formatter] if configuration.logger[:formatter]
63
- @logger
64
- end
65
-
66
- def self.synchronizer
67
- @synchronizer ||= Synchronizer.new
68
- end
69
-
70
- def self.sync_crypto!
71
- synchronizer.sync_crypto!
72
- end
73
34
 
74
- def self.sync_fiat!
75
- synchronizer.sync_fiat!
76
- end
35
+ def root
36
+ File.expand_path("../", File.dirname(__FILE__))
37
+ end
77
38
 
78
- def self.sync!
79
- synchronizer.sync!
80
39
  end
81
40
 
82
- def self.root
83
- File.expand_path("../", File.dirname(__FILE__))
84
- end
85
41
  end
@@ -1,3 +1,3 @@
1
1
  module CurrencyRate
2
- VERSION = "1.7.0"
2
+ VERSION = "2.0.0"
3
3
  end
@@ -3,27 +3,32 @@ module CurrencyRate
3
3
  attr_reader :path
4
4
  attr_accessor :serializer
5
5
 
6
- def initialize(path:, serializer: nil)
6
+ def initialize(path:, container:, serializer: nil)
7
7
  @path = path
8
+ @container = container
8
9
  @serializer = serializer || Storage::YAMLSerializer.new
9
10
  end
10
11
 
11
- def read(exchange_name)
12
- path = path_for exchange_name.downcase
12
+ def exists?(adapter_name)
13
+ File.exists? path_for(adapter_name)
14
+ end
15
+
16
+ def read(adapter_name)
17
+ path = path_for adapter_name
13
18
  @serializer.deserialize File.read(path)
14
19
  rescue StandardError => e
15
- CurrencyRate.logger.error(e)
20
+ @container.log(:error, e)
16
21
  nil
17
22
  end
18
23
 
19
- def write(exchange_name, data = "")
20
- File.write path_for(exchange_name.downcase), @serializer.serialize(data)
24
+ def write(adapter_name, data = "")
25
+ File.write path_for(adapter_name), @serializer.serialize(data)
21
26
  rescue StandardError => e
22
- CurrencyRate.logger.error(e)
27
+ @container.log(:error, e)
23
28
  end
24
29
 
25
- def path_for(exchange_name)
26
- File.join @path, "#{exchange_name}_rates.yml"
30
+ def path_for(adapter_name)
31
+ File.join @path, "#{adapter_name}.yml"
27
32
  end
28
33
  end
29
34
  end
@@ -0,0 +1,19 @@
1
+ module StringExtensions
2
+
3
+ def to_snake_case
4
+ self.to_s.gsub(/::/, '/').
5
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
6
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
7
+ tr("-", "_").
8
+ downcase
9
+ end
10
+
11
+ def to_camel_case
12
+ words = self.to_s.split('_')
13
+ words.map { |w| w[0].capitalize + w[1..-1] }.join
14
+ end
15
+
16
+ end
17
+
18
+ class String; include StringExtensions; end
19
+ class Symbol; include StringExtensions; end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: currency-rate
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roman Snitko
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-08-18 00:00:00.000000000 Z
11
+ date: 2020-12-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: http
@@ -94,19 +94,16 @@ files:
94
94
  - LICENSE.txt
95
95
  - README.md
96
96
  - Rakefile
97
- - api_keys.yml.sample
98
97
  - currency-rate.gemspec
99
98
  - lib/adapter.rb
100
99
  - lib/adapters/crypto/binance_adapter.rb
101
100
  - lib/adapters/crypto/bitfinex_adapter.rb
102
101
  - lib/adapters/crypto/bitpay_adapter.rb
103
102
  - lib/adapters/crypto/bitstamp_adapter.rb
104
- - lib/adapters/crypto/btc_china_adapter.rb
105
- - lib/adapters/crypto/btc_e_adapter.rb
106
103
  - lib/adapters/crypto/coin_market_cap_adapter.rb
107
104
  - lib/adapters/crypto/coinbase_adapter.rb
108
105
  - lib/adapters/crypto/exmo_adapter.rb
109
- - lib/adapters/crypto/hit_btc_adapter.rb
106
+ - lib/adapters/crypto/hit_BTC_adapter.rb
110
107
  - lib/adapters/crypto/huobi_adapter.rb
111
108
  - lib/adapters/crypto/kraken_adapter.rb
112
109
  - lib/adapters/crypto/localbitcoins_adapter.rb
@@ -120,15 +117,13 @@ files:
120
117
  - lib/adapters/fiat/fixer_adapter.rb
121
118
  - lib/adapters/fiat/forge_adapter.rb
122
119
  - lib/adapters/fiat/free_forex_adapter.rb
123
- - lib/adapters/fiat/yahoo_adapter.rb
124
- - lib/configuration.rb
120
+ - lib/container.rb
125
121
  - lib/currency_rate.rb
126
122
  - lib/currency_rate/version.rb
127
123
  - lib/exceptions.rb
128
- - lib/fetcher.rb
129
124
  - lib/storage/file_storage.rb
130
125
  - lib/storage/serializers/yaml_serializer.rb
131
- - lib/synchronizer.rb
126
+ - lib/utils/string_extensions.rb
132
127
  homepage: https://gitlab.com/hodlhodl-public/currency-rate
133
128
  licenses: []
134
129
  metadata:
@@ -150,7 +145,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
150
145
  - !ruby/object:Gem::Version
151
146
  version: '0'
152
147
  requirements: []
153
- rubygems_version: 3.1.4
148
+ rubygems_version: 3.1.2
154
149
  signing_key:
155
150
  specification_version: 4
156
151
  summary: Converter for fiat and crypto currencies
@@ -1,4 +0,0 @@
1
- ForgeAdapter: "<your_key>"
2
- CurrencyLayerAdapter: "<your_key>"
3
- FixerAdapter: "<your_key>"
4
- CoinMarketCapAdapter: "<your_key>"
@@ -1,11 +0,0 @@
1
- module CurrencyRate
2
- class BtcChinaAdapter < Adapter
3
- FETCH_URL = 'https://data.btcchina.com/data/ticker'
4
-
5
- def normalize(data)
6
- return nil unless super
7
- { "BTC_CNY" => BigDecimal(data["ticker"]["last"].to_s) }
8
- end
9
-
10
- end
11
- end
@@ -1,18 +0,0 @@
1
- module CurrencyRate
2
- class BtcEAdapter < Adapter
3
- FETCH_URL = {
4
- 'BTC_USD' => 'https://wex.nz/api/2/btc_usd/ticker',
5
- 'BTC_EUR' => 'https://wex.nz/api/2/btc_eur/ticker',
6
- 'BTC_RUB' => 'https://wex.nz/api/2/btc_rur/ticker',
7
- }
8
-
9
- def normalize(data)
10
- return nil unless super
11
- data.reduce({}) do |result, (pair, value)|
12
- result[pair] = BigDecimal(value["ticker"]["last"].to_s)
13
- result
14
- end
15
- end
16
-
17
- end
18
- end
@@ -1,61 +0,0 @@
1
- module CurrencyRate
2
- class HitBTCAdapter < Adapter
3
- SUPPORTED_CURRENCIES = %w(ZRC EDG IHT AEON SOC HBAR ZRX OPT APPC DRGN PTOY
4
- XDN OKB CHSB NCT GUSD GET FUN EXP EMRX REV GHOST
5
- BMH SNC DTR ERD SCL HMQ ACT ETC QTUM MTX SBTC
6
- KIND SMT BTB SWFTC 1ST UTT AXPR NMR EVX IOTA XPRM
7
- STMX SALT DGB NTK AMM ALGO ORMEUS BDG BQX EKO FYP
8
- IPX HOT MG BTS ADX CRPT WAXP POA PLBT SHIP HTML
9
- BOX GNO UBT BTT ZEN VEO POA20 BCPT SRN XPR ETHBNT
10
- MANA QKC MLN FLP SOLO TRUE VSYS JST GNT BOS PHB
11
- ZEC ESH SWM NANO VIBE HVN SOLVE ELEC LRC AGI LNC
12
- WAVES WTC ONT STRAT GNX NEU BCN XPNT ECA ARDR KIN
13
- LSK USE IOTX CRO IDH LINK OAX CPT NGC XNS KEY TKY
14
- HSR TNT SMART TRST DCR WINGS GT MKR ONE DOGE ARN
15
- ACAT BMC RAISE EXM TIME REX FDZ HT MTH SCC BET
16
- DENT IDRT IPL ZAP CMCT TDP XAUR MTL NEBL SUSDT BAT
17
- STEEM CUR BYTZ PRO LOOM USD DRG DICE ADK COMP DRT
18
- XTZ WETH EURS CHZ NEO NPLC XCON LEVL PAX AIM PART
19
- PRE ERK HEDG FET PAXG DAG AVA CUTE NEXO DAY PITCH
20
- MITX NXT POWR PLR CVCOIN TUSD MYST DLT REM RLC DNA
21
- FOTA SBD ELF TEL C20 PNT CND UTK ASI CVC ETP ETH
22
- ZIL ARPA INK NPXS LEO MESH NIM DATX FXT PBT GST
23
- BSV GAS CBC MCO SENT GBX XRC POE SUR LOC WIKI PPT
24
- CVT APM LEND NUT DOV KMD AYA LUN XEM RVN BCD XMR
25
- NWC USG CLO NLC2 BBTC BERRY ART GRIN VITAE XBP OMG
26
- MDA KRL BCH POLY PLA BANCA ENJ TRIGX UUU PASS ANT
27
- LAMB BIZZ RFR AMB ROOBEE BST LCC RCN MIN BUSD DIT
28
- PPC AE IQ BNK CENNZ SUB OCN DGD VRA STX AERGO HGT
29
- TRAD IGNIS REP DAPP DNT CDT YCC SNGLS ICX PKT COV
30
- PAY ABYSS BLZ DAV TKN ERT SPC SEELE XZC MAID AUTO
31
- REN DATA AUC TV LAVA KAVA DAI DAPS ADA COCOS MITH
32
- SETH NRG PHX DASH VLX CHAT VET EOSDT ZSC KNC DGTX
33
- CEL SHORTUSD IOST BNB PBTT XMC EMC VIB BNT STORJ
34
- ATOM LCX SC FTT BTM XLM TRX CELR BRD DBIX ETN SNT
35
- MOF HEX XUC PLU FACE TNC SIG PXG BDP BTX TAU DCT
36
- YOYOW SYBC SWT MAN GLEEC EOS XVG NAV CURE XRP KICK
37
- BRDG LTC USDC MATIC FTX BTG PMA).freeze
38
-
39
- ANCHOR_CURRENCY = "BTC".freeze
40
-
41
- FETCH_URL = "https://api.hitbtc.com/api/2/public/ticker".freeze
42
-
43
- def normalize(data)
44
- return nil unless super
45
-
46
- data.each_with_object({ "anchor" => ANCHOR_CURRENCY }) do |pair_info, result|
47
- pair_name = pair_info["symbol"]
48
- next unless pair_name.include?(ANCHOR_CURRENCY)
49
-
50
- key = pair_name.sub(ANCHOR_CURRENCY, "")
51
-
52
- result[key] =
53
- if pair_name.index(ANCHOR_CURRENCY) == 0
54
- BigDecimal(pair_info["last"])
55
- else
56
- 1 / BigDecimal(pair_info["last"])
57
- end
58
- end
59
- end
60
- end
61
- end
@@ -1,35 +0,0 @@
1
- module CurrencyRate
2
- class YahooAdapter < Adapter
3
- SUPPORTED_CURRENCIES = %w(
4
- AED AFN ALL AMD ANG AOA ARS AUD AWG AZN BAM BBD BDT BGN BHD BIF BMD BND
5
- BRL BSD BTN BWP BYR BZD CAD CHF CLF CLP CNY COP CRC CUC CUP CVE CZK DEM
6
- DJF DKK DOP DZD ECS EGP ERN ETB EUR FJD FKP FRF GBP GEL GHS GIP GMD GNF
7
- GTQ GYD HKD HNL HRK HTG HUF IDR IEP ILS INR IQD ISK ITL JMD JOD JPY KES
8
- KGS KHR KMF KWD KYD KZT LAK LBP LKR LRD LSL LTL LVL LYD MAD MGA MMK MNT
9
- MOP MRO MUR MVR MWK MXN MXV MYR MZN NAD NGN NIO NOK NPR NZD OMR PAB PEN
10
- PGK PHP PKR PLN PYG QAR RON RSD RUB RWF SAR SBD SCR SDG SEK SGD SLL SOS
11
- SRD STD SVC SYP SZL THB TJS TMT TND TOP TRY TTD UAH UGX USD UYU UZS VND
12
- VUV WST XAF XAG XAU XCD XDR XOF XPD XPF XPT YER ZAR ZMW ZWL
13
- )
14
-
15
- ANCHOR_CURRENCY = "USD"
16
-
17
- FETCH_URL = "http://query.yahooapis.com/v1/public/yql?" + URI.encode_www_form(
18
- format: 'json',
19
- env: "store://datatables.org/alltableswithkeys",
20
- q: "SELECT * FROM yahoo.finance.xchange WHERE pair IN" +
21
- # The following line is building array string in SQL: '("USDJPY", "USDRUB", ...)'
22
- "(#{SUPPORTED_CURRENCIES.map{|x| '"' + ANCHOR_CURRENCY + x.upcase + '"'}.join(',')})"
23
- )
24
-
25
- def normalize(data)
26
- return nil unless super
27
- rates = { "anchor" => self.class::ANCHOR_CURRENCY }
28
- data["query"]["results"]["rate"].each do |rate|
29
- rates[rate["Name"].split("/")[1]] = BigDecimal(rate["Rate"].to_s)
30
- end
31
- rates
32
- end
33
-
34
- end
35
- end
@@ -1,28 +0,0 @@
1
- module CurrencyRate
2
- class Configuration
3
- attr_accessor :api_keys
4
- attr_accessor :logger
5
- attr_accessor :crypto_adapters
6
- attr_accessor :fiat_adapters
7
- attr_accessor :connect_timeout
8
- attr_accessor :read_timeout
9
- attr_accessor :storage
10
- attr_accessor :limit_sources_for_fiat_currencies
11
- attr_accessor :crypto_currencies
12
-
13
- def initialize
14
- @api_keys = { }
15
- @logger = {
16
- device: $stdout,
17
- level: :info,
18
- formatter: nil,
19
- }
20
- @crypto_adapters = CurrencyRate.adapters :crypto
21
- @fiat_adapters = CurrencyRate.adapters :fiat
22
- @connect_timeout = 4
23
- @read_timeout = 4
24
- @limit_sources_for_fiat_currencies = {}
25
- @crypto_currencies = []
26
- end
27
- end
28
- end
@@ -1,80 +0,0 @@
1
- module CurrencyRate
2
- class Fetcher
3
- attr_accessor :storage
4
- attr_accessor :fiat_exchanges
5
- attr_accessor :crypto_exchanges
6
- attr_accessor :limit_sources_for_fiat_currencies
7
-
8
- def initialize(fiat_exchanges: nil, crypto_exchanges: nil, storage: nil, limit_sources_for_fiat_currencies: {})
9
- @storage = storage || CurrencyRate.configuration.storage
10
- raise CurrencyRate::StorageNotDefinedError unless @storage
11
-
12
- @fiat_exchanges = fiat_exchanges || ["Yahoo", "Fixer", "Forge"]
13
- @crypto_exchanges = crypto_exchanges || ["Bitstamp", "Binance"]
14
- @limit_sources_for_fiat_currencies = limit_sources_for_fiat_currencies
15
- end
16
-
17
- def fetch_crypto(exchange, from, to)
18
- from = from.strip.upcase
19
- to = to.strip.upcase
20
- rates = @storage.read(exchange)
21
-
22
- if rates.nil?
23
- CurrencyRate.logger.warn("Fetcher#fetch_crypto: rates for #{exchange} not found in storage <#{@storage.class.name}>")
24
- return nil
25
- end
26
-
27
- rate = calculate_rate(rates, from, to)
28
- return rate unless rate.nil?
29
-
30
- if to != "USD"
31
- usd_fiat = fetch_fiat("USD", to)
32
- return BigDecimal(rates["USD"] * usd_fiat) if usd_fiat && rates["USD"]
33
- end
34
- nil
35
- end
36
-
37
- def fetch_fiat(from, to)
38
- from = from.strip.upcase
39
- to = to.strip.upcase
40
-
41
- exchanges = @fiat_exchanges.dup
42
- exchanges += @crypto_exchanges if is_crypto_currency?(from) || is_crypto_currency?(to)
43
-
44
- if(@limit_sources_for_fiat_currencies[from])
45
- exchanges.select! { |ex| @limit_sources_for_fiat_currencies[from].include?(ex) }
46
- end
47
- if(@limit_sources_for_fiat_currencies[to])
48
- exchanges.select! { |ex| @limit_sources_for_fiat_currencies[to].include?(ex) }
49
- end
50
-
51
- exchanges.each do |exchange|
52
- rates = @storage.read(exchange)
53
- next if rates.nil?
54
-
55
- rate = calculate_rate(rates, from, to)
56
- return rate unless rate.nil?
57
- end
58
- nil
59
- end
60
-
61
- private
62
-
63
- def calculate_rate(rates, from, to)
64
- anchor = rates.delete("anchor")
65
-
66
- return BigDecimal(rates[to]) if anchor == from && rates[to]
67
- return BigDecimal(1 / rates[from]) if anchor == to && rates[from]
68
- return BigDecimal(rates[to] / rates[from]) if rates[from] && rates[to]
69
-
70
- CurrencyRate.logger.warn("Fetcher: rate for #{from}_#{to} not found.")
71
- nil
72
- end
73
-
74
- def is_crypto_currency?(currency)
75
- CurrencyRate.configuration.crypto_currencies.include?(currency)
76
- end
77
-
78
- end
79
- end
80
-
@@ -1,54 +0,0 @@
1
- module CurrencyRate
2
- class Synchronizer
3
- attr_accessor :storage
4
-
5
- def initialize(storage: nil)
6
- @storage = storage || CurrencyRate.configuration.storage
7
- raise CurrencyRate::StorageNotDefinedError unless @storage
8
- end
9
-
10
- def sync_fiat!
11
- _sync CurrencyRate.configuration.fiat_adapters
12
- end
13
-
14
- def sync_crypto!
15
- _sync CurrencyRate.configuration.crypto_adapters
16
- end
17
-
18
- def sync!
19
- fiat = sync_fiat!
20
- crypto = sync_crypto!
21
- [fiat[0] | crypto[0], fiat[1] | crypto[1]]
22
- end
23
-
24
- private
25
-
26
- def _sync(adapters)
27
- successfull = []
28
- failed = []
29
- adapters.each do |provider|
30
- adapter_name = "#{provider}Adapter"
31
- begin
32
- adapter = CurrencyRate.const_get(adapter_name).instance
33
- rates = adapter.fetch_rates
34
-
35
- unless rates
36
- CurrencyRate.logger.warn("Synchronizer#sync!: rates for #{provider} not found")
37
- failed.push(provider)
38
- next
39
- end
40
-
41
- exchange_name = provider
42
- @storage.write(exchange_name, rates)
43
- successfull.push(provider)
44
- rescue StandardError => e
45
- failed.push({ provider: e })
46
- CurrencyRate.logger.error(e)
47
- next
48
- end
49
- end
50
- [successfull, failed]
51
- end
52
-
53
- end
54
- end