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.
- 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
|