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.
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