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.
- 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,28 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Account do
|
4
|
+
describe "#wallet" do
|
5
|
+
subject { described_class.new(wallets: wallets).wallet(currency) }
|
6
|
+
|
7
|
+
let(:wallets) {
|
8
|
+
[
|
9
|
+
{currency: "EUR", value: 123},
|
10
|
+
{currency: "BTC", value: 43}
|
11
|
+
]
|
12
|
+
}
|
13
|
+
|
14
|
+
context "when EUR" do
|
15
|
+
let(:currency) { "EUR" }
|
16
|
+
|
17
|
+
it { should be_a(Wallet) }
|
18
|
+
its(:balance) { should eq(123) }
|
19
|
+
end
|
20
|
+
|
21
|
+
context "when BTC" do
|
22
|
+
let(:currency) { "BTC" }
|
23
|
+
|
24
|
+
it { should be_a(Wallet) }
|
25
|
+
its(:balance) { should eq(43) }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe ApiMethods do
|
4
|
+
describe "#account" do
|
5
|
+
subject { object.account }
|
6
|
+
|
7
|
+
let(:object) { provider_double.new(client) }
|
8
|
+
let(:client) { double("client") }
|
9
|
+
|
10
|
+
let(:request_class) { Class.new }
|
11
|
+
let(:request) { double("request") }
|
12
|
+
|
13
|
+
before do
|
14
|
+
object.class.const_set("AccountInfoRequest", request_class)
|
15
|
+
request_class.stub(:new).with(client) { request }
|
16
|
+
end
|
17
|
+
|
18
|
+
it "calls request class" do
|
19
|
+
request.should_receive(:call)
|
20
|
+
subject
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "#open_orders" do
|
25
|
+
subject { object.open_orders }
|
26
|
+
|
27
|
+
let(:object) { provider_double.new(client) }
|
28
|
+
let(:client) { double("client") }
|
29
|
+
|
30
|
+
let(:request_class) { Class.new }
|
31
|
+
let(:request) { double("request") }
|
32
|
+
|
33
|
+
before do
|
34
|
+
object.class.const_set("OpenOrdersRequest", request_class)
|
35
|
+
request_class.stub(:new).with(client) { request }
|
36
|
+
end
|
37
|
+
|
38
|
+
it "calls request class" do
|
39
|
+
request.should_receive(:call)
|
40
|
+
subject
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe OpenOrder do
|
4
|
+
describe "ask?" do
|
5
|
+
subject { described_class.new(options).ask? }
|
6
|
+
|
7
|
+
context "when bid" do
|
8
|
+
let(:options) { {bid: true} }
|
9
|
+
|
10
|
+
it { should be(false) }
|
11
|
+
end
|
12
|
+
|
13
|
+
context "when ask" do
|
14
|
+
let(:options) { {bid: false} }
|
15
|
+
|
16
|
+
it { should be(true) }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Provider do
|
4
|
+
describe "#client" do
|
5
|
+
subject { klass.new(client).client }
|
6
|
+
|
7
|
+
let(:klass) {
|
8
|
+
Class.new(described_class) {
|
9
|
+
def initialize(client)
|
10
|
+
@client = client
|
11
|
+
end
|
12
|
+
}
|
13
|
+
}
|
14
|
+
let(:client) { double }
|
15
|
+
|
16
|
+
it { should be(client) }
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Providers::Bitstamp::HttpClient do
|
4
|
+
describe ".build" do
|
5
|
+
subject { described_class.build(options) }
|
6
|
+
|
7
|
+
let(:options) { {username: "double", password: "double"} }
|
8
|
+
let(:connection) { double }
|
9
|
+
|
10
|
+
before do
|
11
|
+
described_class.should_receive(:make_connection).
|
12
|
+
and_return(connection)
|
13
|
+
end
|
14
|
+
|
15
|
+
it { should be_a(described_class) }
|
16
|
+
its(:username) { should eq("double") }
|
17
|
+
its(:password) { should eq("double") }
|
18
|
+
its(:connection) { should be(connection) }
|
19
|
+
end
|
20
|
+
|
21
|
+
describe ".make_connection" do
|
22
|
+
subject(:connection) { described_class.make_connection }
|
23
|
+
|
24
|
+
it { should be_a(Faraday::Connection) }
|
25
|
+
|
26
|
+
it "has correct url_prefix" do
|
27
|
+
expect(subject.url_prefix.to_s + "/").to eq(described_class::HOST)
|
28
|
+
end
|
29
|
+
|
30
|
+
context "request handlers" do
|
31
|
+
subject { connection.builder.handlers }
|
32
|
+
|
33
|
+
it { should eq([
|
34
|
+
Faraday::Request::UrlEncoded,
|
35
|
+
Faraday::Adapter::NetHttp])
|
36
|
+
}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "#post" do
|
41
|
+
let(:client) { described_class.new(connection, "username", "password") }
|
42
|
+
let(:connection) { double("connection") }
|
43
|
+
let(:path) { "some_action" }
|
44
|
+
|
45
|
+
context "with default options" do
|
46
|
+
subject { client.post(path) }
|
47
|
+
|
48
|
+
let(:options) { {param: "value"} }
|
49
|
+
|
50
|
+
before do
|
51
|
+
connection.should_receive(:post).
|
52
|
+
with("some_action/", {user: "username", password: "password"}).
|
53
|
+
and_return(double(body: '{"result": "yey"}'))
|
54
|
+
end
|
55
|
+
|
56
|
+
it { should eq({"result" => "yey"}) }
|
57
|
+
end
|
58
|
+
|
59
|
+
context "with custom options" do
|
60
|
+
subject { client.post(path, options) }
|
61
|
+
|
62
|
+
let(:options) { {param: "value"} }
|
63
|
+
|
64
|
+
before do
|
65
|
+
connection.should_receive(:post).
|
66
|
+
with("some_action/", {
|
67
|
+
user: "username", password: "password", param: "value"
|
68
|
+
}).
|
69
|
+
and_return(double(body: '{"result": "yey"}'))
|
70
|
+
end
|
71
|
+
|
72
|
+
it { should eq({"result" => "yey"}) }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Providers::Bitstamp::OpenOrderParser do
|
4
|
+
describe"#parse" do
|
5
|
+
subject(:open_order) { parser.parse }
|
6
|
+
|
7
|
+
let(:parser) { described_class.new(data) }
|
8
|
+
|
9
|
+
context "when bid" do
|
10
|
+
let(:data) {
|
11
|
+
{
|
12
|
+
"id" => 2826860,
|
13
|
+
"type" => 0,
|
14
|
+
"price" => "20.10",
|
15
|
+
"amount" => "10.00000000",
|
16
|
+
"datetime" => "2013-04-20 12:12:14"
|
17
|
+
}
|
18
|
+
}
|
19
|
+
|
20
|
+
it { should be_a(OpenOrder) }
|
21
|
+
it { should be_bid }
|
22
|
+
its(:id) { should eq("2826860") }
|
23
|
+
|
24
|
+
context "amount" do
|
25
|
+
subject { open_order.amount }
|
26
|
+
it { should be_a(Amount) }
|
27
|
+
its(:value) { should eq(10) }
|
28
|
+
its(:currency) { should eq("BTC") }
|
29
|
+
end
|
30
|
+
|
31
|
+
context "price" do
|
32
|
+
subject { open_order.price }
|
33
|
+
it { should be_a(Price) }
|
34
|
+
its(:value) { should eq(20.1) }
|
35
|
+
its(:currency) { should eq("USD") }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context "when ask" do
|
40
|
+
let(:data) {
|
41
|
+
{
|
42
|
+
"id" => 2851404,
|
43
|
+
"type" => 1,
|
44
|
+
"price" => "200.00",
|
45
|
+
"amount" => "6.00000000",
|
46
|
+
"datetime" => "2013-04-22 17:47:43"
|
47
|
+
}
|
48
|
+
}
|
49
|
+
|
50
|
+
it { should be_a(OpenOrder) }
|
51
|
+
it { should be_ask }
|
52
|
+
its(:id) { should eq("2851404") }
|
53
|
+
|
54
|
+
context "amount" do
|
55
|
+
subject { open_order.amount }
|
56
|
+
it { should be_a(Amount) }
|
57
|
+
its(:value) { should eq(6) }
|
58
|
+
its(:currency) { should eq("BTC") }
|
59
|
+
end
|
60
|
+
|
61
|
+
context "price" do
|
62
|
+
subject { open_order.price }
|
63
|
+
it { should be_a(Price) }
|
64
|
+
its(:value) { should eq(200) }
|
65
|
+
its(:currency) { should eq("USD") }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Providers::Bitstamp::OpenOrdersRequest do
|
4
|
+
describe "#call" do
|
5
|
+
subject { request.call }
|
6
|
+
|
7
|
+
let(:request) { described_class.new(client) }
|
8
|
+
let(:client) { HttpRequestMock.new }
|
9
|
+
|
10
|
+
let(:raw_open_orders) { [raw_open_order] }
|
11
|
+
let(:raw_open_order) { double("raw open order") }
|
12
|
+
|
13
|
+
before do
|
14
|
+
client.stub(:post).with("open_orders").and_return(raw_open_orders)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "parses open orders" do
|
18
|
+
Providers::Bitstamp::OpenOrderParser.should_receive(:new).
|
19
|
+
with(raw_open_order).
|
20
|
+
and_return(double(parse: "result"))
|
21
|
+
expect(subject).to eq(["result"])
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Providers::Bitstamp do
|
4
|
+
describe "#initialize" do
|
5
|
+
let(:options) { double("options") }
|
6
|
+
let(:username) { "23443" }
|
7
|
+
let(:password) { "TopSecret" }
|
8
|
+
|
9
|
+
context "when with custom http client" do
|
10
|
+
subject { described_class.new(options, http_client) }
|
11
|
+
|
12
|
+
let(:http_client) { double("HttpClient") }
|
13
|
+
|
14
|
+
it { should be_a(described_class) }
|
15
|
+
its(:client) { should be(http_client) }
|
16
|
+
end
|
17
|
+
|
18
|
+
context "when with default http client class" do
|
19
|
+
subject { described_class.new(options) }
|
20
|
+
|
21
|
+
before do
|
22
|
+
described_class::HttpClient.should_receive(:build).with(options)
|
23
|
+
end
|
24
|
+
|
25
|
+
it { should be_a(described_class) }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#open_orders" do
|
30
|
+
subject { provider.open_orders }
|
31
|
+
|
32
|
+
let(:provider) { described_class.new(options, client) }
|
33
|
+
let(:options) { { username: "double", password: "double" } }
|
34
|
+
let(:client) { double.as_null_object }
|
35
|
+
|
36
|
+
let(:request) { double("request", call: open_orders) }
|
37
|
+
let(:open_orders) { double("open orders") }
|
38
|
+
|
39
|
+
before do
|
40
|
+
described_class::OpenOrdersRequest.stub(:new).with(client) { request }
|
41
|
+
end
|
42
|
+
|
43
|
+
it { should be(open_orders) }
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Providers::MtGox::AccountInfoParser do
|
4
|
+
describe ".parse_wallets" do
|
5
|
+
subject { described_class.parse_wallets(raw_wallets) }
|
6
|
+
|
7
|
+
let(:raw_wallets) {
|
8
|
+
{"EUR" => {"Balance" => balance}}
|
9
|
+
}
|
10
|
+
let(:balance) { double }
|
11
|
+
|
12
|
+
it "delegates each wallet balance to ValueWithCurrency" do
|
13
|
+
Providers::MtGox::ValueWithCurrencyCoercer.
|
14
|
+
should_receive(:call).
|
15
|
+
with(balance)
|
16
|
+
subject
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "#parse" do
|
21
|
+
subject(:account) { described_class.new(data).parse }
|
22
|
+
|
23
|
+
let(:data) {
|
24
|
+
{
|
25
|
+
"Wallets" => {
|
26
|
+
"BTC" => {
|
27
|
+
"Balance" => {"value_int" => "99470000", "currency" => "BTC"}
|
28
|
+
},
|
29
|
+
"EUR" => {
|
30
|
+
"Balance" => {"value_int" => "12314", "currency" => "EUR"},
|
31
|
+
},
|
32
|
+
"USD" => {
|
33
|
+
"Balance" => {"value_int" => "353302728", "currency" => "USD"},
|
34
|
+
},
|
35
|
+
},
|
36
|
+
"Trade_Fee" => 0.53
|
37
|
+
}
|
38
|
+
}
|
39
|
+
|
40
|
+
it { should be_a(Account) }
|
41
|
+
its(:fee) { should eq(0.53) }
|
42
|
+
|
43
|
+
context "btc wallet" do
|
44
|
+
subject { account.wallet("BTC") }
|
45
|
+
its(:balance) { should eq(0.9947) }
|
46
|
+
end
|
47
|
+
|
48
|
+
context "eur wallet" do
|
49
|
+
subject { account.wallet("EUR") }
|
50
|
+
its(:balance) { should eq(0.12314) }
|
51
|
+
end
|
52
|
+
|
53
|
+
context "usd wallet" do
|
54
|
+
subject { account.wallet("USD") }
|
55
|
+
its(:balance) { should eq(3533.02728) }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Providers::MtGox::AccountInfoRequest do
|
4
|
+
describe "#call" do
|
5
|
+
subject { request.call }
|
6
|
+
|
7
|
+
let(:request) { described_class.new(client) }
|
8
|
+
let(:client) { HttpRequestMock.new }
|
9
|
+
|
10
|
+
let(:result) { {"data" => raw_account} }
|
11
|
+
let(:raw_account) { double }
|
12
|
+
|
13
|
+
before do
|
14
|
+
client.stub(:post).with("money/info").and_return(result)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "parses open orders" do
|
18
|
+
Providers::MtGox::AccountInfoParser.should_receive(:new).
|
19
|
+
with(raw_account).
|
20
|
+
and_return(double(parse: "result"))
|
21
|
+
expect(subject).to eq("result")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Providers::MtGox::HttpClient::HmacMiddleware do
|
4
|
+
describe ".extract_params" do
|
5
|
+
subject { described_class.extract_params(env) }
|
6
|
+
|
7
|
+
let(:env) { {request_headers: headers, url: url, body: body} }
|
8
|
+
|
9
|
+
let(:headers) { {} }
|
10
|
+
let(:url) { double(to_s: "https://data.mtgox.com/api/2/money/orders") }
|
11
|
+
let(:body) { {} }
|
12
|
+
|
13
|
+
its([0]) { should eql(headers) }
|
14
|
+
its([1]) { should eq("money/orders") }
|
15
|
+
its([2]) { should eql(body) }
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "#call" do
|
19
|
+
subject { described_class.new(app, options).call(env) }
|
20
|
+
|
21
|
+
let(:app) { double }
|
22
|
+
let(:options) { {key: "KEY", secret: "c2VjcmV0" } }
|
23
|
+
let(:env) { double }
|
24
|
+
|
25
|
+
let(:headers) { {} }
|
26
|
+
let(:path) { "money/orders" }
|
27
|
+
let(:body) { "nonce=123" }
|
28
|
+
|
29
|
+
before do
|
30
|
+
described_class.stub(:extract_params).with(env).
|
31
|
+
and_return([headers, path, body])
|
32
|
+
app.stub(:call)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "forwards environment to app" do
|
36
|
+
app.should_receive(:call).with(env)
|
37
|
+
subject
|
38
|
+
end
|
39
|
+
|
40
|
+
it "adds Rest-Key to headers" do
|
41
|
+
subject
|
42
|
+
expect(headers["Rest-Key"]).to eq("KEY")
|
43
|
+
end
|
44
|
+
|
45
|
+
it "adds Rest-Sign to headers" do
|
46
|
+
subject
|
47
|
+
expect(headers["Rest-Sign"]).to eq("Jd1OnkDAy0mkSCq1+w3Aiyeotw+lGF0GOR3tp5Ch1DMkyRW9QJ4FrmyFZIoee5itfAOnq9sRslqPd3n3UKw64A==")
|
48
|
+
end
|
49
|
+
|
50
|
+
it "adds User-Agent to headers" do
|
51
|
+
subject
|
52
|
+
expect(headers["User-Agent"]).to eq("bitbot-trader")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|