crypto_arbitrer 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.swo
19
+ *.swp
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - "1.9.3"
4
+ script: bundle exec rspec spec
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in crypto_arbitrer.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Nubis
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,83 @@
1
+ ![Travis Build Status](https://secure.travis-ci.org/nubis/crypto_arbitrer.png)
2
+
3
+ # CryptoArbitrer
4
+
5
+ Provides currency conversions across several fiat currencies and crypto currencies.
6
+ Exchange rates are fetch (and in some cases scraped)
7
+ from mtgox.com, btc-e.com, eldolarblue.net, rate-exchange.appspot.com and dolarparalelo.org.
8
+
9
+ Given some particular situations in both Venezuela and Argentina, an unofficial but de-facto exchange rate
10
+ is used for each country's currency (from dolarparalelo.org and eldolarblue.net respectively).
11
+
12
+ The supported fiat currencies are:
13
+ usd ars uyu brl clp sgd eur vef
14
+
15
+ The supported crypto currencies are:
16
+ btc ltc nmc nvc trc ppc ftc cnc
17
+
18
+ If you're just looking to use these rates quickly, they are available as a JSON api at [http://cryptocueva.com](http://cryptocueva.com)
19
+
20
+ ## Installation
21
+
22
+ Add this line to your application's Gemfile:
23
+
24
+ gem 'crypto_arbitrer'
25
+
26
+ And then execute:
27
+
28
+ $ bundle
29
+
30
+ Or install it yourself as:
31
+
32
+ $ gem install crypto_arbitrer
33
+
34
+ ## Usage
35
+
36
+ To get the exchange rate from United States Dollars to Argentine Pesos:
37
+
38
+ irb > CryptoArbitrer::Base.fetch('usd', 'ars')
39
+ => {'sell' => 7.9, 'buy' => 8.0}
40
+
41
+ Prices are returned as 'sell' and 'buy' hashes, where 'sell' is the price at which you can sell the given currency (highest bid price) and 'buy' is the price at which is being offered to you by others (lowest ask price).
42
+
43
+ If you were to buy 10 Bitcoin paying with US Dollars, this is how much you would spend in US Dollars.
44
+
45
+ irb > 10 * CryptoArbitrer::Base.fetch('btc', 'usd')['buy']
46
+ => 1107.7001 # USD
47
+
48
+
49
+ ### Caching and Rails
50
+
51
+ There is basic support for plugging in your own caching mechanism. Caching in Rails is as easy as creating an initializer
52
+ with the following code:
53
+
54
+ CryptoArbitrer::Base.cache_backend = lambda do |from, to, block|
55
+ Rails.cache.fetch([from, to], &block)
56
+ end
57
+
58
+ Essentially, you just make a lambda that would receive the currencies to convert from and to, and a block that would return the
59
+ exchange rate when called.
60
+
61
+ Keeping an updated cache is also rather simple, I have this short rake task running every 10 minutes:
62
+
63
+ namespace :exchange_rates do
64
+ desc "Recaches the exchange rates"
65
+ task recache: :environment do
66
+ CryptoArbitrer::Base.supported_conversions.each do |from, to|
67
+ # Notice the third argument to 'fetch', it forces the lookup, ignoring the existing cache.
68
+ rate = CryptoArbitrer::Base.fetch(from, to, true) rescue next
69
+ Rails.cache.delete([from, to])
70
+ Rails.cache.write([from, to], rate)
71
+ end
72
+ end
73
+ end
74
+
75
+ Docs available at: [http://rubydoc.info/github/nubis/crypto_arbitrer/master/frames](http://rubydoc.info/github/nubis/crypto_arbitrer/master/frames)
76
+
77
+ ## Contributing
78
+
79
+ 1. Fork it
80
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
81
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
82
+ 4. Push to the branch (`git push origin my-new-feature`)
83
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'crypto_arbitrer/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "crypto_arbitrer"
8
+ spec.version = CryptoArbitrer::VERSION
9
+ spec.authors = ["Nubis"]
10
+ spec.email = ["yo@nubis.im"]
11
+ spec.description = "Arbitrage calculator for crypto currencies. Currently focused on Argentina moving funds from btc-e to mt.gox, then into an asian bank account and finally into Argentina as USD which may be sold at the unofficial price."
12
+ spec.summary = "Argentine arbitrage calculator for crypto currencies"
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "json"
22
+ spec.add_development_dependency "bundler", "~> 1.3"
23
+ spec.add_development_dependency "rspec"
24
+ spec.add_development_dependency "rake"
25
+ spec.add_development_dependency "webmock"
26
+ end
@@ -0,0 +1,174 @@
1
+ require "crypto_arbitrer/version"
2
+ require "json"
3
+ require "open-uri"
4
+
5
+ module CryptoArbitrer
6
+ class Base
7
+ # Quasi constant, all supported fiat currencies.
8
+ def self.supported_fiat
9
+ %w(usd ars uyu brl clp sgd eur vef)
10
+ end
11
+
12
+ # Quasi constant, all supported crypto currencies.
13
+ def self.supported_cryptos
14
+ %w(btc ltc nmc nvc trc ppc ftc cnc)
15
+ end
16
+
17
+ # Quasi constant, all supported currencies.
18
+ def self.supported_currencies
19
+ supported_fiat + supported_cryptos
20
+ end
21
+
22
+ # Quasi constant, a list of iso code pairs representing all supported exchange rates.
23
+ # [['ars','usd'],['usd','btc'],['ltc','cnc'],...]
24
+ def self.supported_conversions
25
+ supported_currencies.product(supported_currencies)
26
+ end
27
+
28
+ class << self
29
+ # If you want to cache calls done to third party api's you should use this.
30
+ # Pass in a lambda that will receive the cache key name and the block for fetching it's value.
31
+ # If you're using rails you can configure CryptoArbitrer in an initializer to use rails caching
32
+ # just forwarding the cache key and the block to it.
33
+ # You can set the cache_backend to nil to prevent caching (not recommended, unless you're testing)
34
+ def cache_backend=(backend)
35
+ @cache_backend = backend
36
+ end
37
+
38
+ def cache_backend
39
+ @cache_backend
40
+ end
41
+ end
42
+
43
+ # For existing known exchange rates, we want to derive the reverse lookup,
44
+ # for example: We derive ars_usd from usd_ars
45
+ def self.derive_reversal(from, to)
46
+ define_method("fetch_#{to}_#{from}") do
47
+ rate = send("fetch_#{from}_#{to}")
48
+ {'sell' => 1/rate['sell'], 'buy' => 1/rate['buy']}
49
+ end
50
+ end
51
+
52
+ # Uses eldolarblue.net API for checking Argentine unofficial US dolar prices.
53
+ # The returned hash has 'sell' and 'buy' keys for the different prices.
54
+ # 'buy' is the price at which you can buy USD from agents and 'sell' is the price
55
+ # at which you can sell USD to agents. Notice this is the opposite to argentina's
56
+ # convention for 'buy' and 'sell' (where buy is the price in which agents would buy from you)
57
+ # The ugly regex is to fix the service's response which is
58
+ # not valid json but a javascript object literal.
59
+ def fetch_usd_ars
60
+ response = open('http://www.eldolarblue.net/getDolarBlue.php?as=json').read
61
+ rate = JSON.parse(response.gsub(/([{,])([^:]*)/, '\1"\2"') )['exchangerate']
62
+ {'sell' => rate['buy'], 'buy' => rate['sell']}
63
+ end
64
+ derive_reversal(:usd, :ars)
65
+
66
+ # Uses mt.gox API for checking latest bitcoin sell and buy prices.
67
+ # The returned hash has 'sell' and 'buy' keys for the different prices,
68
+ # the prices mean at which price you could buy and sell from them, respectively.
69
+ def fetch_btc_usd
70
+ response = open('http://data.mtgox.com/api/2/BTCUSD/money/ticker_fast').read
71
+ json = JSON.parse(response)['data']
72
+ {'sell' => json['sell']['value'].to_f, 'buy' => json['buy']['value'].to_f}
73
+ end
74
+ derive_reversal(:btc, :usd)
75
+
76
+ # Goes to dolarparalelo.org to grab the actual price for usd to vef
77
+ # The returned hash has 'sell' and 'buy' keys for the different prices,
78
+ # the prices mean at which price you could buy and sell from them, respectively.
79
+ def fetch_usd_vef
80
+ response = open('http://www.dolarparalelo.org').read
81
+ response =~ /<p><font.*?>Dolar:<\/font>.*?<font.*?>(.*?)</
82
+ rate = $1.to_f
83
+ {'sell' => rate, 'buy' => rate}
84
+ end
85
+ derive_reversal(:usd, :vef)
86
+
87
+ # All prices for non-btc cryptocurrencies are fetch from btc-e, prices are expressed in BTC.
88
+ # We do that by parsing their pages since they don't provide an API.
89
+ # The prices returned mean at which price you could buy and sell from them, respectively.
90
+ # We don't use btc-e pricing for bitcoin since the common arbitrage path is to
91
+ # move bitcoin from btc-e to mt.gox when converting any cryptocurrency to fiat.
92
+ non_btc_cryptos = supported_cryptos - ['btc']
93
+ non_btc_cryptos.each do |currency|
94
+ define_method("fetch_#{currency}_btc") do
95
+ response = open("https://btc-e.com/exchange/#{currency}_btc").read
96
+ response =~ /<span id=.max_price..(.*?)..span./
97
+ sell = $1.to_f
98
+ response =~ /<span id=.min_price..(.*?)..span./
99
+ buy = $1.to_f
100
+ {'sell' => sell, 'buy' => buy}
101
+ end
102
+ derive_reversal(currency, :btc)
103
+ end
104
+
105
+ # Fiat prices are fetch from rate-exchange.appspot.com but there are no buy/sell prices.
106
+ # We use USD as the common denominator for all fiat currencies.
107
+ # @returns [{'sell' => Float, 'buy' => Float}]
108
+ %w(uyu brl clp sgd eur).each do |currency|
109
+ define_method("fetch_usd_#{currency}") do
110
+ response = open("http://rate-exchange.appspot.com/currency?from=usd&to=#{currency}").read
111
+ json = JSON.parse(response)
112
+ {'sell' => json['rate'].to_f, 'buy' => json['rate'].to_f}
113
+ end
114
+ derive_reversal(:usd, currency)
115
+ end
116
+
117
+ # All non usd fiat can be converted to btc through their dollar price.
118
+ non_usd_fiat = supported_fiat - ['usd']
119
+ non_usd_fiat.each do |currency|
120
+ define_method("fetch_btc_#{currency}") do
121
+ btc_rate = fetch_btc_usd
122
+ usd_rate = fetch('usd', currency)
123
+ {'sell' => btc_rate['sell'] * usd_rate['sell'], 'buy' => btc_rate['buy'] * usd_rate['buy']}
124
+ end
125
+ derive_reversal(:btc, currency)
126
+ end
127
+
128
+ # All fiat currencies can be converted to any non-btc cryptocurrency by using their
129
+ # rate to btc as common denominator.
130
+ non_usd_to_non_btc = (non_usd_fiat + non_btc_cryptos).product(non_usd_fiat + non_btc_cryptos)
131
+ usd_to_non_btc = (non_btc_cryptos+[:usd]).product(non_btc_cryptos+[:usd])
132
+ (non_usd_to_non_btc + usd_to_non_btc).each do |from, to|
133
+ define_method("fetch_#{from}_#{to}") do
134
+ from_rate = fetch(from, 'btc')
135
+ to_rate = fetch(to, 'btc')
136
+ {'sell' => from_rate['sell']/to_rate['sell'], 'buy' => from_rate['buy']/to_rate['buy']}
137
+ end
138
+ end
139
+
140
+ # From and to the same currency is always 1, but we include the methods just for robustness
141
+ supported_currencies.each do |c|
142
+ define_method("fetch_#{c}_#{c}"){ {'sell' => 1, 'buy' => 1 } }
143
+ end
144
+
145
+ def fetch(from, to, force = false)
146
+ if force
147
+ send("fetch_#{from}_#{to}")
148
+ else
149
+ cached(from, to){ send("fetch_#{from}_#{to}") }
150
+ end
151
+ end
152
+
153
+ # Fetch a given conversion caching the result if a backend is set.
154
+ # This should be the preferred way to fetch a conversion by users.
155
+ # Check {#supported_currencies} for a list of possible values for 'from' and 'to'
156
+ # @param from [String] a three letter currency code to convert from.
157
+ # @param to [String] a three letter currency code to convert to.
158
+ # @param to [String] a three letter currency code to convert to.
159
+ # @param force [Boolean] Ignore the cache (does not read from it, and does not write to it). Defaults to false.
160
+ # @return [{'buy' => Float, 'sell' => Float}] The buy and sell prices
161
+ def self.fetch(from,to, force = false)
162
+ new.fetch(from, to, force)
163
+ end
164
+
165
+ protected
166
+ def cached(from, to, &block)
167
+ if self.class.cache_backend
168
+ self.class.cache_backend.call(from, to, block)
169
+ else
170
+ block.call
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,3 @@
1
+ module CryptoArbitrer
2
+ VERSION = "0.0.2"
3
+ end
data/spec/base_spec.rb ADDED
@@ -0,0 +1,172 @@
1
+ require_relative '../lib/crypto_arbitrer.rb'
2
+ require 'webmock/rspec'
3
+ require 'csv'
4
+
5
+ describe CryptoArbitrer::Base do
6
+ def stub_get(url, mock_name)
7
+ stub_request(:get, url)
8
+ .to_return(body: open(File.expand_path("../mocks/#{mock_name}", __FILE__)).read)
9
+ end
10
+
11
+ describe 'when matching corpus of conversion rates' do
12
+ before :each do
13
+ stub_get("http://www.eldolarblue.net/getDolarBlue.php?as=json", 'dolarblue.json')
14
+ stub_get('http://data.mtgox.com/api/2/BTCUSD/money/ticker_fast', 'mtgox.json')
15
+ stub_get('http://www.dolarparalelo.org/', 'dolarparalelo.html')
16
+ %w(ltc nmc nvc trc ppc ftc cnc).each do |crypto|
17
+ stub_get("https://btc-e.com/exchange/#{crypto}_btc", "btce_#{crypto}_btc.html")
18
+ end
19
+ %w(uyu brl clp sgd eur).each do |currency|
20
+ stub_get("http://rate-exchange.appspot.com/currency?from=usd&to=#{currency}",
21
+ "rate_exchange_usd_#{currency}.json")
22
+ end
23
+ end
24
+
25
+ CSV.read(File.expand_path('../conversions.csv', __FILE__), 'r').each do |from, to, buy, sell|
26
+ it "converts from #{from} to #{to}" do
27
+ rate = subject.fetch(from, to)
28
+ rate['buy'].should == buy.to_f
29
+ rate['sell'].should == sell.to_f
30
+ end
31
+ end
32
+
33
+ # As a sanity check, we make sure all conversions advertised as supported at least exists.
34
+ # You should always increase the conversions.csv corpus when adding a new currency though.
35
+ CryptoArbitrer::Base.supported_conversions.each do |from, to|
36
+ it "Conversion from #{from} to #{to} exists" do
37
+ subject.send("fetch_#{from}_#{to}").tap do |rate|
38
+ # When adding a new currency, you may want to print out the rates returned by the engine
39
+ # to check them manually and then add them to the corpus
40
+ #puts "#{from},#{to},#{rate['buy']},#{rate['sell']}"
41
+ rate['buy'].should_not be_nil
42
+ rate['sell'].should_not be_nil
43
+ end
44
+ end
45
+ end
46
+
47
+ it 'supports conversions from and to the same currency' do
48
+ subject.class.supported_currencies.each do |c|
49
+ subject.fetch(c,c).should == {'buy' => 1, 'sell' => 1}
50
+ end
51
+ end
52
+ end
53
+
54
+ it 'hits dolarblue for usd to ars rate' do
55
+ stub = stub_get("http://www.eldolarblue.net/getDolarBlue.php?as=json", 'dolarblue.json')
56
+ subject.fetch_usd_ars
57
+ stub.should have_been_requested
58
+ end
59
+
60
+ it 'hits mt.gox for the btc to usd rate' do
61
+ stub = stub_get('http://data.mtgox.com/api/2/BTCUSD/money/ticker_fast', 'mtgox.json')
62
+ subject.fetch_btc_usd
63
+ stub.should have_been_requested
64
+ end
65
+
66
+ it 'hits dolarparalelo for venezuelan bolivar' do
67
+ stub = stub_get('http://www.dolarparalelo.org/', 'dolarparalelo.html')
68
+ subject.fetch_usd_vef
69
+ stub.should have_been_requested
70
+ end
71
+
72
+ it 'hits mt.gox and dolarblue for the btc to ars rate' do
73
+ stub_ars = stub_get("http://www.eldolarblue.net/getDolarBlue.php?as=json", 'dolarblue.json')
74
+ stub_btc = stub_get('http://data.mtgox.com/api/2/BTCUSD/money/ticker_fast', 'mtgox.json')
75
+ subject.fetch_btc_ars
76
+ stub_ars.should have_been_requested
77
+ stub_btc.should have_been_requested
78
+ end
79
+
80
+ %w(ltc nmc nvc trc ppc ftc cnc).each do |currency|
81
+ it "hits btc-e for the #{currency} to btc rate" do
82
+ stub = stub_get("https://btc-e.com/exchange/#{currency}_btc", "btce_#{currency}_btc.html")
83
+ subject.fetch(currency, 'btc')
84
+ subject.fetch('btc', currency)
85
+ stub.should have_been_requested.twice
86
+ end
87
+ end
88
+
89
+ %w(uyu brl clp sgd eur).each do |currency|
90
+ it "hits rate-exchange for the #{currency} to usd rate" do
91
+ stub = stub_get("http://rate-exchange.appspot.com/currency?from=usd&to=#{currency}",
92
+ "rate_exchange_usd_#{currency}.json")
93
+ subject.fetch('usd', currency)
94
+ subject.fetch(currency, 'usd')
95
+ stub.should have_been_requested.twice
96
+ end
97
+ end
98
+
99
+ describe 'when caching remote calls' do
100
+ it 'does not cache anything by default' do
101
+ stub = stub_get("http://www.eldolarblue.net/getDolarBlue.php?as=json", 'dolarblue.json')
102
+ 2.times{ subject.fetch('usd', 'ars') }
103
+ stub.should have_been_requested.twice
104
+ end
105
+
106
+ it 'caches when a cache_backend has been set, stops caching afterwards' do
107
+ cache = {}
108
+ subject.class.cache_backend = lambda {|from, to, block| cache[[from,to]] ||= block.call }
109
+ stub = stub_get('http://data.mtgox.com/api/2/BTCUSD/money/ticker_fast', 'mtgox.json')
110
+ subject.fetch('btc', 'usd')
111
+ cache[['btc','usd']].should be_a(Hash)
112
+ subject.fetch('btc', 'usd')
113
+ stub.should have_been_requested.once
114
+ subject.class.cache_backend = nil
115
+ subject.fetch('btc', 'usd')
116
+ stub.should have_been_requested.twice
117
+ end
118
+
119
+ it 'does not cache for low level calls' do
120
+ cache = {}
121
+ subject.class.cache_backend = lambda {|from, to, block| cache[key] ||= block.call }
122
+ stub = stub_get('http://data.mtgox.com/api/2/BTCUSD/money/ticker_fast', 'mtgox.json')
123
+ subject.fetch_btc_usd
124
+ subject.fetch_btc_usd
125
+ stub.should have_been_requested.twice
126
+ end
127
+
128
+ it 'does not cache when using a force argument' do
129
+ cache = {}
130
+ subject.class.cache_backend = lambda {|from, to, block| cache[key] ||= block.call }
131
+ stub = stub_get('http://data.mtgox.com/api/2/BTCUSD/money/ticker_fast', 'mtgox.json')
132
+ subject.fetch('btc', 'usd', true)
133
+ subject.fetch('btc', 'usd', true)
134
+ stub.should have_been_requested.twice
135
+ end
136
+ end
137
+
138
+ describe "Service is either down or changed their API at:", slow: true do
139
+ before(:all){ WebMock.allow_net_connect! }
140
+ after(:all){ WebMock.disable_net_connect! }
141
+
142
+ def check_service(method)
143
+ %w(buy sell).each do |price|
144
+ price = subject.send(method)[price]
145
+ price.should be_a Float
146
+ price.should >= 0
147
+ end
148
+ end
149
+
150
+ it 'eldolarblue' do
151
+ check_service(:fetch_usd_ars)
152
+ end
153
+
154
+ it 'mtgox' do
155
+ check_service(:fetch_btc_usd)
156
+ end
157
+
158
+ %w(ltc_btc nmc_btc nvc_btc trc_btc ppc_btc ftc_btc cnc_btc).each do |exchange|
159
+ it "btce #{exchange}" do
160
+ check_service("fetch_#{exchange}")
161
+ end
162
+ end
163
+
164
+ it 'rate-exchange' do
165
+ check_service('fetch_usd_sgd')
166
+ end
167
+
168
+ it 'dolarparalelo' do
169
+ check_service('fetch_usd_vef')
170
+ end
171
+ end
172
+ end