trade-o-matic 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/trade-o-matic/adapters/base/raw_account_order.rb +7 -0
- data/lib/trade-o-matic/adapters/base/raw_balance.rb +7 -0
- data/lib/trade-o-matic/adapters/base/raw_resource.rb +31 -0
- data/lib/trade-o-matic/adapters/bitstamp_backend.rb +166 -0
- data/lib/trade-o-matic/adapters/fake_backend.rb +235 -0
- data/lib/trade-o-matic/adapters/itbit_backend.rb +57 -0
- data/lib/trade-o-matic/adapters/surbtc_backend.rb +149 -0
- data/lib/trade-o-matic/cli.rb +6 -22
- data/lib/trade-o-matic/converters/compound_converter.rb +13 -0
- data/lib/trade-o-matic/converters/fixed_converter.rb +0 -7
- data/lib/trade-o-matic/converters/inverse_converter.rb +13 -0
- data/lib/trade-o-matic/converters/json_api_converter.rb +20 -0
- data/lib/trade-o-matic/converters/sync_converter.rb +1 -11
- data/lib/trade-o-matic/core/account.rb +75 -0
- data/lib/trade-o-matic/core/account_order.rb +99 -0
- data/lib/trade-o-matic/core/account_proxy.rb +48 -0
- data/lib/trade-o-matic/core/balance.rb +57 -0
- data/lib/trade-o-matic/core/exchange.rb +19 -0
- data/lib/trade-o-matic/core/market.rb +29 -0
- data/lib/trade-o-matic/core/market_loader.rb +29 -0
- data/lib/trade-o-matic/services/backend_factory.rb +46 -0
- data/lib/trade-o-matic/structs/ask_slope.rb +4 -1
- data/lib/trade-o-matic/structs/bid_slope.rb +4 -1
- data/lib/trade-o-matic/structs/book.rb +52 -0
- data/lib/trade-o-matic/structs/converter.rb +2 -3
- data/lib/trade-o-matic/structs/currency.rb +126 -21
- data/lib/trade-o-matic/structs/currency_pair.rb +31 -5
- data/lib/trade-o-matic/structs/order.rb +53 -1
- data/lib/trade-o-matic/structs/price.rb +9 -8
- data/lib/trade-o-matic/structs/slope.rb +11 -34
- data/lib/trade-o-matic/support/converter_configurator.rb +35 -0
- data/lib/trade-o-matic/version.rb +1 -1
- data/lib/trade-o-matic.rb +29 -1
- metadata +59 -43
- data/lib/trade-o-matic/adapters/bitstamp_account.rb +0 -63
- data/lib/trade-o-matic/flows/ask_replicator_flow.rb +0 -50
- data/lib/trade-o-matic/generators/linear_generator.rb +0 -37
- 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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9b2ab1e0af6b6c8685d51d92657660229ac270e4
|
4
|
+
data.tar.gz: f03f687cbae9e0d8436d5b879166ae186a57bd16
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c524eb2a14bf4d3258e98356ed20493b2ce7943488833137c53fc338ba9a4181c082a4d8a63e0eb08d46cc0a4d0f8e3d400d6ce38c1909cba93f493d75d1db92
|
7
|
+
data.tar.gz: 3e5696eafbdcf0e0022166540eace85363ae4f88fb28704d14ea27cf1c1596c5868e8f97c5a2d2ee8756f54f993f2f95b72b0ced2bbdf1da9bb4f64da90adac6
|
@@ -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
|