rbtc_arbitrage 0.1.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/hstove/rbtc_arbitrage.png?branch=master)](https://travis-ci.org/hstove/rbtc_arbitrage)
|
16
|
+
[![Coverage Status](https://coveralls.io/repos/hstove/rbtc_arbitrage/badge.png)](https://coveralls.io/r/hstove/rbtc_arbitrage)
|
17
|
+
[![Code Climate](https://codeclimate.com/github/hstove/rbtc_arbitrage.png)](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
|