require "crypto_arbitrer/version" require "crypto_arbitrer/spec_helper" require "json" require "open-uri" module CryptoArbitrer class Base def self.btc_usd_strategy @btc_usd_strategy || :mtgox end def self.btc_usd_strategy=(strategy) @btc_usd_strategy = strategy end # Quasi constant, all supported fiat currencies. def self.supported_fiat %w(usd ars uyu brl clp sgd eur vef) end # Quasi constant, all supported crypto currencies. def self.supported_cryptos %w(btc ltc nmc nvc trc ppc ftc cnc) end # Quasi constant, all supported currencies. def self.supported_currencies supported_fiat + supported_cryptos end # Quasi constant, a list of iso code pairs representing all supported exchange rates. # [['ars','usd'],['usd','btc'],['ltc','cnc'],...] def self.supported_conversions supported_currencies.product(supported_currencies) end class << self # If you want to cache calls done to third party api's you should use this. # Pass in a lambda that will receive the cache key name and the block for fetching it's value. # If you're using rails you can configure CryptoArbitrer in an initializer to use rails caching # just forwarding the cache key and the block to it. # You can set the cache_backend to nil to prevent caching (not recommended, unless you're testing) def cache_backend=(backend) @cache_backend = backend end def cache_backend @cache_backend end end # For existing known exchange rates, we want to derive the reverse lookup, # for example: We derive ars_usd from usd_ars def self.derive_reversal(from, to) define_method("fetch_#{to}_#{from}") do rate = send("fetch_#{from}_#{to}") {'sell' => 1/rate['sell'], 'buy' => 1/rate['buy']} end end # Uses eldolarblue.net API for checking Argentine unofficial US dolar prices. # The returned hash has 'sell' and 'buy' keys for the different prices. # 'buy' is the price at which you can buy USD from agents and 'sell' is the price # at which you can sell USD to agents. Notice this is the opposite to argentina's # convention for 'buy' and 'sell' (where buy is the price in which agents would buy from you) # The ugly regex is to fix the service's response which is # not valid json but a javascript object literal. def fetch_usd_ars response = open('http://www.eldolarblue.net/getDolarBlue.php?as=json').read rate = JSON.parse(response.gsub(/([{,])([^:]*)/, '\1"\2"') )['exchangerate'] {'sell' => rate['buy'], 'buy' => rate['sell']} end derive_reversal(:usd, :ars) # Uses mt.gox API for checking latest bitcoin sell and buy prices. # The returned hash has 'sell' and 'buy' keys for the different prices, # the prices mean at which price you could buy and sell from them, respectively. def fetch_btc_usd if self.class.btc_usd_strategy == :mtgox response = open('http://data.mtgox.com/api/2/BTCUSD/money/ticker_fast').read json = JSON.parse(response)['data'] {'sell' => json['sell']['value'].to_f, 'buy' => json['buy']['value'].to_f} else parse_btce_prices open("https://btc-e.com/exchange/btc_usd").read end end derive_reversal(:btc, :usd) # Goes to dolarparalelo.org to grab the actual price for usd to vef # The returned hash has 'sell' and 'buy' keys for the different prices, # the prices mean at which price you could buy and sell from them, respectively. def fetch_usd_vef response = open('http://www.dolarparalelo.org').read response =~ /

Dolar:<\/font>.*?(.*?) rate, 'buy' => rate} end derive_reversal(:usd, :vef) # All prices for non-btc cryptocurrencies are fetch from btc-e, prices are expressed in BTC. # We do that by parsing their pages since they don't provide an API. # The prices returned mean at which price you could buy and sell from them, respectively. # We don't use btc-e pricing for bitcoin since the common arbitrage path is to # move bitcoin from btc-e to mt.gox when converting any cryptocurrency to fiat. non_btc_cryptos = supported_cryptos - ['btc'] non_btc_cryptos.each do |currency| define_method("fetch_#{currency}_btc") do parse_btce_prices open("https://btc-e.com/exchange/#{currency}_btc").read end derive_reversal(currency, :btc) end def parse_btce_prices(response) response =~ / sell, 'buy' => buy} end # Fiat prices are fetch from rate-exchange.appspot.com but there are no buy/sell prices. # We use USD as the common denominator for all fiat currencies. # @returns [{'sell' => Float, 'buy' => Float}] %w(uyu brl clp sgd eur).each do |currency| define_method("fetch_usd_#{currency}") do response = open("http://rate-exchange.appspot.com/currency?from=usd&to=#{currency}").read json = JSON.parse(response) {'sell' => json['rate'].to_f, 'buy' => json['rate'].to_f} end derive_reversal(:usd, currency) end # All non usd fiat can be converted to btc through their dollar price. non_usd_fiat = supported_fiat - ['usd'] non_usd_fiat.each do |currency| define_method("fetch_btc_#{currency}") do btc_rate = fetch_btc_usd usd_rate = fetch('usd', currency) {'sell' => btc_rate['sell'] * usd_rate['sell'], 'buy' => btc_rate['buy'] * usd_rate['buy']} end derive_reversal(:btc, currency) end # All fiat currencies can be converted to any non-btc cryptocurrency by using their # rate to btc as common denominator. non_usd_to_non_btc = (non_usd_fiat + non_btc_cryptos).product(non_usd_fiat + non_btc_cryptos) usd_to_non_btc = (non_btc_cryptos+[:usd]).product(non_btc_cryptos+[:usd]) (non_usd_to_non_btc + usd_to_non_btc).each do |from, to| define_method("fetch_#{from}_#{to}") do from_rate = fetch(from, 'btc') to_rate = fetch(to, 'btc') {'sell' => from_rate['sell']/to_rate['sell'], 'buy' => from_rate['buy']/to_rate['buy']} end end # From and to the same currency is always 1, but we include the methods just for robustness supported_currencies.each do |c| define_method("fetch_#{c}_#{c}"){ {'sell' => 1, 'buy' => 1 } } end def fetch(from, to, force = false) if force send("fetch_#{from}_#{to}") else cached(from, to){ send("fetch_#{from}_#{to}") } end end # Fetch a given conversion caching the result if a backend is set. # This should be the preferred way to fetch a conversion by users. # Check {#supported_currencies} for a list of possible values for 'from' and 'to' # @param from [String] a three letter currency code to convert from. # @param to [String] a three letter currency code to convert to. # @param to [String] a three letter currency code to convert to. # @param force [Boolean] Ignore the cache (does not read from it, and does not write to it). Defaults to false. # @return [{'buy' => Float, 'sell' => Float}] The buy and sell prices def self.fetch(from,to, force = false) new.fetch(from.downcase, to.downcase, force) end protected def cached(from, to, &block) if self.class.cache_backend self.class.cache_backend.call(from, to, block) else block.call end end end end