currency-rate 1.7.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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