ubea 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +54 -0
  5. data/.simplecov +5 -0
  6. data/.travis.yml +12 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE.txt +18 -0
  9. data/README.md +20 -0
  10. data/Rakefile +9 -0
  11. data/lib/ubea.rb +7 -0
  12. data/lib/ubea/config.rb +15 -0
  13. data/lib/ubea/currency_converter.rb +112 -0
  14. data/lib/ubea/exchange.rb +11 -0
  15. data/lib/ubea/exchanges/anx_btc_base.rb +41 -0
  16. data/lib/ubea/exchanges/anx_btc_chf.rb +11 -0
  17. data/lib/ubea/exchanges/base.rb +98 -0
  18. data/lib/ubea/exchanges/bit_nz_nzd.rb +45 -0
  19. data/lib/ubea/exchanges/bitcoin_co_id_idr.rb +152 -0
  20. data/lib/ubea/exchanges/bitcoin_de_eur.rb +37 -0
  21. data/lib/ubea/exchanges/bitcurex_eur.rb +36 -0
  22. data/lib/ubea/exchanges/bitstamp_usd.rb +47 -0
  23. data/lib/ubea/exchanges/btc_e_base.rb +130 -0
  24. data/lib/ubea/exchanges/btc_e_eur.rb +15 -0
  25. data/lib/ubea/exchanges/btc_e_usd.rb +15 -0
  26. data/lib/ubea/exchanges/hit_btc_base.rb +148 -0
  27. data/lib/ubea/exchanges/hit_btc_eur.rb +15 -0
  28. data/lib/ubea/exchanges/hit_btc_usd.rb +15 -0
  29. data/lib/ubea/exchanges/kraken_base.rb +176 -0
  30. data/lib/ubea/exchanges/kraken_eur.rb +15 -0
  31. data/lib/ubea/exchanges/kraken_usd.rb +13 -0
  32. data/lib/ubea/log.rb +28 -0
  33. data/lib/ubea/money.rb +146 -0
  34. data/lib/ubea/offer.rb +45 -0
  35. data/lib/ubea/order_book.rb +138 -0
  36. data/spec/shared_examples.rb +14 -0
  37. data/spec/spec_helper.rb +75 -0
  38. data/spec/ubea/exchanges/anx_btc_chf_spec.rb +5 -0
  39. data/spec/ubea/exchanges/bit_nz_nzd_spec.rb +5 -0
  40. data/spec/ubea/exchanges/bitcoin_co_id_idr_spec.rb +5 -0
  41. data/spec/ubea/exchanges/bitstamp_usd_spec.rb +5 -0
  42. data/spec/ubea/exchanges/btc_e_usd_spec.rb +5 -0
  43. data/spec/ubea/exchanges/kraken_eur_spec.rb +5 -0
  44. data/spec/ubea/money_spec.rb +25 -0
  45. data/ubea.gemspec +29 -0
  46. metadata +209 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5b4c982c7157cc44339f73992bf94bf09214fb0f
4
+ data.tar.gz: b062208810ccc755233a748485e50fd9e3531d07
5
+ SHA512:
6
+ metadata.gz: 287cd35d91b40bd66b85abcc59aa5263daea470f4adeabc0b6bfdc67b116e28403b4c7265746dfb499ccb9c850f0c11f45b20137bef538c411c1c6fc18361851
7
+ data.tar.gz: a68f0079c2116e7b4cdec976bf77eb272c325dddf8b3c43538e9d186376694b5fa9c038f72b438ffa07d64c3c43e4eae10687823d981910bdad2bbc86c767a56
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
@@ -0,0 +1,54 @@
1
+ Style/Documentation:
2
+ Enabled: false
3
+
4
+ Style/StringLiterals:
5
+ Enabled: false
6
+
7
+ Style/TrailingComma:
8
+ Enabled: false
9
+
10
+ Style/SignalException:
11
+ Enabled: false
12
+
13
+ Style/ElseAlignment:
14
+ Enabled: false
15
+
16
+ Style/IndentationWidth:
17
+ Enabled: false
18
+
19
+ Lint/EndAlignment:
20
+ Enabled: false
21
+
22
+ Lint/NonLocalExitFromIterator:
23
+ Enabled: false
24
+
25
+ Style/AccessModifierIndentation:
26
+ Enabled: false
27
+
28
+ Style/AndOr:
29
+ Enabled: false
30
+
31
+ Style/RegexpLiteral:
32
+ Enabled: false
33
+
34
+ Style/FormatString:
35
+ Enabled: false
36
+
37
+ Metrics/MethodLength:
38
+ Enabled: false
39
+
40
+ Metrics/AbcSize:
41
+ Enabled: false
42
+
43
+ Metrics/CyclomaticComplexity:
44
+ Enabled: false
45
+
46
+ Metrics/PerceivedComplexity:
47
+ Enabled: false
48
+
49
+ Metrics/ClassLength:
50
+ Max: 200
51
+
52
+ Metrics/LineLength:
53
+ Max: 120
54
+
@@ -0,0 +1,5 @@
1
+ SimpleCov.start do
2
+ add_filter "/spec"
3
+ end
4
+
5
+ # vim: ft=ruby
@@ -0,0 +1,12 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1
4
+ - 2.2
5
+ - ruby-head
6
+ - rbx
7
+ - rbx-head
8
+ matrix:
9
+ allow_failures:
10
+ - rvm: ruby-head
11
+ - rvm: rbx
12
+ - rvm: rbx-head
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ubea.gemspec
4
+ gemspec
@@ -0,0 +1,18 @@
1
+ AGPLv3+ License
2
+
3
+ Ubea
4
+ Copyright (C) 2014-2015 Cédric Félizard
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU Affero General Public License as
8
+ published by the Free Software Foundation, either version 3 of the
9
+ License, or (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU Affero General Public License for more details.
15
+
16
+ You should have received a copy of the GNU Affero General Public License
17
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+
@@ -0,0 +1,20 @@
1
+ # UBEA: Unified Bitcoin Exchange API
2
+
3
+ [![Build Status](https://travis-ci.org/infertux/ubea.svg?branch=master)](https://travis-ci.org/infertux/ubea)
4
+ [![Code Climate](https://codeclimate.com/github/infertux/ubea/badges/gpa.svg)](https://codeclimate.com/github/infertux/ubea)
5
+ [![Dependency Status](https://gemnasium.com/infertux/ubea.svg)](https://gemnasium.com/infertux/ubea)
6
+
7
+ TODO: Write a gem description
8
+
9
+ ## Usage
10
+
11
+ TODO: Write usage instructions here
12
+
13
+ ## Tests
14
+
15
+ `bundle exec rake`
16
+
17
+ ## License
18
+
19
+ AGPLv3+
20
+
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require "rspec/core/rake_task"
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ require "rubocop/rake_task"
7
+ RuboCop::RakeTask.new(:rubocop)
8
+
9
+ task default: [:rubocop, :spec]
@@ -0,0 +1,7 @@
1
+ require "ubea/config"
2
+
3
+ module Ubea
4
+ def self.config
5
+ @config ||= Config.default
6
+ end
7
+ end
@@ -0,0 +1,15 @@
1
+ require "ostruct"
2
+
3
+ module Ubea
4
+ class Config
5
+ def self.default
6
+ config = OpenStruct.new
7
+
8
+ config.debug = false
9
+ config.default_fiat_currency = "USD" # ideally the most used currency so we do as little conversions as possible
10
+ config.exchange_settings = {}
11
+
12
+ config
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,112 @@
1
+ require "retryable"
2
+ require "ubea/log"
3
+
4
+ module Ubea
5
+ class CurrencyConverter
6
+ @rates = {}
7
+
8
+ def self.convert(amount, from, to)
9
+ amount * rate(from, to)
10
+ end
11
+
12
+ def self.rate(from, to)
13
+ pair = [from, to].join
14
+
15
+ if @rates[pair].nil? || @rates[pair].updated_at < Time.now - 10 * 60
16
+
17
+ rate_1 = YahooExchange.get_rate(from, to)
18
+
19
+ rate_2 = begin
20
+ RateExchange.get_rate(from, to)
21
+ rescue OpenURI::HTTPError
22
+ FreeCurrencyConverterAPI.get_rate(from, to)
23
+ end
24
+
25
+ spread = (1.0 - rate_1 / rate_2).abs * 100
26
+ if spread > 1
27
+ raise "#{spread.to_f}% spread for #{pair}: #{rate_1} vs #{rate_2}!"
28
+ end
29
+
30
+ @rates[pair] = OpenStruct.new(
31
+ rate: rate_1,
32
+ updated_at: Time.now
33
+ ).freeze
34
+
35
+ end
36
+
37
+ @rates[pair].rate
38
+ end
39
+
40
+ class RateExchange
41
+ URL = "http://rate-exchange.appspot.com/currency?from=%s&to=%s".freeze
42
+
43
+ def self.get_rate(from, to)
44
+ url = URL % [from, to]
45
+ json = get_json(url)
46
+ rate = json["rate"].to_s
47
+ BigDecimal.new(rate)
48
+ end
49
+
50
+ def self.get_json(url)
51
+ Retryable.retryable(tries: 3, sleep: 1) do
52
+ json = open(url).read
53
+ JSON.parse(json)
54
+ end
55
+ end
56
+ end
57
+
58
+ class FreeCurrencyConverterAPI
59
+ URL = "http://www.freecurrencyconverterapi.com/api/v2/convert?q=%s_%s&compact=y".freeze
60
+
61
+ def self.get_rate(from, to)
62
+ url = URL % [from, to]
63
+ string = get_json(url, from, to)
64
+ rate = BigDecimal.new(string)
65
+
66
+ if rate <= 1E-3
67
+ Log.warn "Cannot retrieve exchange rate for #{from}#{to}, not enough precision, using the opposite pair"
68
+
69
+ url = URL % [to, from]
70
+ string = get_json(url, to, from)
71
+ rate = BigDecimal.new(1) / BigDecimal.new(string)
72
+ end
73
+
74
+ rate
75
+ end
76
+
77
+ def self.get_json(url, from, to)
78
+ Retryable.retryable(tries: 3, sleep: 1) do
79
+ json = open(url).read
80
+ JSON.parse(json)["#{from}_#{to}"]["val"].to_s
81
+ end
82
+ end
83
+ end
84
+
85
+ class YahooExchange
86
+ URL = "http://download.finance.yahoo.com/d/quotes.csv?s=%s%s=X&f=sl1d1&e=.csv".freeze
87
+
88
+ def self.get_rate(from, to)
89
+ url = URL % [from, to]
90
+ csv = get_csv(url)
91
+ rate = BigDecimal.new(csv[1].to_s)
92
+
93
+ if rate <= 1E-3
94
+ Log.warn "Cannot retrieve exchange rate for #{from}#{to}, not enough precision, using the opposite pair"
95
+
96
+ url = URL % [to, from]
97
+ csv = get_csv(url)
98
+ rate = BigDecimal.new(1) / BigDecimal.new(csv[1].to_s)
99
+ end
100
+
101
+ rate
102
+ end
103
+
104
+ def self.get_csv(url)
105
+ Retryable.retryable(tries: 3, sleep: 1) do
106
+ csv = open(url).read
107
+ csv.split(",")
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,11 @@
1
+ module Ubea
2
+ module Exchange
3
+ def self.find(exchange_id)
4
+ require "ubea/exchanges/#{exchange_id}"
5
+
6
+ klass_name = exchange_id.capitalize.gsub(/_(.)/) { Regexp.last_match[1].capitalize }
7
+ klass = Object.const_get("Ubea::Exchange::#{klass_name}")
8
+ klass.new
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,41 @@
1
+ require "ubea/exchanges/base"
2
+
3
+ module Ubea
4
+ module Exchange
5
+ class AnxBtcBase < Base
6
+ def name
7
+ "ANXBTC (#{fiat_currency})"
8
+ end
9
+
10
+ def trade_fee
11
+ BigDecimal.new("0.005").freeze # 0.5%??? - see https://anxbtc.com/faq#tab1
12
+ end
13
+
14
+ def refresh_order_book!
15
+ json = get_json("https://anxpro.com/api/2/BTC#{fiat_currency}/money/depth/full") or return
16
+
17
+ asks = format_asks_bids(json["data"]["asks"])
18
+ bids = format_asks_bids(json["data"]["bids"])
19
+
20
+ mark_as_refreshed
21
+ @order_book = OrderBook.new(asks: asks, bids: bids)
22
+ end
23
+
24
+ private
25
+
26
+ def format_asks_bids(json)
27
+ json.map do |tuple|
28
+ price = tuple["price"]
29
+ volume = tuple["amount"]
30
+ price_chf = Money.new(price, fiat_currency)
31
+ price_normalized = price_chf.exchange_to(Ubea.config.default_fiat_currency)
32
+
33
+ Offer.new(
34
+ price: price_normalized,
35
+ volume: volume
36
+ ).freeze
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,11 @@
1
+ require "ubea/exchanges/anx_btc_base"
2
+
3
+ module Ubea
4
+ module Exchange
5
+ class AnxBtcChf < AnxBtcBase
6
+ def fiat_currency
7
+ "CHF"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,98 @@
1
+ require "open-uri"
2
+ require "json"
3
+ require "base64"
4
+ require "addressable/uri"
5
+ require "retryable"
6
+ require "faraday"
7
+ require "net/protocol"
8
+ require "zlib"
9
+ require "ubea"
10
+ require "ubea/log"
11
+ require "ubea/money"
12
+ require "ubea/offer"
13
+ require "ubea/order_book"
14
+
15
+ module Ubea
16
+ module Exchange
17
+ class Base
18
+ attr_reader :order_book, :updated_at, :last_rtt
19
+
20
+ def id
21
+ @id ||= begin
22
+ self.class.to_s.gsub(/.*::/, "").gsub(/([A-Z])/, "_\\1").gsub(/\A_/, "").downcase
23
+ end
24
+ end
25
+
26
+ alias_method :to_param, :id
27
+
28
+ def short_name
29
+ @short_name ||= self.class.to_s.gsub(/.*::/, "")
30
+ end
31
+
32
+ alias_method :to_s, :short_name
33
+
34
+ def name
35
+ raise NotImplementedError, to_s
36
+ end
37
+
38
+ def trade_fee
39
+ raise NotImplementedError, to_s
40
+ end
41
+
42
+ def balance
43
+ raise NotImplementedError, to_s
44
+ end
45
+
46
+ protected
47
+
48
+ def retryable_http_request
49
+ exceptions = [
50
+ Faraday::ConnectionFailed,
51
+ Faraday::TimeoutError,
52
+ Faraday::SSLError,
53
+ Net::ReadTimeout,
54
+ OpenURI::HTTPError,
55
+ Zlib::BufError,
56
+ ].freeze
57
+
58
+ Retryable.retryable(tries: 5, sleep: 1, on: exceptions) do
59
+ yield
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def http_adapter(base_uri)
66
+ @http ||= Faraday.new(url: base_uri) do |faraday|
67
+ faraday.request :url_encoded
68
+ faraday.adapter :net_http
69
+ # faraday.response :logger # log requests to STDOUT
70
+ end
71
+ end
72
+
73
+ def get_html(url)
74
+ Retryable.retryable(tries: 5, sleep: 1) do
75
+ open(url).read
76
+ end
77
+ end
78
+
79
+ def get_json(url)
80
+ html = get_html(url) or return
81
+
82
+ begin
83
+ JSON.parse(html)
84
+ rescue JSON::ParserError
85
+ nil
86
+ end
87
+ end
88
+
89
+ def mark_as_refreshed
90
+ now = Time.now.utc
91
+ @last_rtt = now - updated_at if updated_at
92
+ @updated_at = now
93
+
94
+ Log.debug "Order book for #{self} has been refreshed"
95
+ end
96
+ end
97
+ end
98
+ end