devilicious 1.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 +7 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +15 -0
- data/README.md +52 -0
- data/Rakefile +1 -0
- data/bin/devilicious +13 -0
- data/devilicious.gemspec +24 -0
- data/lib/devilicious.rb +6 -0
- data/lib/devilicious/arbitrer.rb +160 -0
- data/lib/devilicious/config.rb +54 -0
- data/lib/devilicious/currency_converter.rb +81 -0
- data/lib/devilicious/formatters/base.rb +22 -0
- data/lib/devilicious/formatters/summary.rb +17 -0
- data/lib/devilicious/formatters/table.rb +20 -0
- data/lib/devilicious/formatters/verbose.rb +26 -0
- data/lib/devilicious/log.rb +29 -0
- data/lib/devilicious/markets/anx_btc.rb +42 -0
- data/lib/devilicious/markets/base.rb +37 -0
- data/lib/devilicious/markets/bit_nz.rb +43 -0
- data/lib/devilicious/markets/bitcoin_de.rb +43 -0
- data/lib/devilicious/markets/bitcurex.rb +39 -0
- data/lib/devilicious/markets/bitstamp.rb +42 -0
- data/lib/devilicious/markets/btc_e.rb +37 -0
- data/lib/devilicious/markets/hit_btc.rb +40 -0
- data/lib/devilicious/markets/kraken.rb +35 -0
- data/lib/devilicious/money.rb +64 -0
- data/lib/devilicious/offer.rb +35 -0
- data/lib/devilicious/order_book.rb +89 -0
- data/lib/devilicious/sounds/boom.wav +0 -0
- metadata +116 -0
    
        checksums.yaml
    ADDED
    
    | @@ -0,0 +1,7 @@ | |
| 1 | 
            +
            ---
         | 
| 2 | 
            +
            SHA1:
         | 
| 3 | 
            +
              metadata.gz: 86c8535c1f1dc7924d4344803a1490359657c349
         | 
| 4 | 
            +
              data.tar.gz: 0de03730367cbca3c56fb8c66f1c7a05d32663c0
         | 
| 5 | 
            +
            SHA512:
         | 
| 6 | 
            +
              metadata.gz: 559c597ff4ff70c7e6057016a8d47b73f7eb227f4a60e9cffff8792ecf83fc669ce8df6a45edbba9f68d3a0aa9762a8919397dca93ed7c40b8f12244ca5aeb43
         | 
| 7 | 
            +
              data.tar.gz: 976cbe3219761aef3e78bef22e994eb50a29fb3a9367881dcab241ffcada2870f6ed4b249b667f522371924bf663758a249a8f641bb27f8315bb8285c252cb83
         | 
    
        data/.gitignore
    ADDED
    
    
    
        data/Gemfile
    ADDED
    
    
    
        data/LICENSE.txt
    ADDED
    
    | @@ -0,0 +1,15 @@ | |
| 1 | 
            +
            Devilicious
         | 
| 2 | 
            +
            Copyright (C) 2014 Cédric Félizard
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            This program is free software: you can redistribute it and/or modify
         | 
| 5 | 
            +
            it under the terms of the GNU Affero General Public License as
         | 
| 6 | 
            +
            published by the Free Software Foundation, either version 3 of the
         | 
| 7 | 
            +
            License, or (at your option) any later version.
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            This program is distributed in the hope that it will be useful,
         | 
| 10 | 
            +
            but WITHOUT ANY WARRANTY; without even the implied warranty of
         | 
| 11 | 
            +
            MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
         | 
| 12 | 
            +
            GNU Affero General Public License for more details.
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            You should have received a copy of the GNU Affero General Public License
         | 
| 15 | 
            +
            along with this program.  If not, see <http://www.gnu.org/licenses/>.
         | 
    
        data/README.md
    ADDED
    
    | @@ -0,0 +1,52 @@ | |
| 1 | 
            +
            # Devilicious
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            **Disclaimer: Do not expect to make money with this program.**
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            This RubyGem is just an experiment to find out whether or not there are (still) arbitrage opportunities across Bitcoin markets (as of August 2014).
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            Spoiler: Meh, not really.
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            Supported exchanges are:
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            - Kraken (EUR)
         | 
| 12 | 
            +
            - BitcoinDe (EUR)
         | 
| 13 | 
            +
            - BTCE (EUR)
         | 
| 14 | 
            +
            - HitBTC (EUR)
         | 
| 15 | 
            +
            - Bitcurex (EUR)
         | 
| 16 | 
            +
            - Bitstamp (USD)
         | 
| 17 | 
            +
            - BitNZ (NZD)
         | 
| 18 | 
            +
            - ANXBTC (CHF)
         | 
| 19 | 
            +
             | 
| 20 | 
            +
            I've been running it for a couple of days and my findings are:
         | 
| 21 | 
            +
             | 
| 22 | 
            +
            - the best volume to trade is usually in the 1 to 5 bitcoins range
         | 
| 23 | 
            +
            - you won't make more than €30/$40 even trading when the price is on a rollercoaster (specifically mid August 2014)
         | 
| 24 | 
            +
            - you'll need to trade 3+ bitcoins to make those 40 bucks
         | 
| 25 | 
            +
            - in order to trade across a bunch of markets, you'll need to have a lot of funds in every exchanges (since you want to buy/sell at the same time)
         | 
| 26 | 
            +
            - high frequency trading might work out better but fuck that, it just adds noise to the blockchain
         | 
| 27 | 
            +
             | 
| 28 | 
            +
            **Disclaimer #2: This code is quite crappy.**
         | 
| 29 | 
            +
             | 
| 30 | 
            +
            This is a just-for-fun quick-and-dirty not-optimized-at-all pretty-dumb program.
         | 
| 31 | 
            +
            Worst of all, it doesn't even have tests!
         | 
| 32 | 
            +
             | 
| 33 | 
            +
            **Disclaimer #3: Obviously, use this program at your own risks!**
         | 
| 34 | 
            +
             | 
| 35 | 
            +
            ## Installation
         | 
| 36 | 
            +
             | 
| 37 | 
            +
            `gem install devilicious`
         | 
| 38 | 
            +
             | 
| 39 | 
            +
            ## Usage
         | 
| 40 | 
            +
             | 
| 41 | 
            +
            `devilicious --help` is your friend.
         | 
| 42 | 
            +
             | 
| 43 | 
            +
            Example: `devilicious -f Table -m 2 -b 30`
         | 
| 44 | 
            +
             | 
| 45 | 
            +
            ## License
         | 
| 46 | 
            +
             | 
| 47 | 
            +
            AGPLv3
         | 
| 48 | 
            +
             | 
| 49 | 
            +
            ## Bonus link
         | 
| 50 | 
            +
             | 
| 51 | 
            +
            http://www.reddit.com/r/Bitcoin/comments/2dqhiy/8month_high/cjs416y
         | 
| 52 | 
            +
             | 
    
        data/Rakefile
    ADDED
    
    | @@ -0,0 +1 @@ | |
| 1 | 
            +
            require "bundler/gem_tasks"
         | 
    
        data/bin/devilicious
    ADDED
    
    | @@ -0,0 +1,13 @@ | |
| 1 | 
            +
            #!/usr/bin/env ruby
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            begin
         | 
| 4 | 
            +
              require "devilicious"
         | 
| 5 | 
            +
            rescue LoadError
         | 
| 6 | 
            +
              $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
         | 
| 7 | 
            +
              Dir[File.expand_path("../lib/**/*.rb", __dir__)].each { |file| require file }
         | 
| 8 | 
            +
            end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
             | 
| 11 | 
            +
            arbitrer = Devilicious::Arbitrer.new
         | 
| 12 | 
            +
            arbitrer.run!
         | 
| 13 | 
            +
             | 
    
        data/devilicious.gemspec
    ADDED
    
    | @@ -0,0 +1,24 @@ | |
| 1 | 
            +
            # coding: utf-8
         | 
| 2 | 
            +
            lib = File.expand_path('../lib', __FILE__)
         | 
| 3 | 
            +
            $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            Gem::Specification.new do |spec|
         | 
| 6 | 
            +
              spec.name          = "devilicious"
         | 
| 7 | 
            +
              spec.version       = "1.0.0"
         | 
| 8 | 
            +
              spec.authors       = ["Cédric Félizard"]
         | 
| 9 | 
            +
              spec.email         = ["cedric@felizard.fr"]
         | 
| 10 | 
            +
              spec.summary       = %q{Multi-currency Bitcoin arbitrage bot}
         | 
| 11 | 
            +
              spec.description   = spec.summary
         | 
| 12 | 
            +
              spec.homepage      = ""
         | 
| 13 | 
            +
              spec.license       = "AGPLv3"
         | 
| 14 | 
            +
             | 
| 15 | 
            +
              spec.files         = `git ls-files`.split($/)
         | 
| 16 | 
            +
              spec.executables   = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
         | 
| 17 | 
            +
              spec.test_files    = spec.files.grep(%r{^(test|spec|features)/})
         | 
| 18 | 
            +
              spec.require_paths = ["lib"]
         | 
| 19 | 
            +
             | 
| 20 | 
            +
              spec.add_dependency "retryable"
         | 
| 21 | 
            +
             | 
| 22 | 
            +
              spec.add_development_dependency "bundler", "~> 1.5"
         | 
| 23 | 
            +
              spec.add_development_dependency "rake"
         | 
| 24 | 
            +
            end
         | 
    
        data/lib/devilicious.rb
    ADDED
    
    
| @@ -0,0 +1,160 @@ | |
| 1 | 
            +
            module Devilicious
         | 
| 2 | 
            +
              class Arbitrer
         | 
| 3 | 
            +
                def markets
         | 
| 4 | 
            +
                  @markets ||= [
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                    Market::Kraken,
         | 
| 7 | 
            +
                    Market::BitcoinDe,
         | 
| 8 | 
            +
                    Market::BtcE,
         | 
| 9 | 
            +
                    Market::HitBtc,
         | 
| 10 | 
            +
                    Market::Bitcurex,
         | 
| 11 | 
            +
                    Market::Bitstamp,
         | 
| 12 | 
            +
                    Market::BitNz,
         | 
| 13 | 
            +
                    Market::AnxBtc,
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  ].map { |market| market.new }
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                def run!
         | 
| 19 | 
            +
                  @market_queue = []
         | 
| 20 | 
            +
                  spawn_observers!
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  loop do
         | 
| 23 | 
            +
                    sleep 1 while @market_queue.empty?
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                    market_1 = @market_queue.shift
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                    markets.each do |market_2|
         | 
| 28 | 
            +
                      next if market_1 == market_2
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                      if market_2.order_book.nil?
         | 
| 31 | 
            +
                        Log.debug "Order book for #{market_2} not available yet, skipping"
         | 
| 32 | 
            +
                        next
         | 
| 33 | 
            +
                      end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                      # dup everything to avoid race conditions while calculating opportunities
         | 
| 36 | 
            +
                      order_book_1 = market_1.order_book.dup
         | 
| 37 | 
            +
                      order_book_2 = market_2.order_book.dup
         | 
| 38 | 
            +
                      order_book_1.market = market_1.dup
         | 
| 39 | 
            +
                      order_book_2.market = market_2.dup
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                      check_for_opportunity(order_book_1, order_book_2)
         | 
| 42 | 
            +
                      check_for_opportunity(order_book_2, order_book_1)
         | 
| 43 | 
            +
                    end
         | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
              private
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                def spawn_observers!
         | 
| 50 | 
            +
                  Thread.abort_on_exception = true
         | 
| 51 | 
            +
                  trap("EXIT", -> { Thread.list.each(&:kill) } )
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                  Thread.new do
         | 
| 54 | 
            +
                    timeout = Devilicious.config.market_refresh_rate
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                    loop do
         | 
| 57 | 
            +
                      threads = markets.map do |market|
         | 
| 58 | 
            +
                        Log.debug "Refreshing order book for #{market}..."
         | 
| 59 | 
            +
                        Thread.new { market.refresh_order_book!; @market_queue << market }
         | 
| 60 | 
            +
                      end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                      sleep timeout
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                      alive_threads = threads.select(&:alive?)
         | 
| 65 | 
            +
                      unless alive_threads.empty?
         | 
| 66 | 
            +
                        Log.warn "Timeout after #{timeout} seconds for #{alive_threads.size} observer threads"
         | 
| 67 | 
            +
                      end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                      threads.each do |thread|
         | 
| 70 | 
            +
                        thread.abort_on_exception = false
         | 
| 71 | 
            +
                        thread.kill
         | 
| 72 | 
            +
                      end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                      threads.each(&:join)
         | 
| 75 | 
            +
                    end
         | 
| 76 | 
            +
                  end
         | 
| 77 | 
            +
                end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                def check_for_opportunity(order_book_1, order_book_2)
         | 
| 80 | 
            +
                  Log.debug "Checking opportunity buying from #{order_book_1.market} and selling at #{order_book_2.market}... "
         | 
| 81 | 
            +
                  if opportunity = find_best_opportunity(order_book_1, order_book_2)
         | 
| 82 | 
            +
                    threshold = Money.new(Devilicious.config.beep_profit_threshold, Devilicious.config.default_fiat_currency)
         | 
| 83 | 
            +
                    if threshold > 0 && opportunity.profit >= threshold
         | 
| 84 | 
            +
                      Thread.new do
         | 
| 85 | 
            +
                        3.times { system "/usr/bin/aplay", "-q", "#{__dir__}/sounds/boom.wav" }
         | 
| 86 | 
            +
                      end
         | 
| 87 | 
            +
                    end
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                    formatter = Formatter.list[Devilicious.config.formatter]
         | 
| 90 | 
            +
                    formatter.output(opportunity)
         | 
| 91 | 
            +
                  end
         | 
| 92 | 
            +
                end
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                def find_best_opportunity(order_book_1, order_book_2)
         | 
| 95 | 
            +
                  return unless order_book_1.lowest_ask.price < order_book_2.highest_bid.price
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                  initial_ask_offer = order_book_1.weighted_asks_up_to(order_book_2.highest_bid.price)
         | 
| 98 | 
            +
                  initial_bid_offer = order_book_2.weighted_bids_down_to(order_book_1.lowest_ask.price)
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                  _dummy = initial_ask_offer.price + initial_bid_offer.price # NOTE: will raise if currency mismatch
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                  max_volume = [initial_ask_offer.volume, initial_bid_offer.volume].min
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                  best_offer_limited_volume = find_best_volume(
         | 
| 105 | 
            +
                    order_book_1, order_book_2,
         | 
| 106 | 
            +
                    [max_volume, Devilicious.config.max_volume].min
         | 
| 107 | 
            +
                  )
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                  best_offer_unlimited_volume = find_best_volume(
         | 
| 110 | 
            +
                    order_book_1, order_book_2,
         | 
| 111 | 
            +
                    [max_volume, BigDecimal.new("22E6")].min
         | 
| 112 | 
            +
                  )
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                  best_volume = if best_offer_limited_volume.profit > best_offer_unlimited_volume.profit
         | 
| 115 | 
            +
                    best_offer_limited_volume
         | 
| 116 | 
            +
                  else
         | 
| 117 | 
            +
                    best_offer_unlimited_volume
         | 
| 118 | 
            +
                  end.volume
         | 
| 119 | 
            +
             | 
| 120 | 
            +
                  return if best_offer_limited_volume.profit <= 0
         | 
| 121 | 
            +
             | 
| 122 | 
            +
                  best_offer_limited_volume.best_volume = best_volume
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                  best_offer_limited_volume
         | 
| 125 | 
            +
                end
         | 
| 126 | 
            +
             | 
| 127 | 
            +
                def find_best_volume(order_book_1, order_book_2, max_volume)
         | 
| 128 | 
            +
                  volume = best_volume = BigDecimal.new(Devilicious.config.min_volume)
         | 
| 129 | 
            +
                  best_profit = 0
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                  while volume <= max_volume
         | 
| 132 | 
            +
                    ask_offer = order_book_1.min_ask_price_for_volume(order_book_2.highest_bid.price, volume)
         | 
| 133 | 
            +
                    bid_offer = order_book_2.max_bid_price_for_volume(order_book_1.lowest_ask.price, volume)
         | 
| 134 | 
            +
             | 
| 135 | 
            +
                    fee = (
         | 
| 136 | 
            +
                      ask_offer.price * order_book_1.market.trade_fee +
         | 
| 137 | 
            +
                      bid_offer.price * order_book_2.market.trade_fee
         | 
| 138 | 
            +
                    ) * volume
         | 
| 139 | 
            +
             | 
| 140 | 
            +
                    profit = (bid_offer.price - ask_offer.price) * volume - fee
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                    if profit > best_profit
         | 
| 143 | 
            +
                      best_profit, best_volume, best_profit_fee = profit, volume, fee
         | 
| 144 | 
            +
                    end
         | 
| 145 | 
            +
             | 
| 146 | 
            +
                    volume += BigDecimal.new("0.1")
         | 
| 147 | 
            +
                  end
         | 
| 148 | 
            +
             | 
| 149 | 
            +
                  OpenStruct.new(
         | 
| 150 | 
            +
                    order_book_1: order_book_1,
         | 
| 151 | 
            +
                    order_book_2: order_book_2,
         | 
| 152 | 
            +
                    ask_offer: order_book_1.min_ask_price_for_volume(order_book_2.highest_bid.price, best_volume),
         | 
| 153 | 
            +
                    bid_offer: order_book_2.max_bid_price_for_volume(order_book_1.lowest_ask.price, best_volume),
         | 
| 154 | 
            +
                    volume: best_volume,
         | 
| 155 | 
            +
                    profit: best_profit,
         | 
| 156 | 
            +
                    fee: best_profit_fee,
         | 
| 157 | 
            +
                  )
         | 
| 158 | 
            +
                end
         | 
| 159 | 
            +
              end
         | 
| 160 | 
            +
            end
         | 
| @@ -0,0 +1,54 @@ | |
| 1 | 
            +
            require "optparse"
         | 
| 2 | 
            +
            require "ostruct"
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module Devilicious
         | 
| 5 | 
            +
              class Config
         | 
| 6 | 
            +
                def self.parse(args)
         | 
| 7 | 
            +
                  config = OpenStruct.new
         | 
| 8 | 
            +
                  config.debug = false
         | 
| 9 | 
            +
                  config.verbose = false
         | 
| 10 | 
            +
                  config.formatter = "Verbose"
         | 
| 11 | 
            +
                  config.max_volume = BigDecimal.new("10")
         | 
| 12 | 
            +
                  config.min_volume = BigDecimal.new("0.1")
         | 
| 13 | 
            +
                  config.beep_profit_threshold = BigDecimal.new("-1") # negative means disabled
         | 
| 14 | 
            +
                  config.default_fiat_currency = "EUR" # ideally the most used currency so we do as little conversions as possible
         | 
| 15 | 
            +
                  config.market_refresh_rate = 30 # order books expire delay in seconds
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  opt_parser = OptionParser.new do |opts|
         | 
| 18 | 
            +
                    opts.banner = "Usage: devilicious [config]"
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                    opts.separator ""
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                    opts.on("-f", "--formatter TYPE", Formatter.list.keys,
         | 
| 23 | 
            +
                            "Select formatter (#{Formatter.list.keys.sort.join(", ")})") do |f|
         | 
| 24 | 
            +
                      config.formatter = f
         | 
| 25 | 
            +
                    end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                    opts.on("-m", "--max-volume N", "Maximum volume to trade") do |m|
         | 
| 28 | 
            +
                      config.max_volume = BigDecimal.new(m)
         | 
| 29 | 
            +
                    end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                    opts.on("-b", "--beep-profit-threshold N", "Beep the fuck out of the speakers when profit threshold is reached") do |t|
         | 
| 32 | 
            +
                      config.beep_profit_threshold = BigDecimal.new(t)
         | 
| 33 | 
            +
                    end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                    opts.on("-v", "--verbose", "Run verbosely") do |v|
         | 
| 36 | 
            +
                      config.verbose = v
         | 
| 37 | 
            +
                    end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                    opts.on("-d", "--debug", "Debug mode") do |d|
         | 
| 40 | 
            +
                      config.debug = d
         | 
| 41 | 
            +
                    end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                    opts.on_tail("-h", "--help", "Show this message") do
         | 
| 44 | 
            +
                      puts opts
         | 
| 45 | 
            +
                      exit
         | 
| 46 | 
            +
                    end
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  opt_parser.parse!(args)
         | 
| 50 | 
            +
                  config.freeze
         | 
| 51 | 
            +
                end
         | 
| 52 | 
            +
              end
         | 
| 53 | 
            +
            end
         | 
| 54 | 
            +
             | 
| @@ -0,0 +1,81 @@ | |
| 1 | 
            +
            require "retryable"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Devilicious
         | 
| 4 | 
            +
              class CurrencyConverter
         | 
| 5 | 
            +
                @rates = {}
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def self.convert(amount, from, to)
         | 
| 8 | 
            +
                  amount * rate(from, to)
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def self.rate from, to
         | 
| 12 | 
            +
                  pair = [from, to].join
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  if @rates[pair].nil? || @rates[pair].updated_at < Time.now - 10*60
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                    Log.debug "Refreshing #{pair} rate..."
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                    rate = begin
         | 
| 19 | 
            +
                      RateExchange.get_rate(from, to)
         | 
| 20 | 
            +
                    rescue => exception
         | 
| 21 | 
            +
                      Log.warn "Could not retrieve exchange rate from RateExchange: #{exception.inspect}"
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                      YahooExchange.get_rate(from, to)
         | 
| 24 | 
            +
                    end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                    @rates[pair] = OpenStruct.new(
         | 
| 27 | 
            +
                      rate: rate,
         | 
| 28 | 
            +
                      updated_at: Time.now
         | 
| 29 | 
            +
                    ).freeze
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  @rates[pair].rate
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                class RateExchange
         | 
| 37 | 
            +
                  URL = "http://rate-exchange.appspot.com/currency?from=%s&to=%s".freeze
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  def self.get_rate(from, to)
         | 
| 40 | 
            +
                    url = URL % [from, to]
         | 
| 41 | 
            +
                    json = get_json(url)
         | 
| 42 | 
            +
                    rate = json["rate"].to_s
         | 
| 43 | 
            +
                    BigDecimal.new(rate)
         | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                  def self.get_json(url)
         | 
| 47 | 
            +
                    retryable(tries: 3, sleep: 1) do
         | 
| 48 | 
            +
                      json = open(url).read
         | 
| 49 | 
            +
                      JSON.parse(json)
         | 
| 50 | 
            +
                    end
         | 
| 51 | 
            +
                  end
         | 
| 52 | 
            +
                end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                class YahooExchange
         | 
| 55 | 
            +
                  URL = "http://download.finance.yahoo.com/d/quotes.csv?s=%s%s=X&f=sl1d1&e=.csv".freeze
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                  def self.get_rate(from, to)
         | 
| 58 | 
            +
                    url = URL % [from, to]
         | 
| 59 | 
            +
                    csv = get_csv(url)
         | 
| 60 | 
            +
                    rate = BigDecimal.new(csv[1].to_s)
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                    if rate <= 1E-3
         | 
| 63 | 
            +
                      Log.warn "Cannot retrieve exchange rate for #{from}#{to}, not enough precision, using the opposite pair"
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                      url = URL % [to, from]
         | 
| 66 | 
            +
                      csv = get_csv(url)
         | 
| 67 | 
            +
                      rate = BigDecimal.new(1) / BigDecimal.new(csv[1].to_s)
         | 
| 68 | 
            +
                    end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                    rate
         | 
| 71 | 
            +
                  end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                  def self.get_csv(url)
         | 
| 74 | 
            +
                    retryable(tries: 3, sleep: 1) do
         | 
| 75 | 
            +
                      csv = open(url).read
         | 
| 76 | 
            +
                      csv.split(",")
         | 
| 77 | 
            +
                    end
         | 
| 78 | 
            +
                  end
         | 
| 79 | 
            +
                end
         | 
| 80 | 
            +
              end
         | 
| 81 | 
            +
            end
         | 
| @@ -0,0 +1,22 @@ | |
| 1 | 
            +
            module Devilicious
         | 
| 2 | 
            +
              module Formatter
         | 
| 3 | 
            +
                def self.list
         | 
| 4 | 
            +
                  @list ||= {}
         | 
| 5 | 
            +
                end
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                class Base
         | 
| 8 | 
            +
                  def self.inherited(child)
         | 
| 9 | 
            +
                    formatter = child.new
         | 
| 10 | 
            +
                    Formatter.list[formatter.to_s] = formatter
         | 
| 11 | 
            +
                  end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  def to_s
         | 
| 14 | 
            +
                    self.class.to_s.gsub(/.*::/, "")
         | 
| 15 | 
            +
                  end
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
              end
         | 
| 18 | 
            +
            end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
            # Dir["#{__dir__}/**/*.rb"].each do |formatter|
         | 
| 21 | 
            +
            #   require formatter
         | 
| 22 | 
            +
            # end
         | 
| @@ -0,0 +1,17 @@ | |
| 1 | 
            +
            module Devilicious
         | 
| 2 | 
            +
              module Formatter
         | 
| 3 | 
            +
                class Summary < Base
         | 
| 4 | 
            +
                  def output(opportunity)
         | 
| 5 | 
            +
                    pair = [opportunity.order_book_1.market, " to ", opportunity.order_book_2.market, " " * 3].map(&:to_s).join
         | 
| 6 | 
            +
                    @best_trades ||= {}
         | 
| 7 | 
            +
                    @best_trades[pair] = opportunity
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                    Log.info "", timestamp: false
         | 
| 10 | 
            +
                    @best_trades.sort_by { |_, opportunity| opportunity.profit }.each do |pair, opportunity|
         | 
| 11 | 
            +
                      Log.info "#{pair} \t#{opportunity.profit} with #{opportunity.volume.to_f} XBT", timestamp: false
         | 
| 12 | 
            +
                    end
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
              end
         | 
| 16 | 
            +
            end
         | 
| 17 | 
            +
             | 
| @@ -0,0 +1,20 @@ | |
| 1 | 
            +
            module Devilicious
         | 
| 2 | 
            +
              module Formatter
         | 
| 3 | 
            +
                class Table < Base
         | 
| 4 | 
            +
                  def output(opportunity)
         | 
| 5 | 
            +
                    pair = [opportunity.order_book_1.market, " to ", opportunity.order_book_2.market, " " * 4].map(&:to_s).join
         | 
| 6 | 
            +
                    @best_trades ||= {}
         | 
| 7 | 
            +
                    @best_trades[pair] = opportunity
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                    Log.info "", timestamp: false
         | 
| 10 | 
            +
                    Log.info "PAIR                    \tPROFIT       \tVOLUME \tBUY          \tSELL", timestamp: false
         | 
| 11 | 
            +
                    @best_trades.sort_by { |_, opportunity| opportunity.profit }.each do |pair, opportunity|
         | 
| 12 | 
            +
                      pair = pair.dup << " " * [30 - pair.size, 0].max # padding
         | 
| 13 | 
            +
                      Log.info [pair, opportunity.profit, opportunity.volume.to_f, opportunity.ask_offer.price, opportunity.bid_offer.price].join(" \t"), timestamp: false
         | 
| 14 | 
            +
                    end
         | 
| 15 | 
            +
                  end
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
              end
         | 
| 18 | 
            +
            end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
             | 
| @@ -0,0 +1,26 @@ | |
| 1 | 
            +
            module Devilicious
         | 
| 2 | 
            +
              module Formatter
         | 
| 3 | 
            +
                class Verbose < Base
         | 
| 4 | 
            +
                  def output(opportunity)
         | 
| 5 | 
            +
                    fiat_out = opportunity.ask_offer.price * opportunity.volume
         | 
| 6 | 
            +
                    fiat_in = opportunity.bid_offer.price * opportunity.volume
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                    opportunity.ask_offer.price = opportunity.ask_offer.price.exchange_to(opportunity.order_book_1.market.fiat_currency)
         | 
| 9 | 
            +
                    opportunity.bid_offer.price = opportunity.bid_offer.price.exchange_to(opportunity.order_book_2.market.fiat_currency)
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                    volume = "#{opportunity.volume.to_f} XBT "
         | 
| 12 | 
            +
                    volume << if opportunity.volume == opportunity.best_volume
         | 
| 13 | 
            +
                      "(BEST VOLUME!)"
         | 
| 14 | 
            +
                    else
         | 
| 15 | 
            +
                      "(best #{opportunity.best_volume.to_f})"
         | 
| 16 | 
            +
                    end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                    Log.info \
         | 
| 19 | 
            +
                      "BUY \e[1m#{volume}\e[m from \e[1m#{opportunity.order_book_1.market}\e[m for #{fiat_out} at \e[1m#{opportunity.ask_offer.price}\e[m (#{opportunity.ask_offer.weighted_price} weighted average)" <<
         | 
| 20 | 
            +
                      " and SELL at \e[1m#{opportunity.order_book_2.market}\e[m for #{fiat_in} at \e[1m#{opportunity.bid_offer.price}\e[m (#{opportunity.bid_offer.weighted_price})" <<
         | 
| 21 | 
            +
                      " - PROFIT = \e[1m#{opportunity.profit}\e[m (including #{opportunity.fee} fee)"
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
              end
         | 
| 25 | 
            +
            end
         | 
| 26 | 
            +
             | 
| @@ -0,0 +1,29 @@ | |
| 1 | 
            +
            module Devilicious
         | 
| 2 | 
            +
              module Log
         | 
| 3 | 
            +
                @semaphore ||= Mutex.new
         | 
| 4 | 
            +
             | 
| 5 | 
            +
              module_function
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def info message, options = {}
         | 
| 8 | 
            +
                  options = {
         | 
| 9 | 
            +
                    output: $stdout,
         | 
| 10 | 
            +
                    timestamp: true
         | 
| 11 | 
            +
                  }.merge(options)
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  message = "#{Time.now} #{message}" if options.fetch(:timestamp)
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  @semaphore.synchronize { options.fetch(:output).puts message }
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                def debug message, options = {}
         | 
| 19 | 
            +
                  info message, options if Devilicious.config.debug
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                def warn message, options = {}
         | 
| 23 | 
            +
                  options = {output: $stderr}.merge(options)
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                  info "[WARN] #{message}", options
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
              end
         | 
| 28 | 
            +
            end
         | 
| 29 | 
            +
             | 
| @@ -0,0 +1,42 @@ | |
| 1 | 
            +
            require "devilicious/markets/base"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Devilicious
         | 
| 4 | 
            +
              module Market
         | 
| 5 | 
            +
                class AnxBtc < Base
         | 
| 6 | 
            +
                  def fiat_currency
         | 
| 7 | 
            +
                    "CHF"
         | 
| 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")
         | 
| 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, volume = tuple["price"], tuple["amount"]
         | 
| 29 | 
            +
                      price_chf = Money.new(price, fiat_currency)
         | 
| 30 | 
            +
                      price_normalized = price_chf.exchange_to(Devilicious.config.default_fiat_currency)
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                      Offer.new(
         | 
| 33 | 
            +
                        price: price_normalized,
         | 
| 34 | 
            +
                        volume: volume
         | 
| 35 | 
            +
                      ).freeze
         | 
| 36 | 
            +
                    end
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
              end
         | 
| 40 | 
            +
            end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
             | 
| @@ -0,0 +1,37 @@ | |
| 1 | 
            +
            require "open-uri"
         | 
| 2 | 
            +
            require "json"
         | 
| 3 | 
            +
            require "retryable"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Devilicious
         | 
| 6 | 
            +
              module Market
         | 
| 7 | 
            +
                class Base
         | 
| 8 | 
            +
                  attr_reader :order_book
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  def to_s
         | 
| 11 | 
            +
                    self.class.to_s.gsub(/.*::/, "")
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  def trade_fee
         | 
| 15 | 
            +
                    raise NotImplementedError
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                private
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  def get_html(url)
         | 
| 21 | 
            +
                    retryable(tries: 5, sleep: 1) do
         | 
| 22 | 
            +
                      open(url).read
         | 
| 23 | 
            +
                    end
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  def get_json(url)
         | 
| 27 | 
            +
                    html = get_html(url)
         | 
| 28 | 
            +
                    JSON.parse(html)
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  def mark_as_refreshed
         | 
| 32 | 
            +
                    Log.debug "Order book for #{self} has been refreshed"
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
              end
         | 
| 36 | 
            +
            end
         | 
| 37 | 
            +
             | 
| @@ -0,0 +1,43 @@ | |
| 1 | 
            +
            require "devilicious/markets/base"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Devilicious
         | 
| 4 | 
            +
              module Market
         | 
| 5 | 
            +
                class BitNz < Base
         | 
| 6 | 
            +
                  # NOTE: https://bitnz.com/
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  def fiat_currency
         | 
| 9 | 
            +
                    "NZD"
         | 
| 10 | 
            +
                  end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def trade_fee
         | 
| 13 | 
            +
                    BigDecimal.new("0.005").freeze # 0.5% - see https://bitnz.com/fees
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  def refresh_order_book!
         | 
| 17 | 
            +
                    json = get_json("https://bitnz.com/api/0/orderbook")
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                    asks = format_asks_bids(json["asks"])
         | 
| 20 | 
            +
                    bids = format_asks_bids(json["bids"])
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                    mark_as_refreshed
         | 
| 23 | 
            +
                    @order_book = OrderBook.new(asks: asks, bids: bids)
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                private
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  def format_asks_bids(json)
         | 
| 29 | 
            +
                    json.map do |price, volume|
         | 
| 30 | 
            +
                      price_nzd = Money.new(price.to_s, fiat_currency)
         | 
| 31 | 
            +
                      price_normalized = price_nzd.exchange_to(Devilicious.config.default_fiat_currency)
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                      Offer.new(
         | 
| 34 | 
            +
                        price: price_normalized,
         | 
| 35 | 
            +
                        volume: volume.to_s
         | 
| 36 | 
            +
                      ).freeze
         | 
| 37 | 
            +
                    end
         | 
| 38 | 
            +
                  end
         | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
              end
         | 
| 41 | 
            +
            end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
             | 
| @@ -0,0 +1,43 @@ | |
| 1 | 
            +
            require "devilicious/markets/base"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Devilicious
         | 
| 4 | 
            +
              module Market
         | 
| 5 | 
            +
                class BitcoinDe < Base
         | 
| 6 | 
            +
                  ScrappingError = Class.new(Exception)
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  def fiat_currency
         | 
| 9 | 
            +
                    "EUR"
         | 
| 10 | 
            +
                  end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def trade_fee
         | 
| 13 | 
            +
                    BigDecimal.new("0.005").freeze # 0.5% - see https://www.bitcoin.de/en/infos#gebuehren
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  def refresh_order_book!
         | 
| 17 | 
            +
                    retryable(tries: 2, sleep: 0, on: ScrappingError) do
         | 
| 18 | 
            +
                      html = get_html("https://www.bitcoin.de/en/market")
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                      asks = format_asks_bids(html, "offer")
         | 
| 21 | 
            +
                      bids = format_asks_bids(html, "order")
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                      raise ScrappingError if asks.empty? || asks.size != bids.size
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                      mark_as_refreshed
         | 
| 26 | 
            +
                      @order_book = OrderBook.new(asks: asks, bids: bids)
         | 
| 27 | 
            +
                    end
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                private
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  def format_asks_bids(html, type)
         | 
| 33 | 
            +
                    raise ScrappingError unless html.match(/<tbody id="trade_#{type}_results_table_body"(.+?)<\/tbody>/m)
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                    $1.each_line.select { |line| line.include?("data-critical-price") }.map do |line|
         | 
| 36 | 
            +
                      raise ScrappingError unless line.match(/<tr[^>]+data-critical-price="([\d\.]+)" data-amount="([\d\.]+)">/)
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                      Offer.new(price: Money.new($1, fiat_currency), volume: $2).freeze
         | 
| 39 | 
            +
                    end
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
              end
         | 
| 43 | 
            +
            end
         | 
| @@ -0,0 +1,39 @@ | |
| 1 | 
            +
            require "devilicious/markets/base"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Devilicious
         | 
| 4 | 
            +
              module Market
         | 
| 5 | 
            +
                class Bitcurex < Base
         | 
| 6 | 
            +
                  # NOTE: https://eur.bitcurex.com/
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  def fiat_currency
         | 
| 9 | 
            +
                    "EUR"
         | 
| 10 | 
            +
                  end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def trade_fee
         | 
| 13 | 
            +
                    BigDecimal.new("0.004").freeze # 0.4% - see https://eur.bitcurex.com/op%C5%82aty-i-limity
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  def refresh_order_book!
         | 
| 17 | 
            +
                    json = get_json("https://#{fiat_currency.downcase}.bitcurex.com/data/orderbook.json")
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                    asks = format_asks_bids(json["asks"])
         | 
| 20 | 
            +
                    bids = format_asks_bids(json["bids"])
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                    mark_as_refreshed
         | 
| 23 | 
            +
                    @order_book = OrderBook.new(asks: asks, bids: bids)
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                private
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  def format_asks_bids(json)
         | 
| 29 | 
            +
                    json.map do |price, volume|
         | 
| 30 | 
            +
                      Offer.new(
         | 
| 31 | 
            +
                        price: Money.new(price, fiat_currency),
         | 
| 32 | 
            +
                        volume: volume
         | 
| 33 | 
            +
                      ).freeze
         | 
| 34 | 
            +
                    end
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
              end
         | 
| 38 | 
            +
            end
         | 
| 39 | 
            +
             | 
| @@ -0,0 +1,42 @@ | |
| 1 | 
            +
            require "devilicious/markets/base"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Devilicious
         | 
| 4 | 
            +
              module Market
         | 
| 5 | 
            +
                class Bitstamp < Base
         | 
| 6 | 
            +
                  # NOTE: https://www.bitstamp.net/
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  def fiat_currency
         | 
| 9 | 
            +
                    "USD"
         | 
| 10 | 
            +
                  end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def trade_fee
         | 
| 13 | 
            +
                    BigDecimal.new("0.005").freeze # 0.5% - see https://www.bitstamp.net/fee_schedule/
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  def refresh_order_book!
         | 
| 17 | 
            +
                    json = get_json("https://www.bitstamp.net/api/order_book/")
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                    asks = format_asks_bids(json["asks"])
         | 
| 20 | 
            +
                    bids = format_asks_bids(json["bids"])
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                    mark_as_refreshed
         | 
| 23 | 
            +
                    @order_book = OrderBook.new(asks: asks, bids: bids)
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                private
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  def format_asks_bids(json)
         | 
| 29 | 
            +
                    json.map do |price, volume|
         | 
| 30 | 
            +
                      price_usd = Money.new(price, fiat_currency)
         | 
| 31 | 
            +
                      price_normalized = price_usd.exchange_to(Devilicious.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
         | 
| 42 | 
            +
             | 
| @@ -0,0 +1,37 @@ | |
| 1 | 
            +
            require "devilicious/markets/base"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Devilicious
         | 
| 4 | 
            +
              module Market
         | 
| 5 | 
            +
                class BtcE < Base
         | 
| 6 | 
            +
                  def fiat_currency
         | 
| 7 | 
            +
                    "EUR"
         | 
| 8 | 
            +
                  end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  def trade_fee
         | 
| 11 | 
            +
                    BigDecimal.new("0.002").freeze # 0.2%
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  def refresh_order_book!
         | 
| 15 | 
            +
                    json = get_json("https://btc-e.com/api/2/btc_#{fiat_currency.downcase}/depth")
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                    asks = format_asks_bids(json["asks"])
         | 
| 18 | 
            +
                    bids = format_asks_bids(json["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 |price, volume|
         | 
| 28 | 
            +
                      Offer.new(
         | 
| 29 | 
            +
                        price: Money.new(price.to_s, fiat_currency),
         | 
| 30 | 
            +
                        volume: volume.to_s
         | 
| 31 | 
            +
                      ).freeze
         | 
| 32 | 
            +
                    end
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
              end
         | 
| 36 | 
            +
            end
         | 
| 37 | 
            +
             | 
| @@ -0,0 +1,40 @@ | |
| 1 | 
            +
            require "devilicious/markets/base"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Devilicious
         | 
| 4 | 
            +
              module Market
         | 
| 5 | 
            +
                class HitBtc < Base
         | 
| 6 | 
            +
                  # NOTE: https://hitbtc-com.github.io/hitbtc-api/
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  def fiat_currency
         | 
| 9 | 
            +
                    "EUR"
         | 
| 10 | 
            +
                  end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def trade_fee
         | 
| 13 | 
            +
                    BigDecimal.new("0.001").freeze # 0.1% - see https://hitbtc.com/fees-and-limits
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  def refresh_order_book!
         | 
| 17 | 
            +
                    json = get_json("https://api.hitbtc.com/api/1/public/BTC#{fiat_currency}/orderbook")
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                    asks = format_asks_bids(json["asks"])
         | 
| 20 | 
            +
                    bids = format_asks_bids(json["bids"])
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                    mark_as_refreshed
         | 
| 23 | 
            +
                    @order_book = OrderBook.new(asks: asks, bids: bids)
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                private
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  def format_asks_bids(json)
         | 
| 29 | 
            +
                    json.map do |price, volume|
         | 
| 30 | 
            +
                      Offer.new(
         | 
| 31 | 
            +
                        price: Money.new(price, fiat_currency),
         | 
| 32 | 
            +
                        volume: volume
         | 
| 33 | 
            +
                      ).freeze
         | 
| 34 | 
            +
                    end
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
              end
         | 
| 38 | 
            +
            end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
             | 
| @@ -0,0 +1,35 @@ | |
| 1 | 
            +
            require "devilicious/markets/base"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Devilicious
         | 
| 4 | 
            +
              module Market
         | 
| 5 | 
            +
                class Kraken < Base
         | 
| 6 | 
            +
                  def fiat_currency
         | 
| 7 | 
            +
                    "EUR"
         | 
| 8 | 
            +
                  end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  def trade_fee
         | 
| 11 | 
            +
                    BigDecimal.new("0.002").freeze # 0.2%
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  def refresh_order_book!
         | 
| 15 | 
            +
                    json = get_json("https://api.kraken.com/0/public/Depth?pair=XBT#{fiat_currency}")
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                    asks = format_asks_bids(json["result"]["XXBTZ#{fiat_currency}"]["asks"])
         | 
| 18 | 
            +
                    bids = format_asks_bids(json["result"]["XXBTZ#{fiat_currency}"]["bids"])
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                    mark_as_refreshed
         | 
| 21 | 
            +
                    @order_book = OrderBook.new(asks: asks, bids: bids)
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
                private
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                  def format_asks_bids(json)
         | 
| 26 | 
            +
                    json.map do |price, volume|
         | 
| 27 | 
            +
                      Offer.new(
         | 
| 28 | 
            +
                        price: Money.new(price, fiat_currency),
         | 
| 29 | 
            +
                        volume: volume
         | 
| 30 | 
            +
                      ).freeze
         | 
| 31 | 
            +
                    end
         | 
| 32 | 
            +
                  end
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
              end
         | 
| 35 | 
            +
            end
         | 
| @@ -0,0 +1,64 @@ | |
| 1 | 
            +
            require "bigdecimal"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Devilicious
         | 
| 4 | 
            +
              class Money < BigDecimal
         | 
| 5 | 
            +
                attr_reader :currency
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def initialize(amount, currency)
         | 
| 8 | 
            +
                  @currency = currency
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  super(amount)
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                def exchange_to new_currency
         | 
| 14 | 
            +
                  new_amount = CurrencyConverter.convert(self, currency, new_currency)
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  self.class.new new_amount, new_currency
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                def to_s
         | 
| 20 | 
            +
                  sprintf("%.#{decimal_places}f", self) << " " << currency
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                def inspect
         | 
| 24 | 
            +
                  "#<Devilicious::Money amount=#{to_s}>"
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                def +(other)
         | 
| 28 | 
            +
                  assert_currency! other
         | 
| 29 | 
            +
                  self.class.new super, currency
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                def -(other)
         | 
| 33 | 
            +
                  assert_currency! other
         | 
| 34 | 
            +
                  self.class.new super, currency
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                def *(other)
         | 
| 38 | 
            +
                  assert_currency! other
         | 
| 39 | 
            +
                  self.class.new super, currency
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                def /(other)
         | 
| 43 | 
            +
                  assert_currency! other
         | 
| 44 | 
            +
                  self.class.new super, currency
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
              private
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                def decimal_places
         | 
| 50 | 
            +
                  max = 6 # NOTE: don't care about extra decimals
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                  2.upto(max) do |i|
         | 
| 53 | 
            +
                    return i if self == self.round(i)
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                  max
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                def assert_currency!(other)
         | 
| 60 | 
            +
                  raise "Currency mismatch: #{self.inspect} #{other.inspect}" if other.is_a?(self.class) && other.currency != currency
         | 
| 61 | 
            +
                end
         | 
| 62 | 
            +
              end
         | 
| 63 | 
            +
            end
         | 
| 64 | 
            +
             | 
| @@ -0,0 +1,35 @@ | |
| 1 | 
            +
            require "bigdecimal"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Devilicious
         | 
| 4 | 
            +
              class Offer
         | 
| 5 | 
            +
                attr_reader :price, :weighted_price, :volume
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def initialize(hash)
         | 
| 8 | 
            +
                  self.price = hash.delete(:price)
         | 
| 9 | 
            +
                  self.weighted_price = hash.delete(:weighted_price)
         | 
| 10 | 
            +
                  self.volume = BigDecimal.new(hash.delete(:volume))
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  raise ArgumentError unless hash.empty?
         | 
| 13 | 
            +
                  raise ArgumentError, "#{price.class} is not Money" unless price.is_a? Money
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                def inspect
         | 
| 17 | 
            +
                  "#<Devilicious::Offer price=#{price} volume=#{volume.to_f}>"
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                def price=(price)
         | 
| 21 | 
            +
                  raise ArgumentError if price < 0
         | 
| 22 | 
            +
                  @price = price
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                def weighted_price=(weighted_price)
         | 
| 26 | 
            +
                  raise ArgumentError if weighted_price && weighted_price < 0
         | 
| 27 | 
            +
                  @weighted_price = weighted_price
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                def volume=(volume)
         | 
| 31 | 
            +
                  raise ArgumentError if volume < 0
         | 
| 32 | 
            +
                  @volume = volume
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
              end
         | 
| 35 | 
            +
            end
         | 
| @@ -0,0 +1,89 @@ | |
| 1 | 
            +
            module Devilicious
         | 
| 2 | 
            +
              class OrderBook
         | 
| 3 | 
            +
                attr_reader :asks, :bids
         | 
| 4 | 
            +
                attr_accessor :market
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                def initialize(hash)
         | 
| 7 | 
            +
                  @asks = hash.delete(:asks).sort_by(&:price).freeze # buy
         | 
| 8 | 
            +
                  @bids = hash.delete(:bids).sort_by(&:price).freeze # sell
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  raise ArgumentError unless hash.empty?
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                def highest_bid
         | 
| 14 | 
            +
                  bids.last
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                def lowest_ask
         | 
| 18 | 
            +
                  asks.first
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def weighted_asks_up_to(max_price)
         | 
| 22 | 
            +
                  weighted_offers(asks, ->(price) { price <= max_price })
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                def weighted_bids_down_to(min_price)
         | 
| 26 | 
            +
                  weighted_offers(bids, ->(price) { price >= min_price })
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                def min_ask_price_for_volume(max_price, max_volume)
         | 
| 30 | 
            +
                  interesting_asks = interesting_offers(asks, ->(price) { price <= max_price })
         | 
| 31 | 
            +
                  best_offer_price_for_volume(interesting_asks, max_volume)
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                def max_bid_price_for_volume(min_price, max_volume)
         | 
| 35 | 
            +
                  interesting_bids = interesting_offers(bids, ->(price) { price >= min_price }).reverse # reverse to start from most expensive
         | 
| 36 | 
            +
                  best_offer_price_for_volume(interesting_bids, max_volume)
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
              private
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                def interesting_offers(offers, condition)
         | 
| 42 | 
            +
                  offers.select { |offer| condition.call(offer.price) }
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                def weighted_offers(offers, condition)
         | 
| 46 | 
            +
                  interesting_offers = interesting_offers(offers, condition)
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                  total_volume = interesting_offers.map(&:volume).inject(:+) || 0
         | 
| 49 | 
            +
                  total_weight_price = interesting_offers.map { |offer| offer.price * offer.volume }.inject(:+) || 0
         | 
| 50 | 
            +
                  weighted_price = total_weight_price / total_volume
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                  Offer.new(
         | 
| 53 | 
            +
                    price: Money.new(weighted_price, currency),
         | 
| 54 | 
            +
                    volume: total_volume
         | 
| 55 | 
            +
                  )
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                def best_offer_price_for_volume(offers, max_volume)
         | 
| 59 | 
            +
                  total_volume = 0
         | 
| 60 | 
            +
                  good_offers = []
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                  offers.each do |offer|
         | 
| 63 | 
            +
                    if total_volume <= max_volume
         | 
| 64 | 
            +
                      good_offers << offer.dup # NOTE: dup because we're gonna mess with its volume below
         | 
| 65 | 
            +
                      total_volume += offer.volume
         | 
| 66 | 
            +
                    end
         | 
| 67 | 
            +
                  end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                  if total_volume > max_volume
         | 
| 70 | 
            +
                    substract_volume = total_volume - max_volume
         | 
| 71 | 
            +
                    good_offers.last.volume -= substract_volume
         | 
| 72 | 
            +
                    total_volume -= substract_volume
         | 
| 73 | 
            +
                  end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                  total_weight_price = good_offers.map { |offer| offer.price * offer.volume }.inject(:+) || 0
         | 
| 76 | 
            +
                  weighted_price = total_weight_price / total_volume
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                  Offer.new(
         | 
| 79 | 
            +
                    price: good_offers.last.price,
         | 
| 80 | 
            +
                    volume: total_volume,
         | 
| 81 | 
            +
                    weighted_price: weighted_price,
         | 
| 82 | 
            +
                  )
         | 
| 83 | 
            +
                end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                def currency
         | 
| 86 | 
            +
                  lowest_ask.price.currency
         | 
| 87 | 
            +
                end
         | 
| 88 | 
            +
              end
         | 
| 89 | 
            +
            end
         | 
| Binary file | 
    
        metadata
    ADDED
    
    | @@ -0,0 +1,116 @@ | |
| 1 | 
            +
            --- !ruby/object:Gem::Specification
         | 
| 2 | 
            +
            name: devilicious
         | 
| 3 | 
            +
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            +
              version: 1.0.0
         | 
| 5 | 
            +
            platform: ruby
         | 
| 6 | 
            +
            authors:
         | 
| 7 | 
            +
            - Cédric Félizard
         | 
| 8 | 
            +
            autorequire: 
         | 
| 9 | 
            +
            bindir: bin
         | 
| 10 | 
            +
            cert_chain: []
         | 
| 11 | 
            +
            date: 2014-08-30 00:00:00.000000000 Z
         | 
| 12 | 
            +
            dependencies:
         | 
| 13 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 14 | 
            +
              name: retryable
         | 
| 15 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 16 | 
            +
                requirements:
         | 
| 17 | 
            +
                - - ">="
         | 
| 18 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 19 | 
            +
                    version: '0'
         | 
| 20 | 
            +
              type: :runtime
         | 
| 21 | 
            +
              prerelease: false
         | 
| 22 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 23 | 
            +
                requirements:
         | 
| 24 | 
            +
                - - ">="
         | 
| 25 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 26 | 
            +
                    version: '0'
         | 
| 27 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 28 | 
            +
              name: bundler
         | 
| 29 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 30 | 
            +
                requirements:
         | 
| 31 | 
            +
                - - "~>"
         | 
| 32 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 33 | 
            +
                    version: '1.5'
         | 
| 34 | 
            +
              type: :development
         | 
| 35 | 
            +
              prerelease: false
         | 
| 36 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 37 | 
            +
                requirements:
         | 
| 38 | 
            +
                - - "~>"
         | 
| 39 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 40 | 
            +
                    version: '1.5'
         | 
| 41 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 42 | 
            +
              name: rake
         | 
| 43 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 44 | 
            +
                requirements:
         | 
| 45 | 
            +
                - - ">="
         | 
| 46 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 47 | 
            +
                    version: '0'
         | 
| 48 | 
            +
              type: :development
         | 
| 49 | 
            +
              prerelease: false
         | 
| 50 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 51 | 
            +
                requirements:
         | 
| 52 | 
            +
                - - ">="
         | 
| 53 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 54 | 
            +
                    version: '0'
         | 
| 55 | 
            +
            description: Multi-currency Bitcoin arbitrage bot
         | 
| 56 | 
            +
            email:
         | 
| 57 | 
            +
            - cedric@felizard.fr
         | 
| 58 | 
            +
            executables:
         | 
| 59 | 
            +
            - devilicious
         | 
| 60 | 
            +
            extensions: []
         | 
| 61 | 
            +
            extra_rdoc_files: []
         | 
| 62 | 
            +
            files:
         | 
| 63 | 
            +
            - ".gitignore"
         | 
| 64 | 
            +
            - Gemfile
         | 
| 65 | 
            +
            - LICENSE.txt
         | 
| 66 | 
            +
            - README.md
         | 
| 67 | 
            +
            - Rakefile
         | 
| 68 | 
            +
            - bin/devilicious
         | 
| 69 | 
            +
            - devilicious.gemspec
         | 
| 70 | 
            +
            - lib/devilicious.rb
         | 
| 71 | 
            +
            - lib/devilicious/arbitrer.rb
         | 
| 72 | 
            +
            - lib/devilicious/config.rb
         | 
| 73 | 
            +
            - lib/devilicious/currency_converter.rb
         | 
| 74 | 
            +
            - lib/devilicious/formatters/base.rb
         | 
| 75 | 
            +
            - lib/devilicious/formatters/summary.rb
         | 
| 76 | 
            +
            - lib/devilicious/formatters/table.rb
         | 
| 77 | 
            +
            - lib/devilicious/formatters/verbose.rb
         | 
| 78 | 
            +
            - lib/devilicious/log.rb
         | 
| 79 | 
            +
            - lib/devilicious/markets/anx_btc.rb
         | 
| 80 | 
            +
            - lib/devilicious/markets/base.rb
         | 
| 81 | 
            +
            - lib/devilicious/markets/bit_nz.rb
         | 
| 82 | 
            +
            - lib/devilicious/markets/bitcoin_de.rb
         | 
| 83 | 
            +
            - lib/devilicious/markets/bitcurex.rb
         | 
| 84 | 
            +
            - lib/devilicious/markets/bitstamp.rb
         | 
| 85 | 
            +
            - lib/devilicious/markets/btc_e.rb
         | 
| 86 | 
            +
            - lib/devilicious/markets/hit_btc.rb
         | 
| 87 | 
            +
            - lib/devilicious/markets/kraken.rb
         | 
| 88 | 
            +
            - lib/devilicious/money.rb
         | 
| 89 | 
            +
            - lib/devilicious/offer.rb
         | 
| 90 | 
            +
            - lib/devilicious/order_book.rb
         | 
| 91 | 
            +
            - lib/devilicious/sounds/boom.wav
         | 
| 92 | 
            +
            homepage: ''
         | 
| 93 | 
            +
            licenses:
         | 
| 94 | 
            +
            - AGPLv3
         | 
| 95 | 
            +
            metadata: {}
         | 
| 96 | 
            +
            post_install_message: 
         | 
| 97 | 
            +
            rdoc_options: []
         | 
| 98 | 
            +
            require_paths:
         | 
| 99 | 
            +
            - lib
         | 
| 100 | 
            +
            required_ruby_version: !ruby/object:Gem::Requirement
         | 
| 101 | 
            +
              requirements:
         | 
| 102 | 
            +
              - - ">="
         | 
| 103 | 
            +
                - !ruby/object:Gem::Version
         | 
| 104 | 
            +
                  version: '0'
         | 
| 105 | 
            +
            required_rubygems_version: !ruby/object:Gem::Requirement
         | 
| 106 | 
            +
              requirements:
         | 
| 107 | 
            +
              - - ">="
         | 
| 108 | 
            +
                - !ruby/object:Gem::Version
         | 
| 109 | 
            +
                  version: '0'
         | 
| 110 | 
            +
            requirements: []
         | 
| 111 | 
            +
            rubyforge_project: 
         | 
| 112 | 
            +
            rubygems_version: 2.2.2
         | 
| 113 | 
            +
            signing_key: 
         | 
| 114 | 
            +
            specification_version: 4
         | 
| 115 | 
            +
            summary: Multi-currency Bitcoin arbitrage bot
         | 
| 116 | 
            +
            test_files: []
         |