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: []
|