rbtc_arbitrage_simple 1.2.3

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