trade-o-matic 0.1.0 → 0.2.0

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/lib/trade-o-matic/adapters/base/raw_account_order.rb +7 -0
  3. data/lib/trade-o-matic/adapters/base/raw_balance.rb +7 -0
  4. data/lib/trade-o-matic/adapters/base/raw_resource.rb +31 -0
  5. data/lib/trade-o-matic/adapters/bitstamp_backend.rb +166 -0
  6. data/lib/trade-o-matic/adapters/fake_backend.rb +235 -0
  7. data/lib/trade-o-matic/adapters/itbit_backend.rb +57 -0
  8. data/lib/trade-o-matic/adapters/surbtc_backend.rb +149 -0
  9. data/lib/trade-o-matic/cli.rb +6 -22
  10. data/lib/trade-o-matic/converters/compound_converter.rb +13 -0
  11. data/lib/trade-o-matic/converters/fixed_converter.rb +0 -7
  12. data/lib/trade-o-matic/converters/inverse_converter.rb +13 -0
  13. data/lib/trade-o-matic/converters/json_api_converter.rb +20 -0
  14. data/lib/trade-o-matic/converters/sync_converter.rb +1 -11
  15. data/lib/trade-o-matic/core/account.rb +75 -0
  16. data/lib/trade-o-matic/core/account_order.rb +99 -0
  17. data/lib/trade-o-matic/core/account_proxy.rb +48 -0
  18. data/lib/trade-o-matic/core/balance.rb +57 -0
  19. data/lib/trade-o-matic/core/exchange.rb +19 -0
  20. data/lib/trade-o-matic/core/market.rb +29 -0
  21. data/lib/trade-o-matic/core/market_loader.rb +29 -0
  22. data/lib/trade-o-matic/services/backend_factory.rb +46 -0
  23. data/lib/trade-o-matic/structs/ask_slope.rb +4 -1
  24. data/lib/trade-o-matic/structs/bid_slope.rb +4 -1
  25. data/lib/trade-o-matic/structs/book.rb +52 -0
  26. data/lib/trade-o-matic/structs/converter.rb +2 -3
  27. data/lib/trade-o-matic/structs/currency.rb +126 -21
  28. data/lib/trade-o-matic/structs/currency_pair.rb +31 -5
  29. data/lib/trade-o-matic/structs/order.rb +53 -1
  30. data/lib/trade-o-matic/structs/price.rb +9 -8
  31. data/lib/trade-o-matic/structs/slope.rb +11 -34
  32. data/lib/trade-o-matic/support/converter_configurator.rb +35 -0
  33. data/lib/trade-o-matic/version.rb +1 -1
  34. data/lib/trade-o-matic.rb +29 -1
  35. metadata +59 -43
  36. data/lib/trade-o-matic/adapters/bitstamp_account.rb +0 -63
  37. data/lib/trade-o-matic/flows/ask_replicator_flow.rb +0 -50
  38. data/lib/trade-o-matic/generators/linear_generator.rb +0 -37
  39. data/lib/trade-o-matic/structs/market.rb +0 -28
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9300715275fb3f78ca15a6d534d493f61c75ea7e
4
- data.tar.gz: b3f66fd9b2f31736c86260f45c817ea8dc85dc01
3
+ metadata.gz: 9b2ab1e0af6b6c8685d51d92657660229ac270e4
4
+ data.tar.gz: f03f687cbae9e0d8436d5b879166ae186a57bd16
5
5
  SHA512:
6
- metadata.gz: 5208ae28f7c8d4da98d1a65bfdef0f1f9b2b1a09e4744ee79b87c32f104a6c9e71ceca7ea4045af8cba160c7b4ffca7f2b338b694cb514310957791e3fd24897
7
- data.tar.gz: 4efe51fef441d0a483e38a766743268f87bf4fb9a968496763c85befd879aa6e05902b5732e24e6e525199e83c4e2435019ca16130341f5ed25f9e18cd7e0b5a
6
+ metadata.gz: c524eb2a14bf4d3258e98356ed20493b2ce7943488833137c53fc338ba9a4181c082a4d8a63e0eb08d46cc0a4d0f8e3d400d6ce38c1909cba93f493d75d1db92
7
+ data.tar.gz: 3e5696eafbdcf0e0022166540eace85363ae4f88fb28704d14ea27cf1c1596c5868e8f97c5a2d2ee8756f54f993f2f95b72b0ced2bbdf1da9bb4f64da90adac6
@@ -0,0 +1,7 @@
1
+ require 'trade-o-matic/adapters/base/raw_resource'
2
+
3
+ module Trader
4
+ class RawAccountOrder < RawResource
5
+ enforce_attr :id, :status, :price, :volume, :executed_volume, :instruction, :limit?
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ require 'trade-o-matic/adapters/base/raw_resource'
2
+
3
+ module Trader
4
+ class RawBalance < RawResource
5
+ enforce_attr :amount, :available_amount
6
+ end
7
+ end
@@ -0,0 +1,31 @@
1
+ module Trader
2
+ class RawResource
3
+
4
+ def self.enforce_attr(*_attrs)
5
+ _attrs.each do |att|
6
+ define_method(att) do
7
+ raise NotImplementedError, "#{att} was not implemented by backend"
8
+ end
9
+ end
10
+ end
11
+
12
+ def self.attr_mapped(_attr, _path=nil, &_block)
13
+ define_method(_attr) do
14
+ if _block
15
+ _block.call(raw)
16
+ elsif _path
17
+ raw[_path]
18
+ else
19
+ raw[_attr.to_s]
20
+ end
21
+ end
22
+ end
23
+
24
+ attr_reader :raw
25
+
26
+ def initialize(_raw)
27
+ @raw = _raw
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,166 @@
1
+ require 'rest-client'
2
+ require 'json'
3
+ require 'trade-o-matic/adapters/base/raw_account_order'
4
+ require 'trade-o-matic/adapters/base/raw_balance'
5
+
6
+ module Trader
7
+ class BitstampBackend
8
+ BASE_CUR = Currency.for_code(:BTC)
9
+ QUOTE_CUR = Currency.for_code(:BITSTAMP_USD)
10
+ MAIN_MARKET = CurrencyPair.new BASE_CUR, QUOTE_CUR
11
+
12
+ TYPE_MAP = {
13
+ 0 => Order::BID,
14
+ 1 => Order::ASK
15
+ }
16
+
17
+ class BackendBalance < Struct.new(:amount, :available_amount)
18
+ # nothing for now
19
+ end
20
+
21
+ class BitstampOrder < RawAccountOrder
22
+ attr_mapped(:id, 'raw') # use original order information as id
23
+ attr_mapped(:pair) { |r| MAIN_MARKET }
24
+ attr_mapped(:price) { |r| r['raw']['price'].to_f }
25
+ attr_mapped(:volume) { |r| r['raw']['amount'].to_f }
26
+ attr_mapped(:executed_volume)
27
+ attr_mapped(:instruction) { |r| r['raw']['type'] == 0 ? Order::BID : Order::ASK }
28
+
29
+ attr_mapped(:status) do |r|
30
+ case r['status']
31
+ when 'Open'
32
+ AccountOrder::OPEN
33
+ when 'Finished'
34
+ r['executed_volume'] < r['raw']['amount'].to_f ? AccountOrder::CANCELED : AccountOrder::CLOSED
35
+ else
36
+ AccountOrder::PENDING
37
+ end
38
+ end
39
+
40
+ def limit?
41
+ true
42
+ end
43
+ end
44
+
45
+ def initialize(_session=nil)
46
+ @session = _session
47
+ end
48
+
49
+ def get_available_markets
50
+ [MAIN_MARKET]
51
+ end
52
+
53
+ def fill_book(_book)
54
+ # TODO: consider book pair
55
+
56
+ _book.prepare Time.now
57
+
58
+ ob = execute_request(nil, 'order_book')
59
+ ob['bids'].each { |o| _book.add_bid(o[0].to_f, o[1].to_f) }
60
+ ob['asks'].each { |o| _book.add_ask(o[0].to_f, o[1].to_f) }
61
+
62
+ tx = execute_request(nil, 'transactions')
63
+ tx.each do |t|
64
+ _book.add_transaction t['price'].to_f, t['amount'].to_f, Time.at(t['date'].to_i)
65
+ end
66
+ end
67
+
68
+ def get_session(_credentials)
69
+ _credentials
70
+ end
71
+
72
+ def get_balance(_session, _currency)
73
+ raise "#{_currency} not supported" unless _currency == BASE_CUR || _currency == QUOTE_CUR
74
+
75
+ raw = execute_request(_session || session, 'balance')
76
+
77
+ if _currency == BASE_CUR
78
+ return BackendBalance.new raw['btc_balance'].to_f, raw['btc_available'].to_f
79
+ else
80
+ return BackendBalance.new raw['usd_balance'].to_f, raw['usd_available'].to_f
81
+ end
82
+ end
83
+
84
+ def get_orders(_session, _pair)
85
+ raise 'market not supported' unless _pair == MAIN_MARKET
86
+
87
+ raw_orders = execute_request(_session || session, 'open_orders')
88
+ raw_orders.map { |o| normalize_raw_order o }
89
+ end
90
+
91
+ def create_order(_session, _pair, _volume, _price, _type)
92
+ raise 'market not supported' unless _pair == MAIN_MARKET
93
+ raise 'market orders not supported' if _price.nil?
94
+
95
+ normalize_raw_order execute_request(
96
+ _session || session,
97
+ _type == Order::BID ? 'buy' : 'sell',
98
+ { amount: _volume, price: _price }
99
+ )
100
+ end
101
+
102
+ def fetch_order(_session, _id)
103
+ normalize_raw_order_status _id, execute_request(
104
+ _session || session,
105
+ 'order_status',
106
+ { id: _id['id'] }
107
+ )
108
+ end
109
+
110
+ def cancel_order(_session, _id)
111
+ # TODO
112
+ end
113
+
114
+ private
115
+
116
+ attr_reader :session
117
+
118
+ def execute_request(_signing, _resource, _data=nil)
119
+ _data = sign_params _signing, _data if _signing
120
+
121
+ JSON.parse(if _data.nil?
122
+ RestClient.get "https://www.bitstamp.net/api/#{_resource}/"
123
+ else
124
+ RestClient.post "https://www.bitstamp.net/api/#{_resource}/", _data
125
+ end)
126
+ end
127
+
128
+ def sign_params(_keys, _params)
129
+ nonce = (Time.now.to_f*10000).to_i.to_s
130
+
131
+ customer_id = _keys[:customer_id].to_s
132
+ api_key = _keys[:api_key]
133
+ api_secret = _keys[:api_secret]
134
+
135
+ digest = OpenSSL::Digest.new('sha256')
136
+ signature_data = "#{nonce}#{customer_id}#{api_key}"
137
+ signature = OpenSSL::HMAC.hexdigest(digest, api_secret, signature_data)
138
+
139
+ (_params || {}).merge({
140
+ nonce: nonce,
141
+ signature: signature.upcase,
142
+ key: api_key
143
+ })
144
+ end
145
+
146
+ def normalize_raw_order(_order)
147
+ BitstampOrder.new({
148
+ 'raw' => _order,
149
+ 'executed_volume' => 0.0,
150
+ 'status' => 'In Queue'
151
+ })
152
+ end
153
+
154
+ def normalize_raw_order_status(_id, _status)
155
+ executed_volume = _status['transactions'].inject(0.0) { |r, t| r + t['btc'].to_f }
156
+ executed_price = _status['transactions'].inject(0.0) { |r, t| r + (t['price'].to_f * t['btc'].to_f) }
157
+
158
+ BitstampOrder.new({
159
+ 'raw' => _id,
160
+ 'executed_volume' => executed_volume,
161
+ 'executed_price' => executed_price / executed_volume,
162
+ 'status' => _status['status']
163
+ })
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,235 @@
1
+ require 'rest-client'
2
+ require 'json'
3
+ require 'trade-o-matic/adapters/base/raw_account_order'
4
+ require 'rainbow'
5
+ require 'rainbow/ext/string'
6
+
7
+ module Trader
8
+ class FakeBackend
9
+ class FakeBalance < Struct.new(:amount, :available_amount);
10
+ end
11
+
12
+ class FakeOrder < RawAccountOrder
13
+ attr_mapped(:id, :id)
14
+ attr_mapped(:pair, :pair)
15
+ attr_mapped(:price, :price)
16
+ attr_mapped(:volume) { |r| r[:pend_volume] + r[:traded_volume] }
17
+ attr_mapped(:executed_volume, :traded_volume)
18
+ attr_mapped(:instruction, :instruction)
19
+ attr_mapped(:status, :status)
20
+
21
+ def limit?
22
+ true
23
+ end
24
+ end
25
+
26
+ attr_reader :pair, :asks, :bids, :verbose
27
+
28
+ def open_bids
29
+ bids.select { |o| o[:status] == AccountOrder::OPEN }
30
+ end
31
+
32
+ def open_asks
33
+ asks.select { |o| o[:status] == AccountOrder::OPEN }
34
+ end
35
+
36
+ def open_orders
37
+ open_bids + open_asks
38
+ end
39
+
40
+ def self.instance
41
+ @@instance ||= self.new
42
+ end
43
+
44
+ def self.reset_instance
45
+ @@instance = nil
46
+ end
47
+
48
+ def initialize(_config=nil)
49
+ get_session(_config) unless _config.nil?
50
+ end
51
+
52
+ def get_available_markets
53
+ [@pair]
54
+ end
55
+
56
+ def fill_book(_book)
57
+ # TODO
58
+ end
59
+
60
+ def available_base_balance
61
+ @base_balance
62
+ end
63
+
64
+ def available_quote_balance
65
+ @quote_balance
66
+ end
67
+
68
+ def total_base_balance
69
+ @base_balance + frozen_base
70
+ end
71
+
72
+ def total_quote_balance
73
+ @quote_balance + frozen_quote
74
+ end
75
+
76
+ def get_session(_config)
77
+ raise ArgumentError, 'must provide login information' if _config.nil?
78
+
79
+ if @config.nil?
80
+ @pair = CurrencyPair.for_code(_config[:base], _config[:quote])
81
+ @base_balance = _config[:base_balance]
82
+ @quote_balance = _config[:quote_balance]
83
+ @verbose = !!_config[:verbose]
84
+ @config = _config
85
+ @bids = []
86
+ @asks = []
87
+ @id = 1
88
+ elsif @config != _config
89
+ raise ArgumentError, 'invalid credentials'
90
+ end
91
+
92
+ nil
93
+ end
94
+
95
+ def get_balance(_session, _currency)
96
+ if _currency == pair.base
97
+ FakeBalance.new(total_base_balance, available_base_balance)
98
+ elsif _currency == pair.quote
99
+ FakeBalance.new(total_quote_balance, available_quote_balance)
100
+ else
101
+ raise 'currency not supported'
102
+ end
103
+ end
104
+
105
+ def get_orders(_session, _pair)
106
+ return [] unless _pair == pair
107
+ (@bids + @asks).map { |o| FakeOrder.new o.clone }
108
+ end
109
+
110
+ def create_order(_session, _pair, _volume, _price, _instruction)
111
+ raise 'market not supported' unless _pair == pair
112
+ raise 'market orders not supported' if _price.nil?
113
+
114
+ if _instruction == Order::ASK
115
+ raise 'Not enough funds' if _volume > @base_balance
116
+ @base_balance -= _volume
117
+ else
118
+ raise 'Not enough funds' if _volume * _price > @quote_balance
119
+ @quote_balance -= _volume * _price
120
+ end
121
+
122
+ info "Creating #{_instruction} order for #{_volume} #{pair.base} @ #{_price} #{pair.quote}", :green
123
+
124
+ raw_order = {
125
+ id: @id.to_s,
126
+ pair: pair,
127
+ instruction: _instruction,
128
+ status: AccountOrder::OPEN,
129
+ price: _price,
130
+ pend_volume: _volume,
131
+ traded_volume: 0.0
132
+ }
133
+
134
+ @id += 1
135
+ (_instruction == Order::ASK ? @asks : @bids) << raw_order
136
+
137
+ FakeOrder.new raw_order.clone
138
+ end
139
+
140
+ def fetch_order(_session, _order_id)
141
+ FakeOrder.new find_order_by_id(_order_id).clone
142
+ end
143
+
144
+ def cancel_order(_session, _order_id)
145
+ order = find_order_by_id(_order_id)
146
+
147
+ info "Closing #{order[:instruction]} order for #{order[:pend_volume] + order[:traded_volume]} #{pair.base} @ #{order[:price]} #{pair.quote}", :red
148
+
149
+ if order[:status] == AccountOrder::OPEN
150
+ if order[:instruction] == Order::ASK
151
+ @base_balance += order[:pend_volume]
152
+ else
153
+ @quote_balance += order[:pend_volume] * order[:price]
154
+ end
155
+ order[:status] = AccountOrder::CANCELED
156
+ end
157
+
158
+ FakeOrder.new order.clone
159
+ end
160
+
161
+ def simulate_buy(_price, _volume)
162
+ ordered_asks = @asks.sort { |b| b[:price] * -1 }
163
+ ordered_asks.each do |order|
164
+ if order[:status] == AccountOrder::OPEN and order[:price] <= _price and _volume > 0
165
+ if order[:pend_volume] > _volume
166
+ @quote_balance += _volume * order[:price]
167
+ order[:pend_volume] -= _volume
168
+ order[:traded_volume] += _volume
169
+ _volume = 0
170
+ else
171
+ @quote_balance += order[:pend_volume] * order[:price]
172
+ _volume -= order[:pend_volume]
173
+ order[:traded_volume] += order[:pend_volume]
174
+ order[:pend_volume] = 0
175
+ end
176
+
177
+ if order[:pend_volume] == 0
178
+ order[:status] = AccountOrder::CLOSED
179
+ end
180
+ end
181
+ end
182
+ end
183
+
184
+ def simulate_sell(_price, _volume)
185
+ ordered_bids = @bids.sort { |b| b[:price] }
186
+ ordered_bids.each do |order|
187
+ if order[:status] == AccountOrder::OPEN and order[:price] >= _price and _volume > 0
188
+ if order[:pend_volume] > _volume
189
+ @base_balance += _volume
190
+ order[:pend_volume] -= _volume
191
+ order[:traded_volume] += _volume
192
+ _volume = 0
193
+ else
194
+ @base_balance += order[:pend_volume]
195
+ _volume -= order[:pend_volume]
196
+ order[:traded_volume] += order[:pend_volume]
197
+ order[:pend_volume] = 0
198
+ end
199
+
200
+ if order[:pend_volume] == 0
201
+ order[:status] = AccountOrder::CLOSED
202
+ end
203
+ end
204
+ end
205
+ end
206
+
207
+ private
208
+
209
+ def info(_msg, _color=:white)
210
+ if verbose
211
+ puts "FakeEx: #{_msg}".color(_color)
212
+ end
213
+ end
214
+
215
+ def frozen_base
216
+ @asks.inject(0) do |r, order|
217
+ next r if order[:status] != AccountOrder::OPEN
218
+ r + order[:pend_volume]
219
+ end
220
+ end
221
+
222
+ def frozen_quote
223
+ @bids.inject(0) do |r, order|
224
+ next r if order[:status] != AccountOrder::OPEN
225
+ r + (order[:pend_volume] * order[:price])
226
+ end
227
+ end
228
+
229
+ def find_order_by_id(_id)
230
+ order = @bids.find { |o| o[:id] == _id }
231
+ order = @asks.find { |o| o[:id] == _id } if order.nil?
232
+ order
233
+ end
234
+ end
235
+ end
@@ -0,0 +1,57 @@
1
+ require 'rest-client'
2
+ require 'json'
3
+ require 'time'
4
+
5
+ module Trader
6
+ class ItbitBackend
7
+ def fill_book(_book)
8
+ # TODO: consider book pair
9
+
10
+ _book.prepare Time.now
11
+
12
+ ob = fetch_raw_order_book _book.pair
13
+ ob['bids'].each { |o| _book.add_bid(o[0].to_f, o[1].to_f) }
14
+ ob['asks'].each { |o| _book.add_ask(o[0].to_f, o[1].to_f) }
15
+
16
+ tx = fetch_raw_transactions _book.pair
17
+ tx['recentTrades'].each do |t|
18
+ _book.add_transaction t['price'].to_f, t['amount'].to_f, Time.parse(t['timestamp'])
19
+ end
20
+ end
21
+
22
+ def get_available_markets
23
+ # TODO.
24
+ end
25
+
26
+ def get_session(_credentials)
27
+ _credentials
28
+ end
29
+
30
+ def get_balance(_session, _currency)
31
+ return Price.new(_currency, 0.0) if _currency.code == :BTC
32
+ return Price.new(_currency, 20000.0) if _currency.code == :Bitstamp_USD
33
+ end
34
+
35
+ def get_orders(_session, _pair)
36
+ end
37
+
38
+ def create_order(_session, _pair, _volume, _price, _type)
39
+ end
40
+
41
+ def fetch_order(_session, _order)
42
+ end
43
+
44
+ def cancel_order(_session, _order)
45
+ end
46
+
47
+ private
48
+
49
+ def fetch_raw_order_book(_pair)
50
+ JSON.parse(RestClient.get "https://api.itbit.com/v1/markets/XBTUSD/order_book")
51
+ end
52
+
53
+ def fetch_raw_transactions(_pair)
54
+ JSON.parse(RestClient.get "https://api.itbit.com/v1/markets/XBTUSD/trades")
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,149 @@
1
+ require 'rest-client'
2
+ require 'json'
3
+ require 'trade-o-matic/adapters/base/raw_account_order'
4
+ require 'trade-o-matic/adapters/base/raw_balance'
5
+
6
+ module Trader
7
+ class SurbtcBackend
8
+ API_PREFIX = 'https://www.surbtc.com/api/v1'
9
+
10
+ TYPE_MAP = {
11
+ 'ask' => Order::ASK,
12
+ 'bid' => Order::BID
13
+ }
14
+
15
+ STATUS_MAP = {
16
+ 'received' => AccountOrder::PENDING,
17
+ 'pending' => AccountOrder::OPEN,
18
+ 'traded' => AccountOrder::CLOSED,
19
+ 'canceling' => AccountOrder::OPEN,
20
+ 'canceled' => AccountOrder::CANCELED
21
+ }
22
+
23
+ MAIN_MARKET = CurrencyPair.new(:SATOSHI, :SURBTC_CLP)
24
+
25
+ class SurbtcBalance < RawBalance
26
+ attr_mapped :amount
27
+ attr_mapped :available_amount
28
+ end
29
+
30
+ class SurbtcBalanceBTC < RawBalance
31
+ attr_mapped(:amount) { |r| r['amount'].to_f }
32
+ attr_mapped(:available_amount) { |r| r['available_amount'].to_f }
33
+ end
34
+
35
+ class SurbtcOrder < RawAccountOrder
36
+ attr_mapped(:id)
37
+ attr_mapped(:pair) { |r| MAIN_MARKET }
38
+ attr_mapped(:price) { |r| r['limit'] }
39
+ attr_mapped(:volume) { |r| r['original_amount'].to_f }
40
+ attr_mapped(:executed_volume) { |r| r['total_exchanged'].to_f }
41
+ attr_mapped(:instruction) { |r| TYPE_MAP[r['type']] }
42
+ attr_mapped(:status) { |r| STATUS_MAP[r['state']] }
43
+ attr_mapped(:limit?) { |r| r['price_type'] == 'limit' }
44
+ end
45
+
46
+ def get_available_markets
47
+ [MAIN_MARKET]
48
+ end
49
+
50
+ def fill_book(_book)
51
+ # TODO
52
+ end
53
+
54
+ def get_session(_credentials)
55
+ _credentials
56
+ end
57
+
58
+ def get_balance(_session, _currency)
59
+ res = resource_for "balances/#{currency_code_for(_currency)}", _session
60
+ raw_balance = postprocess res.get
61
+
62
+ if _currency == MAIN_MARKET.base
63
+ SurbtcBalanceBTC.new raw_balance['balance']
64
+ else
65
+ SurbtcBalance.new raw_balance['balance']
66
+ end
67
+ end
68
+
69
+ def get_orders(_session, _pair)
70
+ res = resource_for "markets/#{market_code_for(_pair)}/orders", _session
71
+
72
+ raw_orders = postprocess res.get
73
+ raw_orders['orders'].map { |o| SurbtcOrder.new o }
74
+ end
75
+
76
+ def create_order(_session, _pair, _volume, _price, _instruction)
77
+ raise 'market not supported' unless _pair == MAIN_MARKET
78
+
79
+ res = resource_for "markets/#{market_code_for(_pair)}/orders", _session
80
+
81
+ raw_order = postprocess res.post({
82
+ order: build_order_json(_price, _volume, _instruction)
83
+ }, :content_type => 'application/json')
84
+
85
+ SurbtcOrder.new raw_order['order']
86
+ end
87
+
88
+ def fetch_order(_session, _order_id)
89
+ res = resource_for "orders/#{_order_id}", _session
90
+
91
+ raw_order = postprocess res.get
92
+ SurbtcOrder.new raw_order['order']
93
+ end
94
+
95
+ def cancel_order(_session, _order_id)
96
+ res = resource_for "orders/#{_order_id}", _session
97
+
98
+ raw_order = postprocess res.put({
99
+ state: 'canceling'
100
+ }, :content_type => 'application/json')
101
+
102
+ while raw_order['order']['state'] == 'canceling'
103
+ raw_order = postprocess res.get
104
+ end
105
+
106
+ SurbtcOrder.new raw_order['order']
107
+ end
108
+
109
+ private
110
+
111
+ def resource_for(_url, _session=nil)
112
+ res = RestClient::Resource.new "#{API_PREFIX}/#{_url}"
113
+ res.options[:headers] = { params: { 'api_key' => _session[:api_key] } } if _session
114
+ res
115
+ end
116
+
117
+ def postprocess(_response)
118
+ # TODO: handle error responses
119
+ JSON.parse _response
120
+ end
121
+
122
+ def currency_code_for(_currency)
123
+ return 'clp' if _currency.code == :SURBTC_CLP
124
+ return 'btc' if _currency.code == :SATOSHI
125
+ _currency.to_s.downcase
126
+ end
127
+
128
+ def market_code_for(_pair)
129
+ "#{currency_code_for(_pair.base)}-#{currency_code_for(_pair.quote)}"
130
+ end
131
+
132
+ def build_order_json(_price, _volume, _instruction)
133
+ if _price.nil?
134
+ {
135
+ amount: _volume,
136
+ type: _instruction == Order::BID ? 'bid' : 'ask',
137
+ price_type: 'market'
138
+ }
139
+ else
140
+ {
141
+ limit: _price,
142
+ amount: _volume,
143
+ type: _instruction == Order::BID ? 'bid' : 'ask',
144
+ price_type: 'limit'
145
+ }
146
+ end
147
+ end
148
+ end
149
+ end