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.
- 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
|