bitbot-trader 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/Gemfile +10 -0
- data/Gemfile.devtools +55 -0
- data/LICENSE.txt +22 -0
- data/README.md +28 -0
- data/Rakefile +4 -0
- data/bitbot-trader.gemspec +22 -0
- data/config/devtools.yml +4 -0
- data/config/flay.yml +3 -0
- data/config/flog.yml +2 -0
- data/config/mutant.yml +3 -0
- data/config/reek.yml +103 -0
- data/config/yardstick.yml +2 -0
- data/examples/account_info.rb +16 -0
- data/examples/open_orders.rb +16 -0
- data/lib/bitbot/trader.rb +32 -0
- data/lib/bitbot/trader/account.rb +29 -0
- data/lib/bitbot/trader/amount.rb +15 -0
- data/lib/bitbot/trader/api_methods.rb +33 -0
- data/lib/bitbot/trader/open_order.rb +29 -0
- data/lib/bitbot/trader/price.rb +15 -0
- data/lib/bitbot/trader/provider.rb +19 -0
- data/lib/bitbot/trader/providers/bitstamp.rb +29 -0
- data/lib/bitbot/trader/providers/bitstamp/http_client.rb +101 -0
- data/lib/bitbot/trader/providers/bitstamp/open_orders_parser.rb +33 -0
- data/lib/bitbot/trader/providers/bitstamp/open_orders_request.rb +25 -0
- data/lib/bitbot/trader/providers/mt_gox.rb +29 -0
- data/lib/bitbot/trader/providers/mt_gox/account_info_parser.rb +45 -0
- data/lib/bitbot/trader/providers/mt_gox/account_info_request.rb +24 -0
- data/lib/bitbot/trader/providers/mt_gox/http_client.rb +98 -0
- data/lib/bitbot/trader/providers/mt_gox/http_client/hmac_middleware.rb +118 -0
- data/lib/bitbot/trader/providers/mt_gox/open_orders_parser.rb +35 -0
- data/lib/bitbot/trader/providers/mt_gox/open_orders_request.rb +25 -0
- data/lib/bitbot/trader/providers/mt_gox/value_with_currency_coercer.rb +28 -0
- data/lib/bitbot/trader/request.rb +41 -0
- data/lib/bitbot/trader/utils/nonce_generator.rb +21 -0
- data/lib/bitbot/trader/version.rb +5 -0
- data/lib/bitbot/trader/wallet.rb +25 -0
- data/spec/integration/bitstamp/open_orders_spec.rb +28 -0
- data/spec/integration/mt_gox/account_spec.rb +28 -0
- data/spec/integration/mt_gox/open_orders_spec.rb +29 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/support/big_decimal_matcher.rb +5 -0
- data/spec/support/http_connection_helpers.rb +15 -0
- data/spec/support/http_request_mock.rb +7 -0
- data/spec/support/provider_mock.rb +5 -0
- data/spec/unit/bitbot/trader/account_spec.rb +28 -0
- data/spec/unit/bitbot/trader/api_methods_spec.rb +43 -0
- data/spec/unit/bitbot/trader/open_order_spec.rb +19 -0
- data/spec/unit/bitbot/trader/provider_spec.rb +18 -0
- data/spec/unit/bitbot/trader/providers/bitstamp/http_client_spec.rb +75 -0
- data/spec/unit/bitbot/trader/providers/bitstamp/open_order_parser_spec.rb +69 -0
- data/spec/unit/bitbot/trader/providers/bitstamp/open_orders_request_spec.rb +24 -0
- data/spec/unit/bitbot/trader/providers/bitstamp_spec.rb +45 -0
- data/spec/unit/bitbot/trader/providers/mt_gox/account_info_parser_spec.rb +58 -0
- data/spec/unit/bitbot/trader/providers/mt_gox/account_info_request_spec.rb +24 -0
- data/spec/unit/bitbot/trader/providers/mt_gox/http_client/hmac_middleware_spec.rb +55 -0
- data/spec/unit/bitbot/trader/providers/mt_gox/http_client_spec.rb +100 -0
- data/spec/unit/bitbot/trader/providers/mt_gox/open_order_parser_spec.rb +95 -0
- data/spec/unit/bitbot/trader/providers/mt_gox/open_orders_request_spec.rb +24 -0
- data/spec/unit/bitbot/trader/providers/mt_gox/value_with_currency_coercer_spec.rb +21 -0
- data/spec/unit/bitbot/trader/providers/mt_gox_spec.rb +26 -0
- data/spec/unit/bitbot/trader/request_spec.rb +39 -0
- data/spec/unit/bitbot/trader/utils/nonce_generator_spec.rb +19 -0
- 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,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
|
data/spec/spec_helper.rb
ADDED
@@ -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,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
|