bitbot-trader 0.0.1

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