trade-o-matic 0.3.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 69c3f0ffd768200acac1e0ccd836c5daaac08e59
4
- data.tar.gz: 7e41570aa4c2a894861134cb50a670787384fe40
3
+ metadata.gz: 2e3ca1570f412bde6d4c5f68a6645e6d9fed6cb3
4
+ data.tar.gz: 1c070414b51612d93db781c98ffd4d6893de7f7f
5
5
  SHA512:
6
- metadata.gz: 3dc8f24f61ba90595a9e5a39dfdd66f753762f3b32fb749e873f299f57ff6156bab77780180811446d5ae576ee1a45fc8e1f2efec4e9d9660c3b96ec5f0687e4
7
- data.tar.gz: 470ce3b882cd7f45fee5330ccbd258c382e494be8836bf5392c70aaf0736f965de6f0741877ae820748002d55bb80ca5ab580944df440aa9f742d38a11e75d0d
6
+ metadata.gz: bf4fea6ae9e13c29f3df4546a8ffdceadcff26575acec5f01dd30a181e91780652d2a39f4c244d6bea56773142477555a10d339bc01cd322c879c5dbaa408c8d
7
+ data.tar.gz: d80321f50158a5c28878754b0395ed6eb162cd1c0d867293c5cad64d238107177a497d9785b7d9a7b9ca43ccce99f2619b2ba3a1b13e508bb8c7f726b6ffbffc
@@ -0,0 +1,46 @@
1
+ module Trader
2
+ class GameBackend
3
+ class CancelOrder < Command.new(:state, :account, :order_id)
4
+ def perform
5
+ raise ArgumentError, 'order belong to another account' if order['account'] != account
6
+ raise ArgumentError, 'order already closed' if order['status'] != 'open'
7
+
8
+ market['open'].delete order
9
+ market['closed'] << order
10
+ order['status'] = 'canceled'
11
+
12
+ base_balance = state.balance_for account, market['base']
13
+ quote_balance = state.balance_for account, market['quote']
14
+
15
+ if bid?
16
+ quote = SFM.quote order['volume'], order['limit']
17
+ quote_balance['available'] += quote
18
+ quote_balance['frozen'] -= quote
19
+ else
20
+ base_balance['available'] += order['volume']
21
+ base_balance['frozen'] -= order['volume']
22
+ end
23
+
24
+ GameOrder.new order
25
+ end
26
+
27
+ private
28
+
29
+ def bid?
30
+ order['instruction'] == 'bid'
31
+ end
32
+
33
+ def order
34
+ result[:order]
35
+ end
36
+
37
+ def market
38
+ result[:market]
39
+ end
40
+
41
+ def result
42
+ @result ||= state.find_order_by_id order_id
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,15 @@
1
+ module Trader
2
+ class GameBackend
3
+ module Configuration
4
+ extend self
5
+
6
+ attr_accessor :db_file
7
+ attr_reader :markets
8
+
9
+ def setup_market(_base, _quote, _options = {})
10
+ @markets ||= []
11
+ @markets << CurrencyPair.for_code(_base, _quote)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,190 @@
1
+ module Trader
2
+ class GameBackend
3
+ class ExecuteOrder < Command.new(:state, :account, :pair, :volume, :limit, :instruction)
4
+ def perform
5
+ @market = state.market_for pair
6
+ @base_balance = state.balance_for account, @market['base']
7
+ @quote_balance = state.balance_for account, @market['quote']
8
+
9
+ if bid?
10
+ raise 'Not enough funds' if limit_order? and !enough_funds_for_bid?
11
+ load_bid_candidates
12
+ match_candidates
13
+ raise 'Not enough funds' if market_order? and !can_execute_market_bid?
14
+ execute_bid_txs
15
+ update_balance_on_bid unless order_fully_consumed?
16
+ store_order 'bid'
17
+ else
18
+ raise 'Not enough funds' unless enough_funds_for_ask?
19
+ load_ask_candidates
20
+ match_candidates
21
+ execute_ask_txs
22
+ update_balance_on_ask unless order_fully_consumed?
23
+ store_order 'ask'
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def bid?
30
+ instruction == Order::BID
31
+ end
32
+
33
+ def market_order?
34
+ limit.nil?
35
+ end
36
+
37
+ def limit_order?
38
+ !limit.nil?
39
+ end
40
+
41
+ def enough_funds_for_bid?
42
+ @quote_balance['available'] >= safe_quote(volume, limit)
43
+ end
44
+
45
+ def enough_funds_for_ask?
46
+ @base_balance['available'] >= volume
47
+ end
48
+
49
+ def load_bid_candidates
50
+ @candidates = @market['open'].select do |order|
51
+ next false if order['instruction'] != 'ask'
52
+ next true if limit.nil?
53
+ order['limit'] <= limit
54
+ end
55
+
56
+ @candidates.sort! do |a, b|
57
+ r = a['limit'] <=> b['limit']
58
+ r = a['ts'] <=> b['ts'] if r == 0
59
+ r
60
+ end
61
+ end
62
+
63
+ def load_ask_candidates
64
+ @candidates = @market['open'].select do |order|
65
+ next false if order['instruction'] != 'bid'
66
+ next true if limit.nil?
67
+ order['limit'] >= limit
68
+ end
69
+
70
+ @candidates.sort! do |a, b|
71
+ r = b['limit'] <=> a['limit']
72
+ r = a['ts'] <=> b['ts'] if r == 0
73
+ r
74
+ end
75
+ end
76
+
77
+ def match_candidates
78
+ @txs = []
79
+ @remaining = volume
80
+ @candidates.each do |other|
81
+ if @remaining >= other['volume']
82
+ @remaining -= other['volume']
83
+ @txs << { order: other, volume: other['volume'] }
84
+ else
85
+ @txs << { order: other, volume: @remaining }
86
+ @remaining = 0.0
87
+ break
88
+ end
89
+ end
90
+ end
91
+
92
+ def can_execute_market_bid?
93
+ available = @quote_balance['available']
94
+ @txs.each do |tx|
95
+ quote = safe_quote(tx[:volume], tx[:order]['limit'])
96
+ return false if quote > available
97
+ available -= quote
98
+ end
99
+ true
100
+ end
101
+
102
+ def execute_bid_txs
103
+ process_each_tx do |tx|
104
+ @base_balance['available'] -= tx[:volume]
105
+ @quote_balance['available'] += tx_quote(tx)
106
+ order_base_balance(tx[:order])['available'] += tx[:volume]
107
+ order_quote_balance(tx[:order])['frozen'] -= tx_quote(tx)
108
+ end
109
+ end
110
+
111
+ def execute_ask_txs
112
+ process_each_tx do |tx|
113
+ @base_balance['available'] -= tx[:volume]
114
+ @quote_balance['available'] += tx_quote(tx)
115
+ order_base_balance(tx[:order])['available'] += tx[:volume]
116
+ order_quote_balance(tx[:order])['frozen'] -= tx_quote(tx)
117
+ end
118
+ end
119
+
120
+ def store_order(_instruction)
121
+ order = {
122
+ 'id' => state.unique_id,
123
+ 'account' => account,
124
+ 'pair' => pair.to_s, # just a little denorm to simplify
125
+ 'instruction' => _instruction,
126
+ 'limit' => limit,
127
+ 'volume' => @remaining,
128
+ 'original_volume' => volume
129
+ }
130
+
131
+ if order_fully_consumed?
132
+ order['status'] = 'closed'
133
+ @market['closed'] << order
134
+ else
135
+ order['status'] = 'open'
136
+ @market['open'] << order
137
+ end
138
+
139
+ GameOrder.new order
140
+ end
141
+
142
+ def update_balance_on_bid
143
+ remaining_quote = safe_quote(@remaining, limit)
144
+ @quote_balance['available'] -= remaining_quote
145
+ @quote_balance['frozen'] += remaining_quote
146
+ end
147
+
148
+ def update_balance_on_ask
149
+ @base_balance['available'] -= @remaining
150
+ @base_balance['frozen'] += @remaining
151
+ end
152
+
153
+ def order_fully_consumed?
154
+ market_order? or @remaining <= 0
155
+ end
156
+
157
+ def process_each_tx
158
+ @txs.each do |tx|
159
+ yield tx
160
+ tx[:order]['volume'] -= tx[:volume]
161
+ update_status tx[:order]
162
+ end
163
+ end
164
+
165
+ def order_base_balance(_order)
166
+ state.balance_for _order['account'], @market['base']
167
+ end
168
+
169
+ def order_quote_balance(_order)
170
+ state.balance_for _order['account'], @market['quote']
171
+ end
172
+
173
+ def tx_quote(_tx)
174
+ safe_quote(_tx[:volume], _tx[:order]['limit'])
175
+ end
176
+
177
+ def update_status(_order)
178
+ if _order['volume'] == 0
179
+ _order['status'] = 'closed'
180
+ @market['open'].delete _order
181
+ @market['closed'] << _order
182
+ end
183
+ end
184
+
185
+ def safe_quote(_volume, _price)
186
+ SFM.quote(_volume, _price)
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,23 @@
1
+ module Trader
2
+ class GameBackend
3
+ module SFM # Safe Float Multiplier (wtf)
4
+ extend self
5
+
6
+ FACTOR = 1_000_000
7
+
8
+ def quote(_volume, _price)
9
+ (_volume * _price) / FACTOR
10
+ end
11
+
12
+ def encode(_amount)
13
+ return nil if _amount.nil?
14
+ (_amount * FACTOR).to_i
15
+ end
16
+
17
+ def decode(_amount)
18
+ return nil if _amount.nil?
19
+ _amount.to_f / FACTOR
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,84 @@
1
+ require 'yaml'
2
+
3
+ module Trader
4
+ class GameBackend
5
+ class State
6
+ def initialize
7
+ reset
8
+ end
9
+
10
+ def load_from_file(_path)
11
+ if File.exists? _path
12
+ @state = YAML.load File.read(_path)
13
+ else
14
+ reset
15
+ end
16
+ end
17
+
18
+ def store_to_file(_path)
19
+ File.write _path, YAML.dump(@state)
20
+ end
21
+
22
+ def all_markets
23
+ state['markets'].values.map { |m| CurrencyPair.new(m['base'], m['quote']) }
24
+ end
25
+
26
+ def unique_id
27
+ state['next_id'] += 1
28
+ end
29
+
30
+ def account_for(_account_name, create: true)
31
+ account = state['accounts'][_account_name]
32
+ account = state['accounts'][_account_name] = {} if account.nil? && create
33
+ account
34
+ end
35
+
36
+ def balance_for(_account_name, _currency, create: true)
37
+ account = account_for(_account_name)
38
+ balance = account[_currency.to_s]
39
+ balance = account[_currency.to_s] = new_balance if balance.nil? && create
40
+ balance
41
+ end
42
+
43
+ def market_for(_pair, create: true)
44
+ market = state['markets'][_pair.to_s]
45
+ market = state['markets'][_pair.to_s] = new_market_for(_pair) if market.nil? && create
46
+ market
47
+ end
48
+
49
+ def find_order_by_id(_id)
50
+ state['markets'].values.each do |market|
51
+ order = market['open'].find { |o| o['id'] == _id } ||
52
+ market['closed'].find { |o| o['id'] == _id }
53
+ return { market: market, order: order } unless order.nil?
54
+ end
55
+ raise ArgumentError, 'order id not found'
56
+ end
57
+
58
+ private
59
+
60
+ attr_reader :state
61
+
62
+ def new_market_for(_pair)
63
+ {
64
+ 'base' => _pair.base.to_s,
65
+ 'quote' => _pair.quote.to_s,
66
+ 'open' => [],
67
+ 'closed' => []
68
+ }
69
+ end
70
+
71
+ def new_balance
72
+ { 'available' => 0, 'frozen' => 0 }
73
+ end
74
+
75
+ def reset
76
+ @state = {
77
+ 'next_id' => 0,
78
+ 'accounts' => {},
79
+ 'markets' => {}
80
+ }
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,168 @@
1
+ require 'json'
2
+ require 'rainbow'
3
+ require 'rainbow/ext/string'
4
+ require 'trade-o-matic/adapters/base/raw_account_order'
5
+ require 'trade-o-matic/adapters/base/raw_balance'
6
+
7
+ require 'trade-o-matic/adapters/game_backend/configuration'
8
+ require 'trade-o-matic/adapters/game_backend/sfm'
9
+ require 'trade-o-matic/adapters/game_backend/state'
10
+ require 'trade-o-matic/adapters/game_backend/execute_order'
11
+ require 'trade-o-matic/adapters/game_backend/cancel_order'
12
+
13
+ module Trader
14
+ class GameBackend
15
+ STATUS_MAP = {
16
+ 'open' => AccountOrder::OPEN,
17
+ 'closed' => AccountOrder::CLOSED,
18
+ 'canceled' => AccountOrder::CANCELED
19
+ }
20
+
21
+ class GameBalance < RawBalance
22
+ attr_mapped(:amount) { |r| SFM.decode(r['available'] + r['frozen']) }
23
+ attr_mapped(:available_amount) { |r| SFM.decode r['available'] }
24
+ end
25
+
26
+ class GameOrder < RawAccountOrder
27
+ attr_mapped(:id)
28
+ attr_mapped(:pair) { |r| CurrencyPair.parse r['pair'] }
29
+ attr_mapped(:price) { |r| SFM.decode r['limit'] }
30
+ attr_mapped(:volume) { |r| SFM.decode r['original_volume'] }
31
+ attr_mapped(:executed_volume) { |r| SFM.decode(r['original_volume'] - r['volume']) }
32
+ attr_mapped(:instruction)
33
+ attr_mapped(:status) { |r| STATUS_MAP[r['status']] }
34
+
35
+ def limit?
36
+ true
37
+ end
38
+ end
39
+
40
+ attr_reader :verbose, :state
41
+
42
+ def initialize
43
+ @state = State.new
44
+ end
45
+
46
+ def increment_balance(_account_name, _currency, _amount = nil)
47
+ if _amount.nil?
48
+ _amount = _currency.amount
49
+ _currency = _currency.currency
50
+ end
51
+
52
+ with_session do
53
+ balance = state.balance_for(_account_name, Currency.for_code(_currency))
54
+ balance['available'] += SFM.encode(_amount)
55
+ end
56
+ end
57
+
58
+ def decrement_balance(_account_name, _currency, _amount = nil)
59
+ if _amount.nil?
60
+ _amount = _currency.amount
61
+ _currency = _currency.currency
62
+ end
63
+
64
+ with_session do
65
+ balance = state.balance_for(_account_name, Currency.for_code(_currency))
66
+ balance['available'] -= SFM.encode(_amount)
67
+ balance['available'] = 0 if balance['available'] < 0
68
+ end
69
+ end
70
+
71
+ # backend implementation:
72
+
73
+ def get_available_markets
74
+ with_session(true) do
75
+ state.all_markets
76
+ end
77
+ end
78
+
79
+ def fill_book(_book)
80
+ # TODO
81
+ end
82
+
83
+ def get_session(_config)
84
+ raise ArgumentError, 'must provide login information' if _config.nil? or _config[:name].nil?
85
+ _config[:name]
86
+ end
87
+
88
+ def get_balance(_session, _currency)
89
+ with_session(true) do
90
+ balance = state.balance_for(_session, _currency)
91
+ GameBalance.new balance
92
+ end
93
+ end
94
+
95
+ def get_orders(_session, _pair)
96
+ with_session(true) do
97
+ market = state.market_for(_pair)
98
+ open = market['open'].select { |o| o['account'] = _session }
99
+ closed = market['closed'].select { |o| o['account'] = _session }
100
+ (open + closed).map { |o| GameOrder.new o }
101
+ end
102
+ end
103
+
104
+ def fetch_order(_session, _order_id)
105
+ with_session(true) do
106
+ GameOrder.new state.find_order_by_id(_order_id.to_i)[:order]
107
+ end
108
+ end
109
+
110
+ def create_order(_session, _pair, _volume, _price, _instruction)
111
+ with_session do
112
+ execute_order = ExecuteOrder.new
113
+ execute_order.state = state
114
+ execute_order.account = _session
115
+ execute_order.pair = _pair
116
+ execute_order.volume = SFM.encode _volume
117
+ execute_order.limit = SFM.encode _price
118
+ execute_order.instruction = _instruction
119
+ execute_order.perform
120
+ end
121
+ end
122
+
123
+ def cancel_order(_session, _order_id)
124
+ with_session do
125
+ cancel_order = CancelOrder.new
126
+ cancel_order.state = state
127
+ cancel_order.account = _session
128
+ cancel_order.order_id = _order_id.to_i
129
+ cancel_order.perform
130
+ end
131
+ end
132
+
133
+ private
134
+
135
+ def with_session(_idemponent = false)
136
+ restore_state_from_file if use_state_file?
137
+ ensure_required_markets
138
+ result = yield
139
+ save_state_to_file if !_idemponent && use_state_file?
140
+ return result
141
+ end
142
+
143
+ def use_state_file?
144
+ !config.db_file.nil?
145
+ end
146
+
147
+ def restore_state_from_file
148
+ state.load_from_file config.db_file
149
+ end
150
+
151
+ def save_state_to_file
152
+ state.store_to_file config.db_file
153
+ end
154
+
155
+ def ensure_required_markets
156
+ return false unless config.markets
157
+ config.markets.each { |mc| state.market_for(mc) }
158
+ end
159
+
160
+ def config
161
+ Configuration
162
+ end
163
+
164
+ def info(_msg, _color = :white)
165
+ puts "GameEx: #{_msg}".color(_color) if verbose
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,25 @@
1
+ module Command
2
+ def self.new(*_attributes)
3
+ attr_names = []
4
+ attr_defaults = {}
5
+
6
+ _attributes.each do |att|
7
+ if att.is_a? Hash
8
+ attr_defaults.merge att
9
+ attr_names += att.keys
10
+ else
11
+ attr_names << att
12
+ end
13
+ end
14
+
15
+ Struct.new(*attr_names) do
16
+ define_method(:initialize) do |kwargs={}|
17
+ kwargs = attr_defaults.merge kwargs
18
+ attr_values = attr_names.map { |a| kwargs[a] }
19
+ super(*attr_values)
20
+ end
21
+
22
+ define_method(:perform) {}
23
+ end
24
+ end
25
+ end
@@ -23,6 +23,11 @@ module Trader
23
23
 
24
24
  private
25
25
 
26
+ def build_game
27
+ require_relative '../adapters/game_backend'
28
+ GameBackend.new
29
+ end
30
+
26
31
  def build_bitstamp
27
32
  require_relative '../adapters/bitstamp_backend'
28
33
  BitstampBackend.new
@@ -2,6 +2,10 @@ module Trader
2
2
  class CurrencyPair
3
3
  attr_reader :base, :quote
4
4
 
5
+ def self.parse(_string)
6
+ new *(_string.split '/')
7
+ end
8
+
5
9
  def self.for_code(_pair, _quote=nil)
6
10
  return _pair if _pair.is_a? self
7
11
  self.new _pair, _quote
@@ -1,3 +1,3 @@
1
1
  module Trader
2
- VERSION = "0.3.0"
2
+ VERSION = "0.3.1"
3
3
  end
data/lib/trade-o-matic.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require "trade-o-matic/version"
2
2
  require 'trade-o-matic/standard'
3
+ require 'trade-o-matic/command'
3
4
 
4
5
  require 'trade-o-matic/structs/currency'
5
6
  require 'trade-o-matic/structs/currency_pair'
@@ -28,7 +29,6 @@ require 'trade-o-matic/converters/inverse_converter'
28
29
  require 'trade-o-matic/converters/compound_converter'
29
30
 
30
31
  module Trader
31
-
32
32
  # Default conversions
33
33
  Currency.register_conversion :BTC, :SATOSHI, 100_000_000
34
34
  Currency.register_conversion :SATOSHI, :BTC, 0.00000001
@@ -45,4 +45,14 @@ module Trader
45
45
  exchange(_backend).login(_credentials)
46
46
  end
47
47
 
48
+ def self.setup_game_backend(&_block)
49
+ require 'trade-o-matic/adapters/game_backend'
50
+ GameBackend::Configuration.tap &_block
51
+ nil
52
+ end
53
+
54
+ def self.game
55
+ require 'trade-o-matic/adapters/game_backend'
56
+ GameBackend.new
57
+ end
48
58
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: trade-o-matic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ignacio Baixas
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-03-18 00:00:00.000000000 Z
11
+ date: 2016-04-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rest-client
@@ -241,9 +241,16 @@ files:
241
241
  - lib/trade-o-matic/adapters/base/raw_resource.rb
242
242
  - lib/trade-o-matic/adapters/bitstamp_backend.rb
243
243
  - lib/trade-o-matic/adapters/fake_backend.rb
244
+ - lib/trade-o-matic/adapters/game_backend.rb
245
+ - lib/trade-o-matic/adapters/game_backend/cancel_order.rb
246
+ - lib/trade-o-matic/adapters/game_backend/configuration.rb
247
+ - lib/trade-o-matic/adapters/game_backend/execute_order.rb
248
+ - lib/trade-o-matic/adapters/game_backend/sfm.rb
249
+ - lib/trade-o-matic/adapters/game_backend/state.rb
244
250
  - lib/trade-o-matic/adapters/itbit_backend.rb
245
251
  - lib/trade-o-matic/adapters/surbtc_backend.rb
246
252
  - lib/trade-o-matic/cli.rb
253
+ - lib/trade-o-matic/command.rb
247
254
  - lib/trade-o-matic/converters/compound_converter.rb
248
255
  - lib/trade-o-matic/converters/fixed_converter.rb
249
256
  - lib/trade-o-matic/converters/inverse_converter.rb