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 = $1.to_f
{'sell' => 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