devilicious 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|