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.
Files changed (38) hide show
  1. checksums.yaml +6 -6
  2. data/.rspec +1 -1
  3. data/.travis.yml +13 -3
  4. data/Gemfile +10 -0
  5. data/Guardfile +8 -0
  6. data/README.md +41 -3
  7. data/btce-api-key.yml +2 -0
  8. data/lib/rbtc_arbitrage/campbx.rb +98 -0
  9. data/lib/rbtc_arbitrage/cli.rb +3 -1
  10. data/lib/rbtc_arbitrage/client.rb +45 -0
  11. data/lib/rbtc_arbitrage/clients/bitstamp_client.rb +52 -0
  12. data/lib/rbtc_arbitrage/clients/mtgox_client.rb +45 -0
  13. data/lib/rbtc_arbitrage/trader.rb +62 -66
  14. data/lib/rbtc_arbitrage/version.rb +1 -1
  15. data/lib/rbtc_arbitrage.rb +3 -1
  16. data/rbtc_arbitrage.gemspec +2 -0
  17. data/spec/cli_spec.rb +8 -0
  18. data/spec/client_spec.rb +18 -0
  19. data/spec/clients/bitstamp_client_spec.rb +53 -0
  20. data/spec/clients/mtgox_client_spec.rb +52 -0
  21. data/spec/spec_helper.rb +27 -12
  22. data/spec/support/cassettes/RbtcArbitrage_Clients_BitstampClient/_balance/fetches_the_balance_correctly.yml +96 -0
  23. data/spec/support/cassettes/RbtcArbitrage_Clients_BitstampClient/_price/fetches_price_for_buy_correctly.yml +52 -0
  24. data/spec/support/cassettes/RbtcArbitrage_Clients_BitstampClient/_price/fetches_price_for_sell_correctly.yml +52 -0
  25. data/spec/support/cassettes/RbtcArbitrage_Clients_BitstampClient/_price/fetches_prices_correctly.yml +44 -0
  26. data/spec/support/cassettes/RbtcArbitrage_Clients_MtGoxClient/_balance/fetches_the_balance_correctly.yml +77 -0
  27. data/spec/support/cassettes/RbtcArbitrage_Clients_MtGoxClient/_price/fetches_price_for_buy_correctly.yml +44 -0
  28. data/spec/support/cassettes/RbtcArbitrage_Clients_MtGoxClient/_price/fetches_price_for_sell_correctly.yml +44 -0
  29. data/spec/support/cassettes/RbtcArbitrage_Clients_MtGoxClient/_price/fetches_prices_correctly.yml +85 -0
  30. data/spec/support/cassettes/RbtcArbitrage_Trader/_execute_trade/should_raise_SecurityError_if_not_live.yml +93 -0
  31. data/spec/support/cassettes/RbtcArbitrage_Trader/_execute_trade/when_live/raises_SecurityError_if_not_enough_BTC.yml +86 -0
  32. data/spec/support/cassettes/RbtcArbitrage_Trader/_execute_trade/when_live/raises_SecurityError_if_not_enough_USD.yml +80 -0
  33. data/spec/support/cassettes/RbtcArbitrage_Trader/_execute_trade/when_live/should_fetch_balance.yml +225 -0
  34. data/spec/support/cassettes/RbtcArbitrage_Trader/_execute_trade/when_live/shouldn_t_raise_security_error.yml +262 -0
  35. data/spec/support/cassettes/RbtcArbitrage_Trader/_fetch_prices/gets_the_right_price_set.yml +176 -0
  36. data/spec/support/cassettes/RbtcArbitrage_Trader/_get_balance/fetches_the_right_balance.yml +166 -0
  37. data/spec/trader_spec.rb +156 -19
  38. metadata +89 -18
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: 8b73f2bb929386db4483f985d6e4ad1e1e630c0f
4
- data.tar.gz: 09877353240e3fad0adaa5a14f7a9d9b15951f76
5
- !binary "U0hBNTEy":
6
- metadata.gz: 3ea1ba33fd9e0cefc968e9af9c4a96b71af16d0ae2d8a8952b9928ba065f83f5f489576e902b20c2b55e2658f446b089fadf16d558e7a3574e52e0dc0d20d422
7
- data.tar.gz: 3267b9bf783cec825d6564b86910ee67715ab5c6aeca56d1fb3d9a90afe1d0a0e17c7f54659495a36f2188e4c92bd67220d1e1c95267533c7e17efc582810cc2
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 progress
2
+ --format d
data/.travis.yml CHANGED
@@ -1,5 +1,15 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.0.0
4
- - 1.9.3
5
- - 1.9.2
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 automating arbitrage between the MtGox and Bitstamp bitcoin exchanges.
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,2 @@
1
+ key: #{ENV['BTCE_KEY']}
2
+ secret: #{ENV['BTCE_SECRET']}
@@ -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
@@ -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
- attr_accessor :stamp, :mtgox, :paid, :received, :options, :percent, :amount_to_buy
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
- if options[:verbose]
33
- puts "Balances:"
34
- puts "stamp usd: $#{stamp[:usd].round(2)} btc: #{stamp[:usd].round(2)}"
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
- puts "Trading live!" if options[:verbose]
42
- Bitstamp.orders.buy amount_to_buy, stamp[:price] + 0.001
43
- MtGox.sell! amount_to_buy, :market
44
- Bitstamp.transfer amount_to_buy, ENV['MTGOX_ADDRESS']
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
- if paid > mtgox[:usd] || amount_to_buy > stamp[:btc]
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
- @amount_to_buy = options[:volume]
60
- stamp[:price] = Bitstamp.ticker.ask.to_f
61
- mtgox[:price] = MtGox.ticker.buy
62
- prices = [stamp[:price], mtgox[:price]]
63
- @paid = prices.min * 1.005 * amount_to_buy
64
- @received = prices.max * 0.994 * amount_to_buy
65
- @percent = ((received/paid - 1) * 100).round(2)
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
- puts "Bitstamp: $#{stamp[:price].round(2)}"
70
- puts "MtGox: $#{mtgox[:price].round(2)}"
71
- lower_ex, higher_ex = stamp[:price] < mtgox[:price] ? %w{Bitstamp, MtGox} : %w{MtGox, Bitstamp}
72
- puts "buying #{amount_to_buy} btc from #{lower_ex} for $#{paid.round(2)}"
73
- puts "selling #{amount_to_buy} btc on #{higher_ex} for $#{received.round(2)}"
74
- puts "profit: $#{(received - paid).round(2)} (#{percent.round(2)}%)"
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 validate_env
78
- ["KEY", "SECRET", "ADDRESS"].each do |suffix|
79
- ["MTGOX", "BITSTAMP"].each do |prefix|
80
- key = "#{prefix}_#{suffix}"
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
- MtGox.configure do |config|
88
- config.key = ENV["MTGOX_KEY"]
89
- config.secret = ENV["MTGOX_SECRET"]
90
- end
83
+ def logger
84
+ @options[:logger]
85
+ end
91
86
 
92
- Bitstamp.setup do |config|
93
- config.key = ENV["BITSTAMP_KEY"]
94
- config.secret = ENV["BITSTAMP_SECRET"]
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 get_balance
99
- balances = MtGox.balance
100
- @mtgox[:btc] = balances[0].amount.to_f
101
- @mtgox[:usd] = balances[1].amount.to_f
102
- balances = Bitstamp.balance if options[:live]
103
- @stamp[:usd] = balances["usd_available"].to_f
104
- @stamp[:btc] = balances["btc_available"].to_f
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
@@ -1,3 +1,3 @@
1
1
  module RbtcArbitrage
2
- VERSION = "0.1.0"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -1,7 +1,9 @@
1
1
  require 'thor'
2
2
  require 'mtgox'
3
+ require_relative 'rbtc_arbitrage/campbx.rb'
3
4
  require 'bitstamp'
4
- Dir[File.expand_path('../rbtc_arbitrage/*', __FILE__)].each { |f| require f }
5
+ require 'btce'
6
+ Dir["#{File.dirname(__FILE__)}/rbtc_arbitrage/**/*.rb"].each { |f| require(f) }
5
7
 
6
8
  module RbtcArbitrage
7
9
  end
@@ -26,4 +26,6 @@ Gem::Specification.new do |spec|
26
26
  spec.add_dependency "activemodel", ">= 3.1"
27
27
  spec.add_dependency "activesupport", ">= 3.1"
28
28
  spec.add_dependency "thor"
29
+ spec.add_dependency "btce"
30
+ spec.add_dependency "bitstamp"
29
31
  end
data/spec/cli_spec.rb ADDED
@@ -0,0 +1,8 @@
1
+ require 'spec_helper'
2
+
3
+ describe RbtcArbitrage::CLI do
4
+ it "calls trade on new trader" do
5
+ RbtcArbitrage::Trader.any_instance.should_receive(:trade)
6
+ RbtcArbitrage::CLI.new.trade
7
+ end
8
+ end
@@ -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