bitbot-trader 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +7 -0
  6. data/Gemfile +10 -0
  7. data/Gemfile.devtools +55 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +28 -0
  10. data/Rakefile +4 -0
  11. data/bitbot-trader.gemspec +22 -0
  12. data/config/devtools.yml +4 -0
  13. data/config/flay.yml +3 -0
  14. data/config/flog.yml +2 -0
  15. data/config/mutant.yml +3 -0
  16. data/config/reek.yml +103 -0
  17. data/config/yardstick.yml +2 -0
  18. data/examples/account_info.rb +16 -0
  19. data/examples/open_orders.rb +16 -0
  20. data/lib/bitbot/trader.rb +32 -0
  21. data/lib/bitbot/trader/account.rb +29 -0
  22. data/lib/bitbot/trader/amount.rb +15 -0
  23. data/lib/bitbot/trader/api_methods.rb +33 -0
  24. data/lib/bitbot/trader/open_order.rb +29 -0
  25. data/lib/bitbot/trader/price.rb +15 -0
  26. data/lib/bitbot/trader/provider.rb +19 -0
  27. data/lib/bitbot/trader/providers/bitstamp.rb +29 -0
  28. data/lib/bitbot/trader/providers/bitstamp/http_client.rb +101 -0
  29. data/lib/bitbot/trader/providers/bitstamp/open_orders_parser.rb +33 -0
  30. data/lib/bitbot/trader/providers/bitstamp/open_orders_request.rb +25 -0
  31. data/lib/bitbot/trader/providers/mt_gox.rb +29 -0
  32. data/lib/bitbot/trader/providers/mt_gox/account_info_parser.rb +45 -0
  33. data/lib/bitbot/trader/providers/mt_gox/account_info_request.rb +24 -0
  34. data/lib/bitbot/trader/providers/mt_gox/http_client.rb +98 -0
  35. data/lib/bitbot/trader/providers/mt_gox/http_client/hmac_middleware.rb +118 -0
  36. data/lib/bitbot/trader/providers/mt_gox/open_orders_parser.rb +35 -0
  37. data/lib/bitbot/trader/providers/mt_gox/open_orders_request.rb +25 -0
  38. data/lib/bitbot/trader/providers/mt_gox/value_with_currency_coercer.rb +28 -0
  39. data/lib/bitbot/trader/request.rb +41 -0
  40. data/lib/bitbot/trader/utils/nonce_generator.rb +21 -0
  41. data/lib/bitbot/trader/version.rb +5 -0
  42. data/lib/bitbot/trader/wallet.rb +25 -0
  43. data/spec/integration/bitstamp/open_orders_spec.rb +28 -0
  44. data/spec/integration/mt_gox/account_spec.rb +28 -0
  45. data/spec/integration/mt_gox/open_orders_spec.rb +29 -0
  46. data/spec/spec_helper.rb +18 -0
  47. data/spec/support/big_decimal_matcher.rb +5 -0
  48. data/spec/support/http_connection_helpers.rb +15 -0
  49. data/spec/support/http_request_mock.rb +7 -0
  50. data/spec/support/provider_mock.rb +5 -0
  51. data/spec/unit/bitbot/trader/account_spec.rb +28 -0
  52. data/spec/unit/bitbot/trader/api_methods_spec.rb +43 -0
  53. data/spec/unit/bitbot/trader/open_order_spec.rb +19 -0
  54. data/spec/unit/bitbot/trader/provider_spec.rb +18 -0
  55. data/spec/unit/bitbot/trader/providers/bitstamp/http_client_spec.rb +75 -0
  56. data/spec/unit/bitbot/trader/providers/bitstamp/open_order_parser_spec.rb +69 -0
  57. data/spec/unit/bitbot/trader/providers/bitstamp/open_orders_request_spec.rb +24 -0
  58. data/spec/unit/bitbot/trader/providers/bitstamp_spec.rb +45 -0
  59. data/spec/unit/bitbot/trader/providers/mt_gox/account_info_parser_spec.rb +58 -0
  60. data/spec/unit/bitbot/trader/providers/mt_gox/account_info_request_spec.rb +24 -0
  61. data/spec/unit/bitbot/trader/providers/mt_gox/http_client/hmac_middleware_spec.rb +55 -0
  62. data/spec/unit/bitbot/trader/providers/mt_gox/http_client_spec.rb +100 -0
  63. data/spec/unit/bitbot/trader/providers/mt_gox/open_order_parser_spec.rb +95 -0
  64. data/spec/unit/bitbot/trader/providers/mt_gox/open_orders_request_spec.rb +24 -0
  65. data/spec/unit/bitbot/trader/providers/mt_gox/value_with_currency_coercer_spec.rb +21 -0
  66. data/spec/unit/bitbot/trader/providers/mt_gox_spec.rb +26 -0
  67. data/spec/unit/bitbot/trader/request_spec.rb +39 -0
  68. data/spec/unit/bitbot/trader/utils/nonce_generator_spec.rb +19 -0
  69. metadata +166 -0
@@ -0,0 +1,118 @@
1
+ require "faraday"
2
+ require "base64"
3
+ require "digest/sha2"
4
+ require "digest/hmac"
5
+
6
+ module Bitbot
7
+ module Trader
8
+ module Providers
9
+ class MtGox
10
+ class HttpClient
11
+ # Signs request with hmac signature
12
+ #
13
+ class HmacMiddleware < Faraday::Middleware
14
+ # Extracts headers, path and body
15
+ #
16
+ # @param [#[]] env
17
+ # must respond to request_headers, url and body
18
+ #
19
+ # @return [Array]
20
+ #
21
+ # @api private
22
+ #
23
+ def self.extract_params(env)
24
+ headers = env[:request_headers]
25
+ path = env[:url].to_s.split("/2/").last
26
+ body = env[:body]
27
+
28
+ [headers, path, body]
29
+ end
30
+
31
+ # API key
32
+ #
33
+ # @return [String]
34
+ #
35
+ # @api private
36
+ #
37
+ attr_reader :key
38
+
39
+ # API secret
40
+ #
41
+ # @return [String]
42
+ #
43
+ # @api private
44
+ #
45
+ attr_reader :secret
46
+
47
+ # Initializes HmacMiddleware
48
+ #
49
+ # @param [Object] app
50
+ # @param [Hash] options
51
+ #
52
+ # @return [undefined]
53
+ #
54
+ # @api private
55
+ #
56
+ def initialize(app, options)
57
+ super(app)
58
+ @key = options.fetch(:key)
59
+ @secret = options.fetch(:secret)
60
+ end
61
+
62
+ # Adds signature to current request
63
+ #
64
+ # @param [Object] env
65
+ #
66
+ # @return [Object]
67
+ #
68
+ # @api private
69
+ #
70
+ def call(env)
71
+ headers, path, body = self.class.extract_params(env)
72
+ prepare_headers(headers, path, body)
73
+ @app.call(env)
74
+ end
75
+
76
+ private
77
+
78
+ # Adds signature to headers
79
+ #
80
+ # @param [Hash] headers
81
+ # @param [String] path
82
+ # @param [String] body
83
+ #
84
+ # @return [undefined]
85
+ #
86
+ # @api private
87
+ #
88
+ def prepare_headers(headers, path, body)
89
+ headers.merge!(
90
+ "Rest-Key" => @key,
91
+ "Rest-Sign" => generate_signature(path, body),
92
+ "User-Agent" => "bitbot-trader"
93
+ )
94
+ end
95
+
96
+ # Generates signature
97
+ #
98
+ # @param [String] path
99
+ # @param [String] body
100
+ #
101
+ # @return [String] singature
102
+ #
103
+ # @api private
104
+ #
105
+ def generate_signature(path, body)
106
+ secret = Base64.strict_decode64(@secret)
107
+ data = path + 0.chr + body
108
+ hmac = Digest::HMAC.digest(data, secret, Digest::SHA512)
109
+ Base64.strict_encode64(hmac)
110
+ end
111
+ end
112
+
113
+ Faraday::Request.register_middleware hmac: HmacMiddleware
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,35 @@
1
+ module Bitbot
2
+ module Trader
3
+ module Providers
4
+ class MtGox
5
+ # Parses raw open orders
6
+ #
7
+ class OpenOrderParser
8
+ include Virtus.model
9
+
10
+ attribute :oid, String
11
+ attribute :currency, String
12
+ attribute :type, String
13
+ attribute :effective_amount, Amount, coercer: ValueWithCurrencyCoercer
14
+ attribute :price, Price, coercer: ValueWithCurrencyCoercer
15
+ attribute :type, String
16
+
17
+ # Makes raw open order hash into OpenOrder object
18
+ #
19
+ # @return [OpenOrder]
20
+ #
21
+ # @api private
22
+ #
23
+ def parse
24
+ OpenOrder.new(
25
+ id: oid,
26
+ price: price,
27
+ amount: effective_amount,
28
+ bid: type == "bid"
29
+ )
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,25 @@
1
+ module Bitbot
2
+ module Trader
3
+ module Providers
4
+ class MtGox
5
+ # POST request to /money/orders
6
+ #
7
+ # @see https://en.bitcoin.it/wiki/MtGox/API/HTTP/v1
8
+ #
9
+ class OpenOrdersRequest < Request
10
+ # Fetches user's open orders
11
+ #
12
+ # @return [Array<OpenOrder>]
13
+ #
14
+ # @api private
15
+ #
16
+ def call
17
+ client.post("money/orders")["data"].map { |raw_order|
18
+ OpenOrderParser.new(raw_order).parse
19
+ }
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,28 @@
1
+ module Bitbot
2
+ module Trader
3
+ module Providers
4
+ class MtGox
5
+ # Helps managing values with currencies
6
+ #
7
+ class ValueWithCurrencyCoercer < Virtus::Attribute
8
+ # Parses value with currency hash
9
+ #
10
+ # @param [Hash] data
11
+ #
12
+ # @param [Integer] decimal_point
13
+ # MtGox sends integer values. We use this to divide that value.
14
+ #
15
+ # @return [Hash]
16
+ #
17
+ # @api private
18
+ #
19
+ def self.call(data)
20
+ value, currency = data.values_at("value_int", "currency")
21
+ decimal_point = currency == "BTC" ? 8 : 5
22
+ {value: BigDecimal(value) / (10 ** decimal_point), currency: currency}
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,41 @@
1
+ module Bitbot
2
+ module Trader
3
+ # Request class for different api methods
4
+ #
5
+ # @abstract
6
+ #
7
+ class Request
8
+ # Object that communicates with external API
9
+ #
10
+ # @return [Object]
11
+ #
12
+ # @api private
13
+ #
14
+ attr_reader :client
15
+
16
+ # Initializes request object
17
+ #
18
+ # @param [Object] client
19
+ #
20
+ # @return [undefined]
21
+ #
22
+ # @api private
23
+ #
24
+ def initialize(client)
25
+ @client = client
26
+ end
27
+
28
+ # Every request object implements call
29
+ #
30
+ # @abstract
31
+ #
32
+ # @return [undefined]
33
+ #
34
+ # @api private
35
+ #
36
+ def call(*)
37
+ raise NotImplementedError
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,21 @@
1
+ module Bitbot
2
+ module Trader
3
+ module Utils
4
+ # Generates a nonce
5
+ #
6
+ # Some http requests use it to prevent double http requests
7
+ #
8
+ class NonceGenerator
9
+ # Generates a nonce from current time
10
+ #
11
+ # @return [Fixnum] nonce
12
+ #
13
+ # @api private
14
+ #
15
+ def self.generate(time_class = Time)
16
+ time_class.now.to_i
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,5 @@
1
+ module Bitbot
2
+ module Trader
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,25 @@
1
+ require "virtus"
2
+
3
+ module Bitbot
4
+ module Trader
5
+ # User USD/BTC/etc wallet
6
+ #
7
+ class Wallet
8
+ include Virtus.model
9
+
10
+ attribute :value, BigDecimal
11
+ attribute :currency, String
12
+
13
+ # Shows wallet balance
14
+ #
15
+ # @example
16
+ # wallet.balance #=> <BigDecimal>
17
+ #
18
+ # @return [BigDecimal]
19
+ #
20
+ # @api public
21
+ #
22
+ alias :balance :value
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,28 @@
1
+ require "spec_helper"
2
+
3
+ describe Providers::Bitstamp, "#open_orders" do
4
+ let(:result) {
5
+ %![{"price": "20.10", "amount": "10.00000000", "type": 0, "id": 2826860, "datetime": "2013-04-20 12:12:14"}]!
6
+ }
7
+
8
+ let(:provider) { described_class.new({}, client) }
9
+
10
+ let(:client) { described_class::HttpClient.new(connection, "double", "double") }
11
+ let(:connection) { mock_connection("/open_orders/", result) }
12
+
13
+ it "parses open orders" do
14
+ open_orders = provider.open_orders
15
+ expect(open_orders.size).to be(1)
16
+
17
+ order = open_orders.first
18
+ expect(order.id).to eq("2826860")
19
+
20
+ amount = order.amount
21
+ expect(amount.value).to be_big_decimal(10)
22
+ expect(amount.currency).to eq("BTC")
23
+
24
+ price = order.price
25
+ expect(price.value).to be_big_decimal(20.10)
26
+ expect(price.currency).to eq("USD")
27
+ end
28
+ end
@@ -0,0 +1,28 @@
1
+ require "spec_helper"
2
+
3
+ describe Providers::MtGox, "#account" do
4
+ let(:result) {
5
+ %!{"result":"success","data":{"Login":"johndoe","Index":"193629","Id":"9b46b62e-4fd3-4cfa-9445-96f49dee846c","Rights":["get_info"],"Language":"en_US","Created":"2012-08-30 21:35:49","Last_Login":"2013-04-22 14:16:11","Wallets":{"BTC":{"Balance":{"value":"0.00000000","value_int":"0","display":"0.00000000\u00a0BTC","display_short":"0.00\u00a0BTC","currency":"BTC"},"Operations":164,"Daily_Withdraw_Limit":{"value":"100.00000000","value_int":"10000000000","display":"100.00000000\u00a0BTC","display_short":"100.00\u00a0BTC","currency":"BTC"},"Monthly_Withdraw_Limit":null,"Max_Withdraw":{"value":"100.00000000","value_int":"10000000000","display":"100.00000000\u00a0BTC","display_short":"100.00\u00a0BTC","currency":"BTC"},"Open_Orders":{"value":"0.00000000","value_int":"0","display":"0.00000000\u00a0BTC","display_short":"0.00\u00a0BTC","currency":"BTC"}},"EUR":{"Balance":{"value":"0.12314","value_int":"12314","display":"0.12314\u00a0\u20ac","display_short":"0.12\u00a0\u20ac","currency":"EUR"},"Operations":3,"Daily_Withdraw_Limit":{"value":"1000.00000","value_int":"100000000","display":"1,000.00000\u00a0\u20ac","display_short":"1,000.00\u00a0\u20ac","currency":"EUR"},"Monthly_Withdraw_Limit":{"value":"10000.00000","value_int":"1000000000","display":"10,000.00000\u00a0\u20ac","display_short":"10,000.00\u00a0\u20ac","currency":"EUR"},"Max_Withdraw":{"value":"1000.00000","value_int":"100000000","display":"1,000.00000\u00a0\u20ac","display_short":"1,000.00\u00a0\u20ac","currency":"EUR"},"Open_Orders":{"value":"0.00000","value_int":"0","display":"0.00000\u00a0\u20ac","display_short":"0.00\u00a0\u20ac","currency":"EUR"}},"USD":{"Balance":{"value":"3533.02728","value_int":"353302728","display":"$3,533.02728","display_short":"$3,533.03","currency":"USD"},"Operations":155,"Daily_Withdraw_Limit":{"value":"1000.00000","value_int":"100000000","display":"$1,000.00000","display_short":"$1,000.00","currency":"USD"},"Monthly_Withdraw_Limit":{"value":"10000.00000","value_int":"1000000000","display":"$10,000.00000","display_short":"$10,000.00","currency":"USD"},"Max_Withdraw":{"value":"1000.00000","value_int":"100000000","display":"$1,000.00000","display_short":"$1,000.00","currency":"USD"},"Open_Orders":{"value":"1053.50000","value_int":"105350000","display":"$1,053.50000","display_short":"$1,053.50","currency":"USD"}}},"Monthly_Volume":{"value":"413.48602792","value_int":"41348602792","display":"413.48602792\u00a0BTC","display_short":"413.49\u00a0BTC","currency":"BTC"},"Trade_Fee":0.53}}!
6
+ }
7
+
8
+ let(:provider) { described_class.new(options, client) }
9
+ let(:options) { {key: "double", secret: "double"} }
10
+
11
+ let(:client) { described_class::HttpClient.new(connection) }
12
+ let(:connection) { mock_connection("/money/info", result) }
13
+
14
+ it "parses account info" do
15
+ account = provider.account
16
+
17
+ expect(account.fee).to eq(0.53)
18
+
19
+ usd_wallet = account.wallet("USD")
20
+ expect(usd_wallet.balance).to eq(3533.02728)
21
+
22
+ eur_wallet = account.wallet("EUR")
23
+ expect(eur_wallet.balance).to eq(0.12314)
24
+
25
+ btc_wallet = account.wallet("BTC")
26
+ expect(btc_wallet.balance).to eq(0)
27
+ end
28
+ end
@@ -0,0 +1,29 @@
1
+ require "spec_helper"
2
+
3
+ describe Providers::MtGox, "#open_orders" do
4
+ let(:result) {
5
+ %!{"result":"success","data":[{"oid":"7c6d2237-52fb-4af4-b6ec-75e42f50c331","currency":"USD","item":"BTC","type":"bid","amount":{"value":"35.00000000","value_int":"3500000000","display":"35.00000000\u00a0BTC","display_short":"35.00\u00a0BTC","currency":"BTC"},"effective_amount":{"value":"35.00000000","value_int":"3500000000","display":"35.00000000\u00a0BTC","display_short":"35.00\u00a0BTC","currency":"BTC"},"price":{"value":"30.10000","value_int":"3010000","display":"$30.10000","display_short":"$30.10","currency":"USD"},"status":"open","date":1365886868,"priority":"1365886868485232","actions":[]}]}!
6
+ }
7
+
8
+ let(:provider) { described_class.new(options, client) }
9
+ let(:options) { {key: "double", secret: "double"} }
10
+
11
+ let(:client) { described_class::HttpClient.new(connection) }
12
+ let(:connection) { mock_connection("/money/orders", result) }
13
+
14
+ it "parses open orders" do
15
+ open_orders = provider.open_orders
16
+ expect(open_orders.size).to be(1)
17
+
18
+ order = open_orders.first
19
+ expect(order.id).to eq("7c6d2237-52fb-4af4-b6ec-75e42f50c331")
20
+
21
+ amount = order.amount
22
+ expect(amount.value).to eq(35)
23
+ expect(amount.currency).to eq("BTC")
24
+
25
+ price = order.price
26
+ expect(price.value).to eq(30.1)
27
+ expect(price.currency).to eq("USD")
28
+ end
29
+ end
@@ -0,0 +1,18 @@
1
+ if ENV["COVERAGE"] == "true"
2
+ require "simplecov"
3
+ require "coveralls"
4
+
5
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
6
+ SimpleCov::Formatter::HTMLFormatter,
7
+ Coveralls::SimpleCov::Formatter
8
+ ]
9
+
10
+ SimpleCov.start do
11
+ add_filter "spec"
12
+ end
13
+ end
14
+
15
+ require "bitbot/trader"
16
+
17
+ include Bitbot::Trader
18
+ require "devtools/spec_helper"
@@ -0,0 +1,5 @@
1
+ RSpec::Matchers.define :be_big_decimal do |expected|
2
+ match do |actual|
3
+ actual.is_a?(BigDecimal) && actual == BigDecimal(expected.to_s)
4
+ end
5
+ end
@@ -0,0 +1,15 @@
1
+ module HttpConnectionHelpers
2
+ def mock_connection(path, result)
3
+ Faraday.new { |builder|
4
+ builder.adapter :test do |stub|
5
+ stub.post(path) { [200, {}, result] }
6
+ end
7
+ }
8
+ end
9
+ end
10
+
11
+ RSpec.configure do |config|
12
+ config.include HttpConnectionHelpers, type: :integration, example_group: {
13
+ file_path: /spec\/integration/
14
+ }
15
+ end