rbtc_arbitrage_simple 1.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +23 -0
  5. data/Gemfile +18 -0
  6. data/Gemfile.lock +156 -0
  7. data/Guardfile +8 -0
  8. data/LICENSE.txt +22 -0
  9. data/Procfile +1 -0
  10. data/README.md +88 -0
  11. data/Rakefile +10 -0
  12. data/bin/rbtc_simple +4 -0
  13. data/dummy_web_server.rb +9 -0
  14. data/lib/rbtc_arbitrage.rb +15 -0
  15. data/lib/rbtc_arbitrage/campbx.rb +98 -0
  16. data/lib/rbtc_arbitrage/cli.rb +19 -0
  17. data/lib/rbtc_arbitrage/client.rb +48 -0
  18. data/lib/rbtc_arbitrage/clients/btce_client.rb +62 -0
  19. data/lib/rbtc_arbitrage/clients/campbx_client.rb +48 -0
  20. data/lib/rbtc_arbitrage/clients/client.rb.example +46 -0
  21. data/lib/rbtc_arbitrage/clients/coinbase_client.rb +63 -0
  22. data/lib/rbtc_arbitrage/clients/mtgox_client.rb +56 -0
  23. data/lib/rbtc_arbitrage/trader.rb +123 -0
  24. data/lib/rbtc_arbitrage/version.rb +3 -0
  25. data/rbtc_arbitrage.gemspec +30 -0
  26. data/spec/cli_spec.rb +8 -0
  27. data/spec/client_spec.rb +33 -0
  28. data/spec/clients/btce_client_spec.rb +93 -0
  29. data/spec/clients/campbx_client_spec.rb +66 -0
  30. data/spec/clients/coinbase_client_spec.rb +66 -0
  31. data/spec/clients/mtgox_client_spec.rb +53 -0
  32. data/spec/rbtc_arbitrage_spec.rb +11 -0
  33. data/spec/spec_helper.rb +34 -0
  34. data/spec/support/cassettes/RbtcArbitrage_Clients_BitstampClient/_balance/fetches_the_balance_correctly.yml +96 -0
  35. data/spec/support/cassettes/RbtcArbitrage_Clients_BitstampClient/_price/fetches_price_for_buy_correctly.yml +52 -0
  36. data/spec/support/cassettes/RbtcArbitrage_Clients_BitstampClient/_price/fetches_price_for_sell_correctly.yml +52 -0
  37. data/spec/support/cassettes/RbtcArbitrage_Clients_BitstampClient/_price/fetches_prices_correctly.yml +44 -0
  38. data/spec/support/cassettes/RbtcArbitrage_Clients_BtceClient/_balance/should_raise_if_bad_API_keys.yml +53 -0
  39. data/spec/support/cassettes/RbtcArbitrage_Clients_BtceClient/_price/calls_btc-e.yml +91 -0
  40. data/spec/support/cassettes/RbtcArbitrage_Clients_BtceClient/_price/fetches_price_for_buy_correctly.yml +47 -0
  41. data/spec/support/cassettes/RbtcArbitrage_Clients_BtceClient/_price/fetches_price_for_sell_correctly.yml +47 -0
  42. data/spec/support/cassettes/RbtcArbitrage_Clients_CampbxClient/_balance/fetches_the_balance_correctly.yml +95 -0
  43. data/spec/support/cassettes/RbtcArbitrage_Clients_CampbxClient/_price/fetches_price_for_buy_correctly.yml +49 -0
  44. data/spec/support/cassettes/RbtcArbitrage_Clients_CampbxClient/_price/fetches_price_for_sell_correctly.yml +49 -0
  45. data/spec/support/cassettes/RbtcArbitrage_Clients_CoinbaseClient/_balance/fetches_the_balance_correctly.yml +109 -0
  46. data/spec/support/cassettes/RbtcArbitrage_Clients_CoinbaseClient/_price/calls_coinbase.yml +215 -0
  47. data/spec/support/cassettes/RbtcArbitrage_Clients_CoinbaseClient/_price/fetches_price_for_buy_correctly.yml +56 -0
  48. data/spec/support/cassettes/RbtcArbitrage_Clients_CoinbaseClient/_price/fetches_price_for_sell_correctly.yml +56 -0
  49. data/spec/support/cassettes/RbtcArbitrage_Clients_MtGoxClient/_balance/fetches_the_balance_correctly.yml +77 -0
  50. data/spec/support/cassettes/RbtcArbitrage_Clients_MtGoxClient/_price/fetches_price_for_buy_correctly.yml +44 -0
  51. data/spec/support/cassettes/RbtcArbitrage_Clients_MtGoxClient/_price/fetches_price_for_sell_correctly.yml +44 -0
  52. data/spec/support/cassettes/RbtcArbitrage_Clients_MtGoxClient/_price/fetches_prices_correctly.yml +85 -0
  53. data/spec/support/cassettes/RbtcArbitrage_Trader/_execute_trade/should_raise_SecurityError_if_not_live.yml +88 -0
  54. data/spec/support/cassettes/RbtcArbitrage_Trader/_execute_trade/when_live/raises_SecurityError_if_not_enough_BTC.yml +81 -0
  55. data/spec/support/cassettes/RbtcArbitrage_Trader/_execute_trade/when_live/raises_SecurityError_if_not_enough_USD.yml +81 -0
  56. data/spec/support/cassettes/RbtcArbitrage_Trader/_execute_trade/when_live/should_fetch_balance.yml +88 -0
  57. data/spec/support/cassettes/RbtcArbitrage_Trader/_execute_trade/when_live/shouldn_t_raise_security_error.yml +166 -0
  58. data/spec/support/cassettes/RbtcArbitrage_Trader/_fetch_prices/gets_the_right_price_set.yml +173 -0
  59. data/spec/trader_spec.rb +228 -0
  60. metadata +249 -0
@@ -0,0 +1,19 @@
1
+ module RbtcArbitrage
2
+ class CLI < Thor
3
+
4
+ desc "arbitrage", "Get information about the current arbitrage levels."
5
+ option :live, type: :boolean, default: false, desc: "Execute live trades."
6
+ option :cutoff, type: :numeric, default: 2, desc: "The minimum profit level required to execute a trade."
7
+ option :volume, type: :numeric, default: 0.01, desc: "The amount of bitcoins to trade per transaction."
8
+ option :verbose, type: :boolean, default: true, desc: "Whether you wish to log information."
9
+ option :buyer, type: :string, default: "campbx"
10
+ option :seller, type: :string, default: "mtgox"
11
+ option :repeat, type: :numeric, default: nil
12
+ def trade
13
+ RbtcArbitrage::Trader.new(options).trade
14
+ end
15
+
16
+ default_task :trade
17
+
18
+ end
19
+ end
@@ -0,0 +1,48 @@
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 *args
18
+ args.each do |key|
19
+ key = key.to_s.upcase
20
+ if ENV[key].blank?
21
+ raise ArgumentError, "Exiting because missing required ENV variable $#{key}."
22
+ end
23
+ end
24
+ end
25
+
26
+ def buy
27
+ trade :buy
28
+ end
29
+
30
+ def sell
31
+ trade :sell
32
+ end
33
+
34
+ def address
35
+ ENV["#{exchange.to_s.upcase}_ADDRESS"]
36
+ end
37
+
38
+ def logger
39
+ @options[:logger]
40
+ end
41
+
42
+ private
43
+
44
+ def set_key config, key, default
45
+ @options[key] = config.has_key?(key) ? config[key] : default
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,62 @@
1
+ module RbtcArbitrage
2
+ module Clients
3
+ class BtceClient
4
+ include RbtcArbitrage::Client
5
+
6
+ def exchange
7
+ :btce
8
+ end
9
+
10
+ def balance
11
+ return @balance if @balance
12
+ begin
13
+ balances = interface.get_info["return"]["funds"]
14
+ @balance = [balances["btc"], balances["usd"]]
15
+ rescue NoMethodError => e
16
+ raise SecurityError, "Invalid API key for BTC-e"
17
+ end
18
+ end
19
+
20
+ def interface
21
+ end
22
+
23
+ def validate_env
24
+ validate_keys :btce_key, :btce_secret, :btce_address
25
+ end
26
+
27
+ def trade action
28
+ warning = "BTC-E does not support API bitcoin transfer. "
29
+ warning << "If you really want to trade, you will have "
30
+ warning << "to manually send bitcoin. Enter 'accept' to continue. \n> "
31
+ @options[:logger].warn warning if @options[:verbose]
32
+ return false unless gets.chomp == "accept"
33
+ opts = {
34
+ type: action,
35
+ rate: price(action),
36
+ amount: @options[:volume],
37
+ pair: "btc_usd"
38
+ }
39
+ interface.trade opts
40
+ end
41
+
42
+ def price action
43
+ return @ticker[action.to_s] if @ticker
44
+ @ticker = Btce::Ticker.new("btc_usd").json["ticker"]
45
+ @ticker[action.to_s]
46
+ end
47
+
48
+ def transfer client
49
+ if @options[:verbose]
50
+ error = "BTC-E does not have a 'transfer' API.\n"
51
+ error << "You must transfer bitcoin manually."
52
+ @options[:logger].error error
53
+ end
54
+ end
55
+
56
+ def interface
57
+ opts = {key: ENV['BTCE_KEY'], secret: ENV['BTCE_SECRET']}
58
+ @interface ||= Btce::TradeAPI.new(opts)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,48 @@
1
+ module RbtcArbitrage
2
+ module Clients
3
+ class CampbxClient
4
+ include RbtcArbitrage::Client
5
+
6
+ def exchange
7
+ :campbx
8
+ end
9
+
10
+ def balance
11
+ return @balance if @balance
12
+ funds = interface.my_funds
13
+ [funds["Total BTC"].to_f, funds["Total USD"].to_f]
14
+ end
15
+
16
+ def interface
17
+ @interface ||= CampBX::API.new(ENV['CAMPBX_KEY'],ENV['CAMPBX_SECRET'])
18
+ end
19
+
20
+ def validate_env
21
+ validate_keys :campbx_key, :campbx_secret
22
+
23
+ end
24
+
25
+ def trade action
26
+ trade_mode = "Quick#{action.to_s.capitalize}"
27
+ interface.trade_enter trade_mode, @options[:volume], price(action)
28
+ end
29
+
30
+ def price action
31
+ return @price if @price
32
+ action = {
33
+ buy: "Best Ask",
34
+ sell: "Best Bid",
35
+ }[action]
36
+ @price = interface.xticker[action].to_f
37
+ end
38
+
39
+ def transfer client
40
+ interface.send_btc client.address, @options[:volume]
41
+ end
42
+
43
+ def address
44
+ @address ||= interface.get_btc_address
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,46 @@
1
+ module RbtcArbitrage
2
+ module Clients
3
+ class ExchangeClient
4
+ include RbtcArbitrage::Client
5
+
6
+ # return a symbol as the name
7
+ # of this exchange
8
+ def exchange
9
+ end
10
+
11
+ # Returns an array of Floats.
12
+ # The first element is the balance in BTC;
13
+ # The second is in USD.
14
+ def balance
15
+ end
16
+
17
+ def interface
18
+ end
19
+
20
+ # Configures the client's API keys.
21
+ def validate_env
22
+ end
23
+
24
+ # `action` is :buy or :sell
25
+ def trade action
26
+ end
27
+
28
+ # `action` is :buy or :sell
29
+ # Returns a Numeric type.
30
+ def price action
31
+ end
32
+
33
+ # Transfers BTC to the address of a different
34
+ # exchange.
35
+ def transfer client
36
+ end
37
+
38
+ # If there is an API method to fetch your
39
+ # BTC address, implement this, otherwise
40
+ # remove this method and set the ENV
41
+ # variable [this-exchange-name-in-caps]_ADDRESS
42
+ def address
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,63 @@
1
+ module RbtcArbitrage
2
+ module Clients
3
+ class CoinbaseClient
4
+ include RbtcArbitrage::Client
5
+
6
+ # return a symbol as the name
7
+ # of this exchange
8
+ def exchange
9
+ :coinbase
10
+ end
11
+
12
+ # Returns an array of Floats.
13
+ # The first element is the balance in BTC;
14
+ # The second is in USD.
15
+ def balance
16
+ if @options[:verbose]
17
+ warning = "Coinbase doesn't provide a USD balance because"
18
+ warning << " it connects to your bank account. Be careful, "
19
+ warning << "because this will withdraw directly from your accounts."
20
+ logger.warn warning
21
+ end
22
+ @balance ||= [interface.balance.to_f, max_float]
23
+ end
24
+
25
+ # Configures the client's API keys.
26
+ def validate_env
27
+ validate_keys :coinbase_key, :coinbase_address
28
+ end
29
+
30
+ # `action` is :buy or :sell
31
+ def trade action
32
+ interface.send("#{action}!".to_sym, @options[:volume])
33
+ end
34
+
35
+ # `action` is :buy or :sell
36
+ # Returns a Numeric type.
37
+ def price action
38
+ method = "#{action}_price".to_sym
39
+ @price ||= interface.send(method).to_f
40
+ end
41
+
42
+ # Transfers BTC to the address of a different
43
+ # exchange.
44
+ def transfer client
45
+ interface.send_money client.address, @options[:volume]
46
+ end
47
+
48
+ def interface
49
+ @interface ||= Coinbase::Client.new(ENV['COINBASE_KEY'])
50
+ end
51
+
52
+ def address
53
+ @address ||= interface.receive_address.address
54
+ end
55
+
56
+ private
57
+
58
+ def max_float
59
+ Float::MAX
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,56 @@
1
+ module RbtcArbitrage
2
+ module Clients
3
+ class MtGoxClient
4
+ include RbtcArbitrage::Client
5
+
6
+ ##
7
+ # Returns an array of Floats.
8
+ # The first element is the balance in BTC;
9
+ # The second is in USD.
10
+ def balance
11
+ return @balance if @balance
12
+ balances = MtGox.balance
13
+ @balance = [balances[0].amount.to_f, balances[1].amount.to_f]
14
+ end
15
+
16
+ ##
17
+ # Configures the MtGox
18
+ # client's API keys.
19
+ def validate_env
20
+ validate_keys :mtgox_key, :mtgox_secret, :mtgox_address
21
+ MtGox.configure do |config|
22
+ config.key = ENV["MTGOX_KEY"]
23
+ config.secret = ENV["MTGOX_SECRET"]
24
+ end
25
+ end
26
+
27
+ def exchange
28
+ :mtgox
29
+ end
30
+
31
+ # `action` is :buy or :sell
32
+ # Returns a Numeric type.
33
+ def price action
34
+ return @price if @price
35
+ action = {
36
+ buy: :sell,
37
+ sell: :buy,
38
+ }[action]
39
+ @price = MtGox.ticker.send(action)
40
+ end
41
+
42
+ # `action` is :buy or :sell
43
+ def trade action
44
+ action = "#{action.to_s}!".to_sym
45
+ MtGox.send(action, @options[:volume], :market)
46
+ end
47
+
48
+ ##
49
+ # Transfers BTC to the address of a different
50
+ # exchange.
51
+ def transfer other_client
52
+ MtGox.withdraw! @options[:volume], other_client.address
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,123 @@
1
+ module RbtcArbitrage
2
+ class Trader
3
+ attr_reader :buy_client, :sell_client, :received
4
+ attr_accessor :buyer, :seller, :options
5
+
6
+ def initialize config={}
7
+ opts = {}
8
+ config.each do |key, val|
9
+ opts[(key.to_sym rescue key) || key] = val
10
+ end
11
+ @buyer = {}
12
+ @seller = {}
13
+ @options = {}
14
+ set_key opts, :volume, 0.01
15
+ set_key opts, :cutoff, 2
16
+ set_key opts, :logger, Logger.new(STDOUT)
17
+ set_key opts, :verbose, true
18
+ set_key opts, :live, false
19
+ set_key opts, :repeat, nil
20
+ exchange = opts[:buyer] || :campbx
21
+ @buy_client = client_for_exchange(exchange)
22
+ exchange = opts[:seller] || :mtgox
23
+ @sell_client = client_for_exchange(exchange)
24
+ self
25
+ end
26
+
27
+ def set_key config, key, default
28
+ @options[key] = config.has_key?(key) ? config[key] : default
29
+ end
30
+
31
+ def trade
32
+ fetch_prices
33
+ log_info if options[:verbose]
34
+
35
+ if options[:live] && options[:cutoff] > @percent
36
+ raise SecurityError, "Exiting because real profit (#{@percent.round(2)}%) is less than cutoff (#{options[:cutoff].round(2)}%)"
37
+ end
38
+
39
+ execute_trade if options[:live]
40
+
41
+ if @options[:repeat]
42
+ trade_again
43
+ end
44
+ self
45
+ end
46
+
47
+ def trade_again
48
+ sleep @options[:repeat]
49
+ @logger.info " - " if @options[:verbose]
50
+ @buy_client = @buy_client.class.new(@options)
51
+ @sell_client = @sell_client.class.new(@options)
52
+ trade
53
+ end
54
+
55
+ def execute_trade
56
+ fetch_prices unless @paid
57
+ validate_env
58
+ raise SecurityError, "--live flag is false. Not executing trade." unless options[:live]
59
+ get_balance
60
+ if @percent > @options[:cutoff]
61
+ if @paid > buyer[:usd] || @options[:volume] > seller[:btc]
62
+ raise SecurityError, "Not enough funds. Exiting."
63
+ else
64
+ logger.info "Trading live!" if options[:verbose]
65
+ @buy_client.buy
66
+ @sell_client.sell
67
+ @buy_client.transfer @sell_client
68
+ end
69
+ else
70
+ logger.info "Not trading live because cutoff is higher than profit." if @options[:verbose]
71
+ end
72
+ end
73
+
74
+ def fetch_prices
75
+ logger.info "Fetching exchange rates" if @options[:verbose]
76
+ buyer[:price] = @buy_client.price(:buy)
77
+ seller[:price] = @sell_client.price(:sell)
78
+ prices = [buyer[:price], seller[:price]]
79
+ @paid = buyer[:price] * 1.006 * @options[:volume]
80
+ @received = seller[:price] * 0.994 * @options[:volume]
81
+ @percent = ((received/@paid - 1) * 100).round(2)
82
+ end
83
+
84
+ def log_info
85
+ lower_ex = @buy_client.exchange.to_s.capitalize
86
+ higher_ex = @sell_client.exchange.to_s.capitalize
87
+ logger.info "#{lower_ex}: $#{buyer[:price].round(2)}"
88
+ logger.info "#{higher_ex}: $#{seller[:price].round(2)}"
89
+ logger.info "buying #{@options[:volume]} btc from #{lower_ex} for $#{@paid.round(2)}"
90
+ logger.info "selling #{@options[:volume]} btc on #{higher_ex} for $#{@received.round(2)}"
91
+ logger.info "profit: $#{(@received - @paid).round(2)} (#{@percent.round(2)}%)"
92
+ end
93
+
94
+ def get_balance
95
+ @seller[:btc], @seller[:usd] = @sell_client.balance
96
+ @buyer[:btc], @buyer[:usd] = @buy_client.balance
97
+ end
98
+
99
+ def logger
100
+ @options[:logger]
101
+ end
102
+
103
+ def validate_env
104
+ [@sell_client, @buy_client].each do |client|
105
+ client.validate_env
106
+ end
107
+ end
108
+
109
+ def client_for_exchange market
110
+ market = market.to_sym unless market.is_a?(Symbol)
111
+ clazz = RbtcArbitrage::Clients.constants.find do |c|
112
+ clazz = RbtcArbitrage::Clients.const_get(c)
113
+ clazz.new.exchange == market
114
+ end
115
+ begin
116
+ clazz = RbtcArbitrage::Clients.const_get(clazz)
117
+ clazz.new @options
118
+ rescue TypeError => e
119
+ raise ArgumentError, "Invalid exchange - '#{market}'"
120
+ end
121
+ end
122
+ end
123
+ end