rbtc_arbitrage 0.1.0 → 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 +6 -6
- data/.rspec +1 -1
- data/.travis.yml +13 -3
- data/Gemfile +10 -0
- data/Guardfile +8 -0
- data/README.md +41 -3
- data/btce-api-key.yml +2 -0
- data/lib/rbtc_arbitrage/campbx.rb +98 -0
- data/lib/rbtc_arbitrage/cli.rb +3 -1
- data/lib/rbtc_arbitrage/client.rb +45 -0
- data/lib/rbtc_arbitrage/clients/bitstamp_client.rb +52 -0
- data/lib/rbtc_arbitrage/clients/mtgox_client.rb +45 -0
- data/lib/rbtc_arbitrage/trader.rb +62 -66
- data/lib/rbtc_arbitrage/version.rb +1 -1
- data/lib/rbtc_arbitrage.rb +3 -1
- data/rbtc_arbitrage.gemspec +2 -0
- data/spec/cli_spec.rb +8 -0
- data/spec/client_spec.rb +18 -0
- data/spec/clients/bitstamp_client_spec.rb +53 -0
- data/spec/clients/mtgox_client_spec.rb +52 -0
- data/spec/spec_helper.rb +27 -12
- data/spec/support/cassettes/RbtcArbitrage_Clients_BitstampClient/_balance/fetches_the_balance_correctly.yml +96 -0
- data/spec/support/cassettes/RbtcArbitrage_Clients_BitstampClient/_price/fetches_price_for_buy_correctly.yml +52 -0
- data/spec/support/cassettes/RbtcArbitrage_Clients_BitstampClient/_price/fetches_price_for_sell_correctly.yml +52 -0
- data/spec/support/cassettes/RbtcArbitrage_Clients_BitstampClient/_price/fetches_prices_correctly.yml +44 -0
- data/spec/support/cassettes/RbtcArbitrage_Clients_MtGoxClient/_balance/fetches_the_balance_correctly.yml +77 -0
- data/spec/support/cassettes/RbtcArbitrage_Clients_MtGoxClient/_price/fetches_price_for_buy_correctly.yml +44 -0
- data/spec/support/cassettes/RbtcArbitrage_Clients_MtGoxClient/_price/fetches_price_for_sell_correctly.yml +44 -0
- data/spec/support/cassettes/RbtcArbitrage_Clients_MtGoxClient/_price/fetches_prices_correctly.yml +85 -0
- data/spec/support/cassettes/RbtcArbitrage_Trader/_execute_trade/should_raise_SecurityError_if_not_live.yml +93 -0
- data/spec/support/cassettes/RbtcArbitrage_Trader/_execute_trade/when_live/raises_SecurityError_if_not_enough_BTC.yml +86 -0
- data/spec/support/cassettes/RbtcArbitrage_Trader/_execute_trade/when_live/raises_SecurityError_if_not_enough_USD.yml +80 -0
- data/spec/support/cassettes/RbtcArbitrage_Trader/_execute_trade/when_live/should_fetch_balance.yml +225 -0
- data/spec/support/cassettes/RbtcArbitrage_Trader/_execute_trade/when_live/shouldn_t_raise_security_error.yml +262 -0
- data/spec/support/cassettes/RbtcArbitrage_Trader/_fetch_prices/gets_the_right_price_set.yml +176 -0
- data/spec/support/cassettes/RbtcArbitrage_Trader/_get_balance/fetches_the_right_balance.yml +166 -0
- data/spec/trader_spec.rb +156 -19
- metadata +89 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
5
|
-
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 085039358b729bed53c64ff11885c145b9a90686
|
4
|
+
data.tar.gz: 841862abfc69c37d33ed1225727f19a5fe2be36c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4523e93e04eff256b8e811f20c1abe832d156e05bf965cf6053989bb772e55d91f7563d647bff86f0d2ef0b3dabf690ee9408c40d00e80b9bbde2b2266ae852e
|
7
|
+
data.tar.gz: eaf1b7274b62f349c57141f82826fba5dea50394b13f26a6d59c3be79b20e30688ea4fce917077cf1e171f7980be2297b9043138ae8a3c9c2d90d9b226d8b369
|
data/.rspec
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
--color
|
2
|
-
--format
|
2
|
+
--format d
|
data/.travis.yml
CHANGED
@@ -1,5 +1,15 @@
|
|
1
1
|
language: ruby
|
2
2
|
rvm:
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
- 2.1.0
|
4
|
+
- 2.0.0
|
5
|
+
- 1.9.3
|
6
|
+
- ruby-head
|
7
|
+
env:
|
8
|
+
global:
|
9
|
+
- secure: FdoR9cSL5JzSiGRgmrEGCx55VdP7QU6jt2vzeJyc+2Ho67UXxJuXWgFdwojs6X85O3in8YfSOInBLGXXsD8a3mBuvhsAaMmDLbhFKfEExsc9bnpTj3k95UMGRO3pGNkk1tfj/ohsLFs+yL4r/mFZ5pOoxWW0uhYuRPA+Jk39hZg=
|
10
|
+
- secure: k8OmNupst65nssbCEYLnUdJdYvwnW6YUqcBCNOLcxXKRWcABf2RGmfI/T/iyO0uzzF6e9Ms3C9eZixfIXw4BVTY7DajKHicjvUMPMzzAwAqpK9fb2+mHUmNuksl8wjLvZB4j3TkAAxMECjpPQ2TvY1NRj375STUICC1Ahk9V6CU=
|
11
|
+
- secure: DTjDDBK+lKN6unPF1WOj8a/IIEcty4hkqeP2brilboAyKczxxuDkWcEY7q4mr1whzSQqnWgZgKH2my7mJwWm+J8DMW4bkD5se6eQtmHB/lXkIkIUxHdHT1KM5o+8MqMnb8PX7gquudeIGwNfI2yXa8Nh33y/fVCiKsMDg25Z8Es=
|
12
|
+
- secure: VDB+4rlwVZHOVDQanlAp/QD6owxd5RtiOCUC1H/NudyFqAeXUrn57FC8IN4f4s5Mk7mW9M7F7VYF2l4egjfVYszjq3OlUaWb5uMS7LSVl7I4rNaCM+pPA4vi7rlt6F0LqZoDJzNX7Md1SwIQjCBracRbpV3ZKL8GHh5yS3Ojk38=
|
13
|
+
- secure: ksmMGyDkwmIUkJIHzdrAld+GN2KEm9l8dtxZikoP7B9zRymhnPhImDfZHbByFV0DIZmNiUMFvNLIIpgznjpFdpcEjCbLQnfniKTscMyQZ1pvOesCyaPnSHBENRDsADFCG/TMeP5VXtUUL2k/eNIzHU0cVJT8JaEMdihPDy+mrO8=
|
14
|
+
- secure: I3rS8d84BoCsZo9c/PcY6F/GDThVu/rPKMjTUGHkbtWXVD8E809JxaWavu/+EesyUhn8dDGwMRUc+4d6gtk+QeQVOyLkkPIaCII4puJEHqdyJpqrJlsZbXaGO/JJ6mx2y0I6wDryKeLhZqVn6VdxycF+7OTqYdCFGV+o49wirIU=
|
15
|
+
- secure: kih2LqMwQDZErJFU4hslPZcAdtYwQIYyzxnkObIWtIQpLAVQNssbISLBe5ogWNXi8FyiEktAH5Yy0orjTp2ynmrA6rE6NU/vD3oBDeeI7CCd+13weeIW+uReyixuI5zn6zzJLHdxn+rUwowfdYrt9vF54uEjKCVL32pbLZjdavk=
|
data/Gemfile
CHANGED
@@ -5,3 +5,13 @@ gemspec
|
|
5
5
|
gem 'rspec'
|
6
6
|
gem "activemodel", ">= 3.1"
|
7
7
|
gem "activesupport", ">= 3.1"
|
8
|
+
gem 'guard'
|
9
|
+
gem 'ruby_gntp'
|
10
|
+
# gem 'awesome_print'
|
11
|
+
gem 'guard-rspec'
|
12
|
+
gem 'simplecov'
|
13
|
+
gem 'coveralls', require: false
|
14
|
+
gem "webmock"
|
15
|
+
gem 'vcr'
|
16
|
+
gem 'codeclimate-test-reporter'
|
17
|
+
gem 'hashie'
|
data/Guardfile
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
guard 'rspec' do
|
2
|
+
watch(%r{^spec/.+_spec\.rb$})
|
3
|
+
watch(%r{^lib/rbtc_arbitrage/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
4
|
+
watch('spec/spec_helper.rb') { 'spec' }
|
5
|
+
watch(%r{^spec/clients/.+_spec\.rb$})
|
6
|
+
watch(%r{^lib/rbtc_arbitrage/clients/(.+)\.rb$}) { |m| "spec/clients/#{m[1]}_spec.rb" }
|
7
|
+
end
|
8
|
+
|
data/README.md
CHANGED
@@ -1,6 +1,20 @@
|
|
1
1
|
# RbtcArbitrage
|
2
2
|
|
3
|
-
A Ruby gem for
|
3
|
+
A Ruby gem for executing arbitrage between the MtGox and Bitstamp bitcoin exchanges.
|
4
|
+
|
5
|
+
## Meta
|
6
|
+
|
7
|
+
[Explanation of bitcoin arbitrage](http://hankstoever.com/posts/13-Everything-you-need-to-know-about-Bitcoin-arbitrage)
|
8
|
+
|
9
|
+
[Why I open sourced a bitcoin arbitrate bot](http://hankstoever.com/posts/2-Why-I-open-sourced-a-bitcoin-arbitrage-bot)
|
10
|
+
|
11
|
+
I'm also creating a course on [creating your own bitcoin arbitrage bot](https://www.uludum.org/funds/2)
|
12
|
+
|
13
|
+
Donations accepted: **16BMcqf93eEpb2aWgMkJCSQQH85WzrpbdZ**
|
14
|
+
|
15
|
+
[](https://travis-ci.org/hstove/rbtc_arbitrage)
|
16
|
+
[](https://coveralls.io/r/hstove/rbtc_arbitrage)
|
17
|
+
[](https://codeclimate.com/github/hstove/rbtc_arbitrage)
|
4
18
|
|
5
19
|
## Installation
|
6
20
|
|
@@ -21,17 +35,21 @@ After installing the gem, simply run `rbtc` in the command line.
|
|
21
35
|
2. BITSTAMP_KEY
|
22
36
|
2. BITSTAMP_SECRET
|
23
37
|
3. BITSTAMP_ADDRESS
|
24
|
-
|
38
|
+
4. BITSTAMP_CLIENT_ID
|
39
|
+
|
25
40
|
- **Cutoff**: the minimum profit percentage required to execute a trade. Defaults to **%2.00**.
|
26
41
|
- **Volume**: The amount of bitcoins to trade per transaction. Defaults to **0.01** (the minimum transaction size).
|
42
|
+
- **Buyer**: The exchange you'd like to buy bitcoins from during arbitrage. `"mtgox"` or `"bitstamp"`. Default is `bitstamp`
|
43
|
+
- **Seller**: The exchange you'd like to sell bitcoins from during arbitrage. `"mtgox"` or `"bitstamp"`. Default is `bitstamp`
|
27
44
|
|
28
45
|
#### Examples
|
29
46
|
|
30
47
|
$ rbtc --live --cutoff 4
|
31
48
|
$ rbtc --cutoff 0.5
|
32
49
|
$ rbtc --cutoff 3 --volume 0.05
|
50
|
+
$ rbtc --seller bitstamp --buyer mtgox
|
33
51
|
$ rbtc
|
34
|
-
|
52
|
+
|
35
53
|
The output will look like this:
|
36
54
|
|
37
55
|
07/08/2013 at 10:41AM
|
@@ -42,6 +60,14 @@ The output will look like this:
|
|
42
60
|
selling 0.01 btc on MtGox for $0.76
|
43
61
|
profit: $0.02 (2.77%)
|
44
62
|
|
63
|
+
## Changelog
|
64
|
+
|
65
|
+
### 2.0.0
|
66
|
+
|
67
|
+
- full refactor
|
68
|
+
- 100% test coverage
|
69
|
+
- Modularized exchange-specific code to allow for easier extension.
|
70
|
+
- CLI `buyer` and `seller` option.
|
45
71
|
|
46
72
|
## Contributing
|
47
73
|
|
@@ -52,3 +78,15 @@ The output will look like this:
|
|
52
78
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
53
79
|
4. Push to the branch (`git push origin my-new-feature`)
|
54
80
|
5. Create new Pull Request
|
81
|
+
|
82
|
+
## Adding an exchange
|
83
|
+
|
84
|
+
Right now there is support for only MtGox and Bitstamp, but adding support for other exchanges is dead simple. First, you'll need to create a new `client` in `lib/rbtc_arbitrage/clients`. Follow the example from the [mtgox client](https://github.com/hstove/rbtc_arbitrage/blob/master/lib/rbtc_arbitrage/clients/mtgox_client.rb). You'll need to provide custom implementations of the following methods:
|
85
|
+
|
86
|
+
- `validate_env`
|
87
|
+
- `balance`
|
88
|
+
- `price`
|
89
|
+
- `trade`
|
90
|
+
- `exchange`
|
91
|
+
|
92
|
+
Make sure that the methods accept the same arguments and return similar objects. At the same time, make sure you copy the [mtgox_cient_spec](https://github.com/hstove/rbtc_arbitrage/blob/master/spec/clients/mtgox_client_spec.rb) and change it to test your client.
|
data/btce-api-key.yml
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
## Ruby module for working with CampBX API
|
2
|
+
## May 2013
|
3
|
+
## based on glenbot's python work @ https://github.com/glenbot/campbx/
|
4
|
+
|
5
|
+
require 'json'
|
6
|
+
require 'net/http'
|
7
|
+
require 'uri'
|
8
|
+
|
9
|
+
module CampBX
|
10
|
+
API_BASE = 'https://campbx.com/api/'
|
11
|
+
|
12
|
+
# { method_name => [ url, auth?, [ parameters ] ] }
|
13
|
+
# See: https://campbx.com/api.php for specifics.
|
14
|
+
# Each method returns a Hash with JSON data from the API.
|
15
|
+
# Some parameters are optional.
|
16
|
+
CALLS = {
|
17
|
+
'xdepth' => [ 'xdepth', FALSE, [] ],
|
18
|
+
'xticker' => [ 'xticker', FALSE, [] ],
|
19
|
+
'my_funds' => ['myfunds', TRUE, [] ],
|
20
|
+
'my_orders' => ['myorders', TRUE, [] ],
|
21
|
+
'my_margins' => ['mymargins', TRUE, [] ],
|
22
|
+
'send_instant' => ['sendinstant', TRUE, [ 'CBXCode', 'BTCAmt' ] ],
|
23
|
+
'get_btc_address' => ['getbtcaddr', TRUE, [] ],
|
24
|
+
'send_btc' => ['sendbtc', TRUE, [ 'BTCTo', 'BTCAmt' ] ],
|
25
|
+
# 'dwolla' => [ nil, TRUE, [] ], # Coming Soon (TM)
|
26
|
+
'trade_cancel' => ['tradecancel', TRUE, [ 'Type', 'OrderID' ] ],
|
27
|
+
'trade_enter' => ['tradeenter', TRUE, [ 'TradeMode', 'Quantity', 'Price' ] ],
|
28
|
+
'trade_advanced' => ['tradeadv', TRUE, [ 'TradeMode', 'Price', 'Quantity', 'FillType', 'DarkPool', 'Expiry' ] ],
|
29
|
+
# 'margin_buy' => [ nil, TRUE, [] ], # Coming Soon (TM)
|
30
|
+
# 'short_sell' => [ nil, TRUE, [] ], # Coming Soon (TM)
|
31
|
+
}
|
32
|
+
|
33
|
+
|
34
|
+
class API
|
35
|
+
# CampBX API rate limiting probably per IP address (not account)
|
36
|
+
# which is why we don't limit per instance
|
37
|
+
@@last = Time.new(0)
|
38
|
+
@username = nil
|
39
|
+
@password = nil
|
40
|
+
|
41
|
+
def initialize( username=nil, password=nil )
|
42
|
+
@username = username
|
43
|
+
@password = password
|
44
|
+
|
45
|
+
# Build meta-methods for each API call
|
46
|
+
CALLS.each do |name|
|
47
|
+
define_singleton_method name[0], lambda { |*args|
|
48
|
+
data = CALLS[name[0]]
|
49
|
+
api_request( [data[0], data[1]], Hash[data[2].zip( args )] )
|
50
|
+
}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def api_request( info, post_data={} )
|
55
|
+
url, auth = info
|
56
|
+
uri = URI.parse(API_BASE + url + '.php')
|
57
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
58
|
+
http.use_ssl=TRUE
|
59
|
+
# CampBX advises latency can be >4 minutes when markets are volatile
|
60
|
+
http.read_timeout = 300
|
61
|
+
res = nil
|
62
|
+
|
63
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
64
|
+
if auth then
|
65
|
+
post_data.merge!({
|
66
|
+
'user' => @username,
|
67
|
+
'pass' => @password,
|
68
|
+
})
|
69
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
70
|
+
request.set_form_data( post_data )
|
71
|
+
end
|
72
|
+
|
73
|
+
# debug # need to test w/valid credentials
|
74
|
+
#puts "Sending request to #{uri}"
|
75
|
+
#puts "Post Data: #{post_data}"
|
76
|
+
|
77
|
+
# CampBX API: max 1 request per 500ms
|
78
|
+
delta = Time.now - @@last
|
79
|
+
#puts delta*1000
|
80
|
+
if delta*1000 <= 500 then
|
81
|
+
#puts "sleeping! for #{0.5 - delta}"
|
82
|
+
sleep(0.5 - delta)
|
83
|
+
end
|
84
|
+
|
85
|
+
res = http.request(request)
|
86
|
+
@@last = Time.now # Update time after request returns
|
87
|
+
|
88
|
+
if res.message == 'OK' then # HTTP OK
|
89
|
+
JSON.parse( res.body )
|
90
|
+
else # HTTP ERROR
|
91
|
+
warn "HTTP Error: + #{res.code}"
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
data/lib/rbtc_arbitrage/cli.rb
CHANGED
@@ -6,11 +6,13 @@ module RbtcArbitrage
|
|
6
6
|
option :cutoff, type: :numeric, default: 2, desc: "The minimum profit level required to execute a trade."
|
7
7
|
option :volume, type: :numeric, default: 0.01, desc: "The amount of bitcoins to trade per transaction."
|
8
8
|
option :verbose, type: :boolean, default: true, desc: "Whether you wish to log information."
|
9
|
+
option :buyer, type: :string, default: "bitstamp"
|
10
|
+
option :seller, type: :string, default: "mtgox"
|
9
11
|
def trade
|
10
12
|
RbtcArbitrage::Trader.new(options).trade
|
11
13
|
end
|
12
14
|
|
13
15
|
default_task :trade
|
14
|
-
|
16
|
+
|
15
17
|
end
|
16
18
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module RbtcArbitrage
|
2
|
+
module Client
|
3
|
+
attr_accessor :options
|
4
|
+
attr_writer :balance
|
5
|
+
|
6
|
+
def initialize config={}
|
7
|
+
@options = config
|
8
|
+
@options = {}
|
9
|
+
set_key config, :volume, 0.01
|
10
|
+
set_key config, :cutoff, 2
|
11
|
+
set_key config, :logger, Logger.new(STDOUT)
|
12
|
+
set_key config, :verbose, true
|
13
|
+
set_key config, :live, false
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
def validate_keys
|
18
|
+
["KEY", "SECRET", "ADDRESS"].each do |suffix|
|
19
|
+
prefix = exchange.to_s.upcase
|
20
|
+
key = "#{prefix}_#{suffix}"
|
21
|
+
if ENV[key].blank?
|
22
|
+
raise ArgumentError, "Exiting because missing required ENV variable $#{key}."
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def buy
|
28
|
+
trade :buy
|
29
|
+
end
|
30
|
+
|
31
|
+
def sell
|
32
|
+
trade :sell
|
33
|
+
end
|
34
|
+
|
35
|
+
def address
|
36
|
+
ENV["#{exchange.to_s.upcase}_ADDRESS"]
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def set_key config, key, default
|
42
|
+
@options[key] = config.has_key?(key) ? config[key] : default
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module RbtcArbitrage
|
2
|
+
module Clients
|
3
|
+
class BitstampClient
|
4
|
+
include RbtcArbitrage::Client
|
5
|
+
|
6
|
+
def balance
|
7
|
+
return @balance if @balance
|
8
|
+
balances = Bitstamp.balance
|
9
|
+
@balance = [balances[0].to_f, balances[1].to_f]
|
10
|
+
end
|
11
|
+
|
12
|
+
def validate_env
|
13
|
+
validate_keys
|
14
|
+
Bitstamp.setup do |config|
|
15
|
+
config.client_id = ENV["BITSTAMP_CLIENT_ID"]
|
16
|
+
config.key = ENV["BITSTAMP_KEY"]
|
17
|
+
config.secret = ENV["BITSTAMP_SECRET"]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def exchange
|
22
|
+
:bitstamp
|
23
|
+
end
|
24
|
+
|
25
|
+
def price action
|
26
|
+
return @price if @price
|
27
|
+
action = {
|
28
|
+
buy: :ask,
|
29
|
+
sell: :bid,
|
30
|
+
}[action]
|
31
|
+
@price = Bitstamp.ticker.send(action).to_f
|
32
|
+
end
|
33
|
+
|
34
|
+
def trade action
|
35
|
+
price(action) unless @price #memoize
|
36
|
+
multiple = {
|
37
|
+
buy: 1,
|
38
|
+
sell: -1,
|
39
|
+
}[action]
|
40
|
+
bitstamp_options = {
|
41
|
+
price: (@price + 0.001 * multiple),
|
42
|
+
amount: @options[:volume],
|
43
|
+
}
|
44
|
+
Bitstamp.orders.send(action, bitstamp_options)
|
45
|
+
end
|
46
|
+
|
47
|
+
def transfer other_client
|
48
|
+
Bitstamp.transfer(@options[:volume], other_client.address)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module RbtcArbitrage
|
2
|
+
module Clients
|
3
|
+
class MtGoxClient
|
4
|
+
include RbtcArbitrage::Client
|
5
|
+
|
6
|
+
def balance
|
7
|
+
return @balance if @balance
|
8
|
+
balances = MtGox.balance
|
9
|
+
@balance = [balances[0].amount.to_f, balances[1].amount.to_f]
|
10
|
+
end
|
11
|
+
|
12
|
+
def validate_env
|
13
|
+
validate_keys
|
14
|
+
MtGox.configure do |config|
|
15
|
+
config.key = ENV["MTGOX_KEY"]
|
16
|
+
config.secret = ENV["MTGOX_SECRET"]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def exchange
|
21
|
+
:mtgox
|
22
|
+
end
|
23
|
+
|
24
|
+
# `action` is :buy or :sell
|
25
|
+
def price action
|
26
|
+
return @price if @price
|
27
|
+
action = {
|
28
|
+
buy: :sell,
|
29
|
+
sell: :buy,
|
30
|
+
}[action]
|
31
|
+
@price = MtGox.ticker.send(action)
|
32
|
+
end
|
33
|
+
|
34
|
+
# `action` is :buy or :sell
|
35
|
+
def trade action
|
36
|
+
action = "#{action.to_s}!".to_sym
|
37
|
+
MtGox.send(action, @options[:volume], :market)
|
38
|
+
end
|
39
|
+
|
40
|
+
def transfer other_client
|
41
|
+
MtGox.withdraw! @options[:volume], other_client.address
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -1,23 +1,34 @@
|
|
1
1
|
module RbtcArbitrage
|
2
2
|
class Trader
|
3
|
-
|
4
|
-
|
5
|
-
def initialize options={}
|
6
|
-
@stamp = {}
|
7
|
-
@mtgox = {}
|
8
|
-
@options = options
|
9
|
-
@options[:volume] ||= 0.01
|
10
|
-
@options[:cutoff] ||= 2
|
3
|
+
attr_reader :buy_client, :sell_client, :received
|
4
|
+
attr_accessor :buyer, :seller, :options
|
11
5
|
|
6
|
+
def initialize config={}
|
7
|
+
@buyer = {}
|
8
|
+
@seller = {}
|
9
|
+
@options = {}
|
10
|
+
set_key config, :volume, 0.01
|
11
|
+
set_key config, :cutoff, 2
|
12
|
+
set_key config, :logger, Logger.new(STDOUT)
|
13
|
+
set_key config, :verbose, true
|
14
|
+
set_key config, :live, false
|
15
|
+
exchange = config[:buyer] || :bitstamp
|
16
|
+
@buy_client = client_for_exchange(exchange)
|
17
|
+
exchange = config[:seller] || :mtgox
|
18
|
+
@sell_client = client_for_exchange(exchange)
|
12
19
|
self
|
13
20
|
end
|
14
21
|
|
22
|
+
def set_key config, key, default
|
23
|
+
@options[key] = config.has_key?(key) ? config[key] : default
|
24
|
+
end
|
25
|
+
|
15
26
|
def trade
|
16
27
|
fetch_prices
|
17
28
|
log_info if options[:verbose]
|
18
29
|
|
19
|
-
if options[:cutoff] > percent
|
20
|
-
raise SecurityError, "Exiting because real profit (#{percent.round(2)}%) is less than cutoff (#{options[:cutoff].round(2)}%)"
|
30
|
+
if options[:cutoff] > @percent && options[:live]
|
31
|
+
raise SecurityError, "Exiting because real profit (#{@percent.round(2)}%) is less than cutoff (#{options[:cutoff].round(2)}%)"
|
21
32
|
end
|
22
33
|
|
23
34
|
execute_trade if options[:live]
|
@@ -26,82 +37,67 @@ module RbtcArbitrage
|
|
26
37
|
end
|
27
38
|
|
28
39
|
def execute_trade
|
40
|
+
fetch_prices unless @paid
|
29
41
|
validate_env
|
30
|
-
get_balance
|
31
42
|
raise SecurityError, "--live flag is false. Not executing trade." unless options[:live]
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
puts "mtgox usd: $#{mtgox[:usd].round(2)} btc: #{mtgox[:usd].round(2)}"
|
36
|
-
end
|
37
|
-
if stamp[:price] < mtgox[:price]
|
38
|
-
if paid > stamp[:usd] || amount_to_buy > mtgox[:btc]
|
43
|
+
get_balance
|
44
|
+
if @percent > @options[:cutoff]
|
45
|
+
if @paid > buyer[:usd] || @options[:volume] > seller[:btc]
|
39
46
|
raise SecurityError, "Not enough funds. Exiting."
|
40
47
|
else
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
48
|
+
logger.info "Trading live!" if options[:verbose]
|
49
|
+
@buy_client.buy
|
50
|
+
@sell_client.sell
|
51
|
+
@buy_client.transfer @sell_client
|
45
52
|
end
|
46
53
|
else
|
47
|
-
|
48
|
-
raise SecurityError, "Not enough funds. Exiting."
|
49
|
-
else
|
50
|
-
puts "Trading live!" if options[:verbose]
|
51
|
-
MtGox.buy! amount_to_buy, :market
|
52
|
-
Bitstamp.orders.sell amount_to_buy, stamp[:price] - 0.001
|
53
|
-
MtGox.withdraw amount_to_buy, ENV['BITSTAMP_ADDRESS']
|
54
|
-
end
|
54
|
+
logger.info "Not trading live because cutoff is higher than profit." if @options[:verbose]
|
55
55
|
end
|
56
56
|
end
|
57
57
|
|
58
58
|
def fetch_prices
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
prices = [
|
63
|
-
@paid = prices.min * 1.005 *
|
64
|
-
@received = prices.max * 0.994 *
|
65
|
-
@percent = ((received
|
59
|
+
logger.info "Fetching exchange rates" if @options[:verbose]
|
60
|
+
buyer[:price] = @buy_client.price(:buy)
|
61
|
+
seller[:price] = @sell_client.price(:sell)
|
62
|
+
prices = [buyer[:price], seller[:price]]
|
63
|
+
@paid = prices.min * 1.005 * @options[:volume]
|
64
|
+
@received = prices.max * 0.994 * @options[:volume]
|
65
|
+
@percent = ((received/@paid - 1) * 100).round(2)
|
66
66
|
end
|
67
67
|
|
68
68
|
def log_info
|
69
|
-
|
70
|
-
|
71
|
-
lower_ex
|
72
|
-
|
73
|
-
|
74
|
-
|
69
|
+
lower_ex = @buy_client.exchange.to_s.capitalize
|
70
|
+
higher_ex = @sell_client.exchange.to_s.capitalize
|
71
|
+
logger.info "#{lower_ex}: $#{buyer[:price].round(2)}"
|
72
|
+
logger.info "#{higher_ex}: $#{seller[:price].round(2)}"
|
73
|
+
logger.info "buying #{@options[:volume]} btc from #{lower_ex} for $#{@paid.round(2)}"
|
74
|
+
logger.info "selling #{@options[:volume]} btc on #{higher_ex} for $#{@received.round(2)}"
|
75
|
+
logger.info "profit: $#{(@received - @paid).round(2)} (#{@percent.round(2)}%)"
|
75
76
|
end
|
76
77
|
|
77
|
-
def
|
78
|
-
[
|
79
|
-
|
80
|
-
|
81
|
-
if ENV[key].blank?
|
82
|
-
raise ArgumentError, "Exiting because missing required ENV variable $#{key}."
|
83
|
-
end
|
84
|
-
end
|
85
|
-
end
|
78
|
+
def get_balance
|
79
|
+
@seller[:btc], @seller[:usd] = @sell_client.balance
|
80
|
+
@buyer[:btc], @buyer[:usd] = @buy_client.balance
|
81
|
+
end
|
86
82
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
end
|
83
|
+
def logger
|
84
|
+
@options[:logger]
|
85
|
+
end
|
91
86
|
|
92
|
-
|
93
|
-
|
94
|
-
|
87
|
+
def validate_env
|
88
|
+
[@sell_client, @buy_client].each do |client|
|
89
|
+
client.validate_env
|
95
90
|
end
|
96
91
|
end
|
97
92
|
|
98
|
-
def
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
93
|
+
def client_for_exchange market
|
94
|
+
market = market.to_sym unless market.is_a?(Symbol)
|
95
|
+
clazz = RbtcArbitrage::Clients.constants.find do |c|
|
96
|
+
clazz = RbtcArbitrage::Clients.const_get(c)
|
97
|
+
clazz.new.exchange == market
|
98
|
+
end
|
99
|
+
clazz = RbtcArbitrage::Clients.const_get(clazz)
|
100
|
+
clazz.new @options
|
105
101
|
end
|
106
102
|
end
|
107
103
|
end
|
data/lib/rbtc_arbitrage.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
require 'thor'
|
2
2
|
require 'mtgox'
|
3
|
+
require_relative 'rbtc_arbitrage/campbx.rb'
|
3
4
|
require 'bitstamp'
|
4
|
-
|
5
|
+
require 'btce'
|
6
|
+
Dir["#{File.dirname(__FILE__)}/rbtc_arbitrage/**/*.rb"].each { |f| require(f) }
|
5
7
|
|
6
8
|
module RbtcArbitrage
|
7
9
|
end
|
data/rbtc_arbitrage.gemspec
CHANGED
data/spec/cli_spec.rb
ADDED
data/spec/client_spec.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class FakeClient
|
4
|
+
include RbtcArbitrage::Client
|
5
|
+
|
6
|
+
def trade action
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
describe RbtcArbitrage::Client do
|
11
|
+
let(:client) { FakeClient.new }
|
12
|
+
it "aliases buy and sell" do
|
13
|
+
client.should_receive(:trade).with(:sell)
|
14
|
+
client.sell
|
15
|
+
client.should_receive(:trade).with(:buy)
|
16
|
+
client.buy
|
17
|
+
end
|
18
|
+
end
|