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.
- checksums.yaml +4 -4
- data/.gitignore +1 -1
- data/lib/adapter.rb +53 -24
- data/lib/adapters/crypto/binance_adapter.rb +1 -1
- data/lib/adapters/crypto/bitfinex_adapter.rb +1 -1
- data/lib/adapters/crypto/bitpay_adapter.rb +1 -1
- data/lib/adapters/crypto/bitstamp_adapter.rb +1 -1
- data/lib/adapters/crypto/coin_market_cap_adapter.rb +2 -4
- data/lib/adapters/crypto/coinbase_adapter.rb +3 -3
- data/lib/adapters/crypto/exmo_adapter.rb +1 -1
- data/lib/adapters/crypto/hit_BTC_adapter.rb +64 -0
- data/lib/adapters/crypto/huobi_adapter.rb +1 -1
- data/lib/adapters/crypto/kraken_adapter.rb +1 -1
- data/lib/adapters/crypto/localbitcoins_adapter.rb +1 -1
- data/lib/adapters/crypto/okcoin_adapter.rb +4 -6
- data/lib/adapters/crypto/paxful_adapter.rb +1 -1
- data/lib/adapters/crypto/poloniex_adapter.rb +1 -1
- data/lib/adapters/crypto/yadio_adapter.rb +1 -1
- data/lib/adapters/fiat/bonbast_adapter.rb +3 -5
- data/lib/adapters/fiat/coinmonitor_adapter.rb +4 -5
- data/lib/adapters/fiat/currency_layer_adapter.rb +2 -3
- data/lib/adapters/fiat/fixer_adapter.rb +3 -4
- data/lib/adapters/fiat/forge_adapter.rb +15 -6
- data/lib/adapters/fiat/free_forex_adapter.rb +7 -3
- data/lib/container.rb +203 -0
- data/lib/currency_rate.rb +19 -63
- data/lib/currency_rate/version.rb +1 -1
- data/lib/storage/file_storage.rb +14 -9
- data/lib/utils/string_extensions.rb +19 -0
- metadata +6 -11
- data/api_keys.yml.sample +0 -4
- data/lib/adapters/crypto/btc_china_adapter.rb +0 -11
- data/lib/adapters/crypto/btc_e_adapter.rb +0 -18
- data/lib/adapters/crypto/hit_btc_adapter.rb +0 -61
- data/lib/adapters/fiat/yahoo_adapter.rb +0 -35
- data/lib/configuration.rb +0 -28
- data/lib/fetcher.rb +0 -80
- data/lib/synchronizer.rb +0 -54
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e33cf57e2389c7faea732bc6a2c1005d1373daa26622d77d07058a157cb408b4
|
4
|
+
data.tar.gz: ecdcdf02e7478deb612bebe5fed48c3d46d99916f9f25b4e0666bcf88ed4c1f9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d6d71b129e93052a97d6e27749531c16818e19eeb1f7b44b6889d67683543094eea7c9cf59f7dc6dae01e1e6e0f570c0709c06d3c786b7c7d275f1b4e60c6e65
|
7
|
+
data.tar.gz: 21cd15e77e63eb8df45f4b52904b7c9ac3fb0e0a93838a3373c31165af5a7d115c6982b2e8cdf869898ed24dbe07008bcc817b9464ca0510717cf5a92e359844
|
data/.gitignore
CHANGED
data/lib/adapter.rb
CHANGED
@@ -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
|
-
|
10
|
-
|
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
|
28
|
+
@rates = normalize(exchange_data)
|
16
29
|
rescue StandardError => e
|
17
|
-
|
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
|
-
|
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
|
-
|
44
|
-
CurrencyRate.logger.error(e)
|
56
|
+
@container.log(:error, e)
|
45
57
|
nil
|
46
58
|
end
|
47
59
|
end
|
48
60
|
|
49
|
-
def
|
50
|
-
|
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
|
-
|
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
|
-
|
58
|
-
|
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:
|
61
|
-
|
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(
|
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(
|
22
|
-
return nil unless super
|
23
|
-
|
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
|
@@ -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/
|
5
|
-
'BTC_USD' => 'https://www.okcoin.com/api/
|
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["
|
11
|
+
result[pair] = BigDecimal(value["last"].to_s)
|
14
12
|
result
|
15
13
|
end
|
16
14
|
end
|
@@ -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)
|
@@ -12,13 +12,11 @@ module CurrencyRate
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def request(url)
|
15
|
-
|
16
|
-
|
17
|
-
|
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/
|
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://
|
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,
|
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
|
-
|
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
|
-
|
9
|
-
|
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
|
-
|
25
|
+
@container.logger.error("Forge exchange returned error")
|
17
26
|
return nil
|
18
27
|
end
|
19
|
-
rates[rate["
|
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{|
|
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
|
data/lib/container.rb
ADDED
@@ -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
|
data/lib/currency_rate.rb
CHANGED
@@ -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 "
|
11
|
-
require_relative "
|
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
|
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
|
-
|
22
|
-
|
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
|
-
|
30
|
-
|
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
|
-
|
75
|
-
|
76
|
-
|
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
|
data/lib/storage/file_storage.rb
CHANGED
@@ -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
|
12
|
-
|
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
|
-
|
20
|
+
@container.log(:error, e)
|
16
21
|
nil
|
17
22
|
end
|
18
23
|
|
19
|
-
def write(
|
20
|
-
File.write path_for(
|
24
|
+
def write(adapter_name, data = "")
|
25
|
+
File.write path_for(adapter_name), @serializer.serialize(data)
|
21
26
|
rescue StandardError => e
|
22
|
-
|
27
|
+
@container.log(:error, e)
|
23
28
|
end
|
24
29
|
|
25
|
-
def path_for(
|
26
|
-
File.join @path, "#{
|
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:
|
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-
|
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/
|
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/
|
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/
|
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.
|
148
|
+
rubygems_version: 3.1.2
|
154
149
|
signing_key:
|
155
150
|
specification_version: 4
|
156
151
|
summary: Converter for fiat and crypto currencies
|
data/api_keys.yml.sample
DELETED
@@ -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
|
data/lib/configuration.rb
DELETED
@@ -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
|
data/lib/fetcher.rb
DELETED
@@ -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
|
-
|
data/lib/synchronizer.rb
DELETED
@@ -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
|