bitex_bot 0.6.1 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -3
- data/Gemfile +3 -1
- data/bitex_bot.gemspec +5 -2
- data/lib/bitex_bot/database.rb +2 -2
- data/lib/bitex_bot/models/api_wrappers/api_wrapper.rb +47 -35
- data/lib/bitex_bot/models/api_wrappers/bitex/bitex_api_wrapper.rb +178 -0
- data/lib/bitex_bot/models/api_wrappers/bitstamp/bitstamp_api_wrapper.rb +62 -45
- data/lib/bitex_bot/models/api_wrappers/itbit/itbit_api_wrapper.rb +52 -28
- data/lib/bitex_bot/models/api_wrappers/kraken/kraken_api_wrapper.rb +61 -28
- data/lib/bitex_bot/models/api_wrappers/kraken/kraken_order.rb +12 -6
- data/lib/bitex_bot/models/buy_closing_flow.rb +3 -2
- data/lib/bitex_bot/models/buy_opening_flow.rb +31 -6
- data/lib/bitex_bot/models/closing_flow.rb +37 -22
- data/lib/bitex_bot/models/open_buy.rb +1 -3
- data/lib/bitex_bot/models/open_sell.rb +1 -3
- data/lib/bitex_bot/models/opening_flow.rb +42 -28
- data/lib/bitex_bot/models/order_book_simulator.rb +14 -13
- data/lib/bitex_bot/models/sell_closing_flow.rb +3 -2
- data/lib/bitex_bot/models/sell_opening_flow.rb +29 -4
- data/lib/bitex_bot/robot.rb +28 -43
- data/lib/bitex_bot/settings.rb +2 -0
- data/lib/bitex_bot/version.rb +1 -1
- data/settings.rb.sample +23 -5
- data/spec/bitex_bot/settings_spec.rb +13 -6
- data/spec/factories/bitex_ask.rb +14 -0
- data/spec/factories/bitex_bid.rb +14 -0
- data/spec/factories/bitex_buy.rb +7 -7
- data/spec/factories/bitex_sell.rb +7 -7
- data/spec/factories/buy_opening_flow.rb +10 -10
- data/spec/factories/open_buy.rb +8 -8
- data/spec/factories/open_sell.rb +8 -8
- data/spec/factories/sell_opening_flow.rb +10 -10
- data/spec/fixtures/bitstamp/balance.yml +63 -0
- data/spec/fixtures/bitstamp/order_book.yml +60 -0
- data/spec/fixtures/bitstamp/orders/all.yml +62 -0
- data/spec/fixtures/bitstamp/orders/failure_sell.yml +60 -0
- data/spec/fixtures/bitstamp/orders/successful_buy.yml +62 -0
- data/spec/fixtures/bitstamp/transactions.yml +244 -0
- data/spec/fixtures/bitstamp/user_transactions.yml +223 -0
- data/spec/models/api_wrappers/bitex_api_wrapper_spec.rb +147 -0
- data/spec/models/api_wrappers/bitstamp_api_wrapper_spec.rb +134 -140
- data/spec/models/api_wrappers/itbit_api_wrapper_spec.rb +9 -3
- data/spec/models/api_wrappers/kraken_api_wrapper_spec.rb +142 -73
- data/spec/models/bitex_api_spec.rb +4 -4
- data/spec/models/buy_closing_flow_spec.rb +19 -24
- data/spec/models/buy_opening_flow_spec.rb +102 -83
- data/spec/models/order_book_simulator_spec.rb +5 -0
- data/spec/models/robot_spec.rb +7 -4
- data/spec/models/sell_closing_flow_spec.rb +21 -25
- data/spec/models/sell_opening_flow_spec.rb +100 -80
- data/spec/spec_helper.rb +3 -1
- data/spec/support/bitex_stubs.rb +80 -40
- data/spec/support/bitstamp/bitstamp_api_wrapper_stubs.rb +2 -2
- data/spec/support/bitstamp/bitstamp_stubs.rb +3 -3
- data/spec/support/vcr.rb +8 -0
- data/spec/support/webmock.rb +8 -0
- metadata +77 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8a52c57b2cb3f955ac849623421abc8296b1dc9c
|
4
|
+
data.tar.gz: b1a88154cb71ef5868c510f9316f0e755b161c33
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1cb5d465e54e321a5d181aa86e988c0183938a70540ffcb5434000abf55627d9e8f875da413d67dd1cfa07892f0198c6be7d7c628d2e46a1a31caed5c6afa17d
|
7
|
+
data.tar.gz: 8cef55c3f44533c9b43c7f82bede8a7fb2777e248ad533845a50ac6cc9006cf59bdb95499d94a5f8a6b509fa0a7bff46347d6020bb765f5cd503358a34512dea
|
data/.rubocop.yml
CHANGED
@@ -2,16 +2,16 @@ Style/FrozenStringLiteralComment:
|
|
2
2
|
EnforcedStyle: never
|
3
3
|
|
4
4
|
Metrics/AbcSize:
|
5
|
-
Max:
|
5
|
+
Max: 18
|
6
6
|
|
7
7
|
Metrics/ClassLength:
|
8
|
-
|
8
|
+
Enabled: false
|
9
9
|
|
10
10
|
Metrics/LineLength:
|
11
11
|
Max: 130
|
12
12
|
|
13
13
|
Metrics/MethodLength:
|
14
|
-
Max:
|
14
|
+
Max: 30
|
15
15
|
|
16
16
|
Metrics/ParameterLists:
|
17
17
|
Max: 7
|
data/Gemfile
CHANGED
@@ -2,4 +2,6 @@ source 'https://rubygems.org'
|
|
2
2
|
|
3
3
|
# Specify your gem's dependencies in bitex_bot.gemspec
|
4
4
|
gemspec
|
5
|
-
|
5
|
+
|
6
|
+
gem 'bitex', github: 'bitex-la/bitex-ruby', branch: 'bitcoin-cash-market'
|
7
|
+
gem 'bitstamp', github: 'bitex-la/bitstamp', branch: 'update-lib'
|
data/bitex_bot.gemspec
CHANGED
@@ -18,14 +18,14 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
19
|
spec.require_paths = %w[lib]
|
20
20
|
|
21
|
-
spec.add_dependency 'activerecord'
|
21
|
+
spec.add_dependency 'activerecord'
|
22
22
|
spec.add_dependency 'hashie', '~> 3.5.4'
|
23
23
|
spec.add_dependency 'mail'
|
24
24
|
spec.add_dependency 'sqlite3'
|
25
25
|
|
26
26
|
spec.add_dependency 'bitex', '0.6'
|
27
27
|
spec.add_dependency 'bitstamp'
|
28
|
-
spec.add_dependency 'itbit'
|
28
|
+
spec.add_dependency 'itbit'
|
29
29
|
spec.add_dependency 'kraken_client', '~> 1.2.1'
|
30
30
|
|
31
31
|
spec.add_development_dependency 'bundler'
|
@@ -34,11 +34,14 @@ Gem::Specification.new do |spec|
|
|
34
34
|
spec.add_development_dependency 'byebug'
|
35
35
|
spec.add_development_dependency 'database_cleaner'
|
36
36
|
spec.add_development_dependency 'factory_bot'
|
37
|
+
spec.add_development_dependency 'faker'
|
37
38
|
spec.add_development_dependency 'rspec'
|
39
|
+
spec.add_development_dependency 'rspec-its'
|
38
40
|
spec.add_development_dependency 'rspec-mocks'
|
39
41
|
spec.add_development_dependency 'rspec_junit_formatter'
|
40
42
|
spec.add_development_dependency 'rubocop'
|
41
43
|
spec.add_development_dependency 'shoulda-matchers'
|
42
44
|
spec.add_development_dependency 'timecop'
|
45
|
+
spec.add_development_dependency 'vcr'
|
43
46
|
spec.add_development_dependency 'webmock'
|
44
47
|
end
|
data/lib/bitex_bot/database.rb
CHANGED
@@ -53,7 +53,7 @@ module BitexBot
|
|
53
53
|
t.decimal :quantity, precision: 30, scale: 15
|
54
54
|
t.decimal :amount, precision: 30, scale: 15
|
55
55
|
t.boolean :done, null: false, default: false
|
56
|
-
t.decimal :crypto_profit,
|
56
|
+
t.decimal :crypto_profit, precision: 30, scale: 15
|
57
57
|
t.decimal :fiat_profit, precision: 30, scale: 15
|
58
58
|
t.decimal :fx_rate, precision: 20, scale: 8
|
59
59
|
t.timestamps null: true
|
@@ -64,7 +64,7 @@ module BitexBot
|
|
64
64
|
t.decimal :quantity, precision: 30, scale: 15
|
65
65
|
t.decimal :amount, precision: 30, scale: 15
|
66
66
|
t.boolean :done, null: false, default: false
|
67
|
-
t.decimal :crypto_profit,
|
67
|
+
t.decimal :crypto_profit, precision: 30, scale: 15
|
68
68
|
t.decimal :fiat_profit, precision: 30, scale: 15
|
69
69
|
t.decimal :fx_rate, precision: 20, scale: 8
|
70
70
|
t.timestamps null: true
|
@@ -1,12 +1,15 @@
|
|
1
1
|
# This class represents the general behaviour for trading platform wrappers.
|
2
2
|
class ApiWrapper
|
3
|
+
attr_accessor :currency_pair
|
4
|
+
|
3
5
|
MIN_AMOUNT = 5
|
4
6
|
|
5
7
|
Transaction = Struct.new(
|
6
|
-
:id,
|
7
|
-
:price,
|
8
|
-
:amount,
|
9
|
-
:timestamp # Epoch Integer
|
8
|
+
:id, # Integer
|
9
|
+
:price, # Decimal
|
10
|
+
:amount, # Decimal
|
11
|
+
:timestamp, # Epoch Integer
|
12
|
+
:raw # Actual transaction
|
10
13
|
)
|
11
14
|
|
12
15
|
Order = Struct.new(
|
@@ -15,14 +18,14 @@ class ApiWrapper
|
|
15
18
|
:price, # Decimal
|
16
19
|
:amount, # Decimal
|
17
20
|
:timestamp, # Integer
|
18
|
-
:
|
21
|
+
:raw # Actual order object
|
19
22
|
) do
|
20
23
|
def method_missing(method_name, *args, &block)
|
21
|
-
|
24
|
+
raw.respond_to?(method_name) ? raw.send(method_name, *args, &block) : super
|
22
25
|
end
|
23
26
|
|
24
27
|
def respond_to_missing?(method_name, include_private = false)
|
25
|
-
|
28
|
+
raw.respond_to?(method_name) || super
|
26
29
|
end
|
27
30
|
end
|
28
31
|
|
@@ -50,58 +53,53 @@ class ApiWrapper
|
|
50
53
|
)
|
51
54
|
|
52
55
|
UserTransaction = Struct.new(
|
53
|
-
:order_id,
|
54
|
-
:
|
55
|
-
:
|
56
|
-
:
|
57
|
-
:fee,
|
58
|
-
:type,
|
59
|
-
:timestamp
|
56
|
+
:order_id, # Integer
|
57
|
+
:fiat, # Decimal
|
58
|
+
:crypto, # Decimal
|
59
|
+
:crypto_fiat, # Decimal
|
60
|
+
:fee, # Decimal
|
61
|
+
:type, # Integer
|
62
|
+
:timestamp # Epoch Integer
|
60
63
|
)
|
61
64
|
|
62
|
-
# @return [Void]
|
63
|
-
def self.setup(_settings)
|
64
|
-
raise 'self subclass responsibility'
|
65
|
-
end
|
66
|
-
|
67
65
|
# @return [Array<Transaction>]
|
68
|
-
def
|
66
|
+
def transactions
|
69
67
|
raise 'self subclass responsibility'
|
70
68
|
end
|
71
69
|
|
72
70
|
# @return [OrderBook]
|
73
|
-
def
|
71
|
+
def order_book(_retries = 20)
|
74
72
|
raise 'self subclass responsibility'
|
75
73
|
end
|
76
74
|
|
77
75
|
# @return [BalanceSummary]
|
78
|
-
def
|
76
|
+
def balance
|
79
77
|
raise 'self subclass responsibility'
|
80
78
|
end
|
81
79
|
|
82
80
|
# @return [nil]
|
83
|
-
def
|
81
|
+
def cancel
|
84
82
|
raise 'self subclass responsibility'
|
85
83
|
end
|
86
84
|
|
87
85
|
# @return [Array<Order>]
|
88
|
-
def
|
86
|
+
def orders
|
89
87
|
raise 'self subclass responsibility'
|
90
88
|
end
|
91
89
|
|
92
90
|
# @return [UserTransaction]
|
93
|
-
def
|
91
|
+
def user_transactions
|
94
92
|
raise 'self subclass responsibility'
|
95
93
|
end
|
96
94
|
|
97
95
|
# @param type
|
98
96
|
# @param price
|
99
97
|
# @param quantity
|
100
|
-
def
|
98
|
+
def place_order(type, price, quantity)
|
101
99
|
order = send_order(type, price, quantity)
|
102
100
|
return order unless order.nil? || order.id.nil?
|
103
101
|
|
104
|
-
BitexBot::Robot.log(:debug, "Captured error when placing order on #{self.class
|
102
|
+
BitexBot::Robot.log(:debug, "Captured error when placing order on #{self.class}")
|
105
103
|
# Order may have gone through and be stuck somewhere in Wrapper's pipeline.
|
106
104
|
# We just sleep for a bit and then look for the order.
|
107
105
|
20.times do
|
@@ -109,32 +107,46 @@ class ApiWrapper
|
|
109
107
|
order = find_lost(type, price, quantity)
|
110
108
|
return order if order.present?
|
111
109
|
end
|
112
|
-
raise OrderNotFound, "Closing: #{type} order not found for #{quantity}
|
110
|
+
raise OrderNotFound, "Closing: #{type} order not found for #{quantity} #{base} @ #{quote} #{price}. #{order}"
|
113
111
|
end
|
114
112
|
|
115
113
|
# Hook Method - arguments could not be used in their entirety by the subclasses
|
116
|
-
def
|
114
|
+
def send_order(_type, _price, _quantity)
|
117
115
|
raise 'self subclass responsibility'
|
118
116
|
end
|
119
117
|
|
120
|
-
# @param
|
118
|
+
# @param order_type [String] buy|sell
|
121
119
|
# @param price [Decimal]
|
120
|
+
# @param quantity [Decimal]
|
122
121
|
#
|
123
122
|
# Hook Method - arguments could not be used in their entirety by the subclasses
|
124
|
-
def
|
123
|
+
def find_lost(_type, _price, _quantity)
|
125
124
|
raise 'self subclass responsibility'
|
126
125
|
end
|
127
126
|
|
127
|
+
# From an order when you buy or sell, when you place an order and it matches, you can match more than one order.
|
128
128
|
# @param order_id
|
129
|
-
# @param transactions
|
129
|
+
# @param transactions: all matches for a purchase or sale order.
|
130
130
|
#
|
131
131
|
# @return [Array<Decimal, Decimal>]
|
132
|
-
def
|
132
|
+
def amount_and_quantity(_order_id)
|
133
133
|
raise 'self subclass responsibility'
|
134
134
|
end
|
135
135
|
|
136
|
-
def
|
137
|
-
|
136
|
+
def enough_order_size?(quantity, price)
|
137
|
+
quantity * price > MIN_AMOUNT
|
138
|
+
end
|
139
|
+
|
140
|
+
def base_quote
|
141
|
+
"#{base}_#{quote}"
|
142
|
+
end
|
143
|
+
|
144
|
+
def base
|
145
|
+
currency_pair[:base]
|
146
|
+
end
|
147
|
+
|
148
|
+
def quote
|
149
|
+
currency_pair[:quote]
|
138
150
|
end
|
139
151
|
end
|
140
152
|
|
@@ -0,0 +1,178 @@
|
|
1
|
+
# Wrapper implementation for Bitex API.
|
2
|
+
# https://bitex.la/developers
|
3
|
+
class BitexApiWrapper < ApiWrapper
|
4
|
+
Order = Struct.new(
|
5
|
+
:id, # String
|
6
|
+
:type, # Symbol
|
7
|
+
:price, # Decimal
|
8
|
+
:amount, # Decimal
|
9
|
+
:timestamp, # Integer
|
10
|
+
:raw # Actual order object
|
11
|
+
) do
|
12
|
+
def method_missing(method_name, *args, &block)
|
13
|
+
raw.respond_to?(method_name) ? raw.send(method_name, *args, &block) : super
|
14
|
+
end
|
15
|
+
|
16
|
+
def respond_to_missing?(method_name, include_private = false)
|
17
|
+
raw.respond_to?(method_name) || super
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_accessor :api_key, :ssl_version, :debug, :sandbox
|
22
|
+
|
23
|
+
def initialize(settings)
|
24
|
+
self.api_key = settings.api_key
|
25
|
+
self.ssl_version = settings.ssl_version
|
26
|
+
self.debug = settings.debug
|
27
|
+
self.sandbox = settings.sandbox
|
28
|
+
currency_pair(settings.order_book)
|
29
|
+
setup
|
30
|
+
end
|
31
|
+
|
32
|
+
def setup
|
33
|
+
Bitex.api_key = api_key
|
34
|
+
Bitex.ssl_version = ssl_version
|
35
|
+
Bitex.debug = debug
|
36
|
+
Bitex.sandbox = sandbox
|
37
|
+
end
|
38
|
+
|
39
|
+
def profile
|
40
|
+
Bitex::Profile.get
|
41
|
+
end
|
42
|
+
|
43
|
+
# rubocop:disable Metrics/AbcSize
|
44
|
+
def amount_and_quantity(order_id)
|
45
|
+
closes = user_transactions.select { |t| t.order_id.to_s == order_id }
|
46
|
+
amount = closes.map { |c| c.send(currency[:quote]).to_d }.sum.abs
|
47
|
+
quantity = closes.map { |c| c.send(currency[:base]).to_d }.sum.abs
|
48
|
+
|
49
|
+
[amount, quantity]
|
50
|
+
end
|
51
|
+
# rubocop:enable Metrics/AbcSize
|
52
|
+
|
53
|
+
def balance
|
54
|
+
balance_summary_parser(profile)
|
55
|
+
end
|
56
|
+
|
57
|
+
def find_lost(type, price, _quantity)
|
58
|
+
orders.find { |o| o.type == type && o.price == price && o.timestamp >= 5.minutes.ago.to_i }
|
59
|
+
end
|
60
|
+
|
61
|
+
def order_book
|
62
|
+
order_book_parser(market.order_book)
|
63
|
+
end
|
64
|
+
|
65
|
+
def orders
|
66
|
+
Bitex::Order.all.map { |o| order_parser(o) }
|
67
|
+
end
|
68
|
+
|
69
|
+
def send_order(type, price, quantity, wait = false)
|
70
|
+
{ sell: Bitex::Ask, buy: Bitex::Bid }[type].create!(base_quote.to_sym, quantity, price, wait)
|
71
|
+
end
|
72
|
+
|
73
|
+
def transactions
|
74
|
+
Bitex::Trade.all.map { |t| transaction_parser(t) }
|
75
|
+
end
|
76
|
+
|
77
|
+
def user_transactions
|
78
|
+
Bitex::Trade.all.map { |trade| user_transaction_parser(trade) }
|
79
|
+
end
|
80
|
+
|
81
|
+
# {
|
82
|
+
# usd_balance: 10000.00, # Total USD balance.
|
83
|
+
# usd_reserved: 2000.00, # USD reserved in open orders.
|
84
|
+
# usd_available: 8000.00, # USD available for trading.
|
85
|
+
# btc_balance: 20.00000000, # Total BTC balance.
|
86
|
+
# btc_reserved: 5.00000000, # BTC reserved in open orders.
|
87
|
+
# btc_available: 15.00000000, # BTC available for trading.
|
88
|
+
# fee: 0.5, # Your trading fee (0.5 means 0.5%).
|
89
|
+
# btc_deposit_address: "1XXXXXXXX..." # Your BTC deposit address.
|
90
|
+
# }
|
91
|
+
def balance_summary_parser(balances)
|
92
|
+
BalanceSummary.new(
|
93
|
+
balance_parser(balances, currency_pair[:base]),
|
94
|
+
balance_parser(balances, currency_pair[:quote]),
|
95
|
+
balances[:fee].to_d
|
96
|
+
)
|
97
|
+
end
|
98
|
+
|
99
|
+
def balance_parser(balances, currency)
|
100
|
+
Balance.new(
|
101
|
+
balances["#{currency}_balance".to_sym].to_d,
|
102
|
+
balances["#{currency}_reserved".to_sym].to_d,
|
103
|
+
balances["#{currency}_available".to_sym].to_d
|
104
|
+
)
|
105
|
+
end
|
106
|
+
|
107
|
+
def last_order_by(price)
|
108
|
+
orders.select { |o| o.price == price && (o.timestamp - Time.now.to_i).abs < 500 }.first
|
109
|
+
end
|
110
|
+
|
111
|
+
# {
|
112
|
+
# bids: [[0.63921e3, 0.195e1], [0.637e3, 0.47e0], [0.63e3, 0.158e1]],
|
113
|
+
# asks: [[0.6424e3, 0.4e0], [0.6433e3, 0.95e0], [0.6443e3, 0.25e0]]
|
114
|
+
# }
|
115
|
+
def order_book_parser(book)
|
116
|
+
OrderBook.new(Time.now.to_i, order_summary_parser(book[:bids]), order_summary_parser(book[:asks]))
|
117
|
+
end
|
118
|
+
|
119
|
+
def order_summary_parser(orders)
|
120
|
+
orders.map { |order| OrderSummary.new(order[0].to_d, order[1].to_d) }
|
121
|
+
end
|
122
|
+
|
123
|
+
# <Bitex::Bid
|
124
|
+
# @id=12345678, @created_at=1999-12-31 21:10:00 -0300, @order_book=:btc_usd, @price=0.1e4, @status=:executing, @reason=nil,
|
125
|
+
# @issuer=nil, @amount=0.1e3, @remaining_amount=0.1e2, @produced_quantity=0.0
|
126
|
+
# >
|
127
|
+
def order_parser(order)
|
128
|
+
Order.new(order.id.to_s, order_type(order), order.price, order_amount(order), order.created_at.to_i, order)
|
129
|
+
end
|
130
|
+
|
131
|
+
def order_type(order)
|
132
|
+
order.is_a?(Bitex::Bid) ? :buy : :sell
|
133
|
+
end
|
134
|
+
|
135
|
+
def order_amount(order)
|
136
|
+
order.is_a?(Bitex::Bid) ? order.amount : order.quantity
|
137
|
+
end
|
138
|
+
|
139
|
+
# [
|
140
|
+
# [1492795215, 80310, 1243.51657154, 4.60321971],
|
141
|
+
# [UNIX timestamp, Transaction ID, Price Paid, Amound Sold]
|
142
|
+
# ]
|
143
|
+
def transaction_parser(transaction)
|
144
|
+
Transaction.new(transaction.id, transaction.price.to_d, transaction.amount.to_d, transaction.created_at.to_i, transaction)
|
145
|
+
end
|
146
|
+
|
147
|
+
# <Bitex::Buy:0x007ff9a2979390
|
148
|
+
# @id=12345678, @created_at=1999-12-31 21:10:00 -0300, @order_book=:btc_usd, @quantity=0.2e1, @amount=0.6e3, @fee=0.5e-1,
|
149
|
+
# @price=0.3e3, @bid_id=123
|
150
|
+
# >
|
151
|
+
#
|
152
|
+
# <Bitex::Sell:0x007ff9a2978710
|
153
|
+
# @id=12345678, @created_at=1999-12-31 21:10:00 -0300, @order_book=:btc_usd, @quantity=0.2e1, @amount=0.6e3, @fee=0.5e-1,
|
154
|
+
# @price=0.3e3, @ask_id=456i
|
155
|
+
# >
|
156
|
+
def user_transaction_parser(trade)
|
157
|
+
UserTransaction.new(
|
158
|
+
trade.id, trade.amount, trade.quantity, trade.price, trade.fee, trade_type(trade), trade.created_at.to_i
|
159
|
+
)
|
160
|
+
end
|
161
|
+
|
162
|
+
def trade_type(trade)
|
163
|
+
# ask: 0, bid: 1
|
164
|
+
trade.is_a?(Bitex::Buy) ? 1 : 0
|
165
|
+
end
|
166
|
+
|
167
|
+
def market
|
168
|
+
@market ||= { btc: Bitex::BitcoinMarketData, bch: Bitex::BitcoinCashMarketData }[currency_pair[:base].to_sym]
|
169
|
+
end
|
170
|
+
|
171
|
+
def currency_pair(order_book = '_')
|
172
|
+
@currency_pair ||= {
|
173
|
+
name: order_book,
|
174
|
+
base: order_book.split('_').first,
|
175
|
+
quote: order_book.split('_').last
|
176
|
+
}
|
177
|
+
end
|
178
|
+
end
|
@@ -1,41 +1,45 @@
|
|
1
1
|
# Wrapper implementation for Bitstamp API.
|
2
2
|
# https://www.bitstamp.net/api/
|
3
3
|
class BitstampApiWrapper < ApiWrapper
|
4
|
-
|
4
|
+
attr_accessor :key, :secret, :client_id
|
5
|
+
|
6
|
+
def initialize(settings)
|
7
|
+
self.key = settings.api_key
|
8
|
+
self.secret = settings.secret
|
9
|
+
self.client_id = settings.client_id
|
10
|
+
currency_pair(settings.order_book)
|
11
|
+
setup
|
12
|
+
end
|
13
|
+
|
14
|
+
def setup
|
5
15
|
Bitstamp.setup do |config|
|
6
|
-
config.key =
|
7
|
-
config.secret =
|
8
|
-
config.client_id =
|
16
|
+
config.key = key
|
17
|
+
config.secret = secret
|
18
|
+
config.client_id = client_id
|
9
19
|
end
|
10
20
|
end
|
11
21
|
|
12
|
-
def
|
13
|
-
closes =
|
14
|
-
amount = closes.
|
15
|
-
quantity = closes.
|
22
|
+
def amount_and_quantity(order_id)
|
23
|
+
closes = user_transactions.select { |t| t.order_id.to_s == order_id }
|
24
|
+
amount = closes.sum(&:fiat).abs
|
25
|
+
quantity = closes.sum(&:crypto).abs
|
16
26
|
|
17
27
|
[amount, quantity]
|
18
28
|
end
|
19
29
|
|
20
|
-
def
|
21
|
-
balance_summary_parser(Bitstamp.balance.symbolize_keys)
|
30
|
+
def balance
|
31
|
+
balance_summary_parser(Bitstamp.balance(currency_pair[:name]).symbolize_keys)
|
22
32
|
rescue StandardError => e
|
23
33
|
raise ApiWrapperError, "Bitstamp balance failed: #{e.message}"
|
24
34
|
end
|
25
35
|
|
26
|
-
def
|
27
|
-
Bitstamp::Order.new(id: order.id).cancel!
|
28
|
-
rescue StandardError => e
|
29
|
-
raise ApiWrapperError, "Bitstamp cancel! failed: #{e.message}"
|
30
|
-
end
|
31
|
-
|
32
|
-
def self.find_lost(type, price, _quantity)
|
36
|
+
def find_lost(type, price, _quantity)
|
33
37
|
orders.find { |o| o.type == type && o.price == price && o.timestamp >= 5.minutes.ago.to_i }
|
34
38
|
end
|
35
39
|
|
36
40
|
# rubocop:disable Metrics/AbcSize
|
37
|
-
def
|
38
|
-
book = Bitstamp.order_book.deep_symbolize_keys
|
41
|
+
def order_book(retries = 20)
|
42
|
+
book = Bitstamp.order_book(currency_pair[:name]).deep_symbolize_keys
|
39
43
|
age = Time.now.to_i - book[:timestamp].to_i
|
40
44
|
return order_book_parser(book) if age <= 300
|
41
45
|
|
@@ -50,24 +54,25 @@ class BitstampApiWrapper < ApiWrapper
|
|
50
54
|
end
|
51
55
|
# rubocop:enable Metrics/AbcSize
|
52
56
|
|
53
|
-
def
|
54
|
-
Bitstamp.orders.all.map { |o| order_parser(o) }
|
57
|
+
def orders
|
58
|
+
Bitstamp.orders.all(currency_pair: currency_pair[:name]).map { |o| order_parser(o) }
|
55
59
|
rescue StandardError => e
|
56
60
|
raise ApiWrapperError, "Bitstamp orders failed: #{e.message}"
|
57
61
|
end
|
58
62
|
|
59
|
-
def
|
60
|
-
Bitstamp.orders.send(type, amount: quantity.round(4), price: price.round(2))
|
63
|
+
def send_order(type, price, quantity)
|
64
|
+
order = Bitstamp.orders.send(type, currency_pair: currency_pair[:name], amount: quantity.round(4), price: price.round(2))
|
65
|
+
order_parser(order) unless order.error.present?
|
61
66
|
end
|
62
67
|
|
63
|
-
def
|
64
|
-
Bitstamp.transactions.map { |t| transaction_parser(t) }
|
68
|
+
def transactions
|
69
|
+
Bitstamp.transactions(currency_pair[:name]).map { |t| transaction_parser(t) }
|
65
70
|
rescue StandardError => e
|
66
71
|
raise ApiWrapperError, "Bitstamp transactions failed: #{e.message}"
|
67
72
|
end
|
68
73
|
|
69
|
-
def
|
70
|
-
Bitstamp.user_transactions.all.map { |ut| user_transaction_parser(ut) }
|
74
|
+
def user_transactions
|
75
|
+
Bitstamp.user_transactions.all(currency_pair: currency_pair[:name]).map { |ut| user_transaction_parser(ut) }
|
71
76
|
rescue StandardError => e
|
72
77
|
raise ApiWrapperError, "Bitstamp user_transactions failed: #{e.message}"
|
73
78
|
end
|
@@ -77,11 +82,15 @@ class BitstampApiWrapper < ApiWrapper
|
|
77
82
|
# usd_reserved: '1.02, usd_available: '6952.05', usd_balance: '6953.07',
|
78
83
|
# fee: '0.4000'
|
79
84
|
# }
|
80
|
-
def
|
81
|
-
BalanceSummary.new(
|
85
|
+
def balance_summary_parser(balances)
|
86
|
+
BalanceSummary.new(
|
87
|
+
balance_parser(balances, currency_pair[:base]),
|
88
|
+
balance_parser(balances, currency_pair[:quote]),
|
89
|
+
balances[:fee].to_d
|
90
|
+
)
|
82
91
|
end
|
83
92
|
|
84
|
-
def
|
93
|
+
def balance_parser(balances, currency)
|
85
94
|
Balance.new(
|
86
95
|
balances["#{currency}_balance".to_sym].to_d,
|
87
96
|
balances["#{currency}_reserved".to_sym].to_d,
|
@@ -94,42 +103,50 @@ class BitstampApiWrapper < ApiWrapper
|
|
94
103
|
# bids: [['124.55', '1.58057006'], ['124.40', '14.91779125']],
|
95
104
|
# asks: [['124.56', '0.81888247'], ['124.57', '0.81078911']]
|
96
105
|
# }
|
97
|
-
def
|
106
|
+
def order_book_parser(book)
|
98
107
|
OrderBook.new(book[:timestamp].to_i, order_summary_parser(book[:bids]), order_summary_parser(book[:asks]))
|
99
108
|
end
|
100
109
|
|
101
|
-
def
|
110
|
+
def order_is_done?(order)
|
102
111
|
order.nil?
|
103
112
|
end
|
104
113
|
|
105
|
-
# <Bitstamp::Order @id=76, @type=0, @price='1.1', @amount='1.0', @datetime='2013-09-26 23:15:04'>
|
106
|
-
def
|
107
|
-
type = order.type
|
114
|
+
# <Bitstamp::Order @id='76', @type=0, @price='1.1', @amount='1.0', @datetime='2013-09-26 23:15:04'>
|
115
|
+
def order_parser(order)
|
116
|
+
type = order.type == '0' ? :buy : :sell
|
108
117
|
Order.new(order.id.to_s, type, order.price.to_d, order.amount.to_d, order.datetime.to_datetime.to_i, order)
|
109
118
|
end
|
110
119
|
|
111
|
-
def
|
120
|
+
def order_summary_parser(orders)
|
112
121
|
orders.map { |order| OrderSummary.new(order[0].to_d, order[1].to_d) }
|
113
122
|
end
|
114
123
|
|
115
|
-
# <Bitstamp::Transactions: @tid=1469074, @price='126.95', @amount='1.10000000', @date='1380648951'>
|
116
|
-
def
|
117
|
-
Transaction.new(transaction.tid, transaction.price.to_d, transaction.amount.to_d, transaction.date.to_i)
|
124
|
+
# <Bitstamp::Transactions: @tid='1469074', @price='126.95', @amount='1.10000000', @date='1380648951'>
|
125
|
+
def transaction_parser(transaction)
|
126
|
+
Transaction.new(transaction.tid, transaction.price.to_d, transaction.amount.to_d, transaction.date.to_i, transaction)
|
118
127
|
end
|
119
128
|
|
120
129
|
# <Bitstamp::UserTransaction:
|
121
130
|
# @usd='-373.51', @btc='3.00781124', @btc_usd='124.18', @order_id=7623942, @fee='1.50', @type=2, @id=1444404,
|
122
131
|
# @datetime='2013-09-26 13:28:55'
|
123
132
|
# >
|
124
|
-
def
|
133
|
+
def user_transaction_parser(user_transaction)
|
125
134
|
UserTransaction.new(
|
126
135
|
user_transaction.order_id,
|
127
|
-
user_transaction.
|
128
|
-
user_transaction.
|
129
|
-
user_transaction.
|
136
|
+
user_transaction.send(quote).to_d,
|
137
|
+
user_transaction.send(base).to_d,
|
138
|
+
user_transaction.send(base_quote).to_d,
|
130
139
|
user_transaction.fee.to_d,
|
131
|
-
user_transaction.type,
|
132
|
-
Time.
|
140
|
+
user_transaction.type.to_i,
|
141
|
+
Time.parse(user_transaction.datetime).to_i
|
133
142
|
)
|
134
143
|
end
|
144
|
+
|
145
|
+
def currency_pair(order_book = '')
|
146
|
+
@currency_pair ||= {
|
147
|
+
name: order_book,
|
148
|
+
base: order_book.slice(0..2),
|
149
|
+
quote: order_book.slice(3..5)
|
150
|
+
}
|
151
|
+
end
|
135
152
|
end
|