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.
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
@@ -6,34 +6,18 @@ module Trader
6
6
 
7
7
  program_desc 'Trader command line interface'
8
8
 
9
- desc "Executes trade-o-matic using provided settings"
10
- command [:run, :r] do |c|
9
+ desc "Executes a trade-o-matic enviroment"
10
+ command [:console, :r] do |c|
11
11
 
12
- c.desc "Specifies the configuration file"
12
+ c.desc "Specifies the robot configuration file"
13
13
  c.flag [:c, :config]
14
14
 
15
- c.desc "Start the server in verbose mode"
16
- c.switch :dry, :default_value => false
17
-
18
15
  c.action do |global_options,options,args|
19
16
 
20
- require 'trade-o-matic/adapters/bitstamp_account'
21
- require 'trade-o-matic/generators/linear_generator'
22
- require 'trade-o-matic/flows/ask_replicator_flow'
23
-
24
- Currency.register_conversion(:CLP, :Bitstamp_USD, 1.0/710.0)
25
-
26
- source = Trader::BitstampAccount.new
27
- target = nil
28
- generator = LinearGenerator.new(spread: Price.new(:CLP, 10000.0), step: Price.new(:CLP, 1000.0))
29
-
30
- flow = Trader::AskReplicatorFlow.new source, target, generator
31
-
32
- puts 'Running ask replicator'
33
- orders = flow.run options[:dry]
17
+ config = YAML.load_file options[:config]
34
18
 
35
- puts 'Generated the following orders :'
36
- orders.each { |o| puts "#{o.price} [#{o.volume}]" }
19
+ # TODO: load accounts from config file
20
+ # TODO: start a REPL in loaded accounts context
37
21
  end
38
22
  end
39
23
 
@@ -0,0 +1,13 @@
1
+ module Trader
2
+ class CompoundConverter < Converter
3
+ attr_accessor :converters
4
+
5
+ def initialize(_converters)
6
+ @converters = _converters
7
+ end
8
+
9
+ def rate
10
+ converters.inject(1.0) { |r, c| c.rate * r }
11
+ end
12
+ end
13
+ end
@@ -1,16 +1,9 @@
1
1
  module Trader
2
-
3
2
  class FixedConverter < Converter
4
-
5
3
  attr_accessor :rate
6
4
 
7
5
  def initialize(_rate)
8
6
  @rate = _rate
9
7
  end
10
-
11
- def convert(_value, _from, _to)
12
- _value * rate
13
- end
14
-
15
8
  end
16
9
  end
@@ -0,0 +1,13 @@
1
+ module Trader
2
+ class InverseConverter < Converter
3
+ attr_reader :inverse
4
+
5
+ def initialize(_inverse)
6
+ @inverse = _inverse
7
+ end
8
+
9
+ def rate
10
+ 1.0 / inverse.rate
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,20 @@
1
+ require 'rest-client'
2
+ require 'json'
3
+
4
+ module Trader
5
+ class JsonApiConverter < SyncConverter
6
+ attr_reader :url, :path
7
+
8
+ def initialize(_url, _path, _ttl=30)
9
+ @url = _url
10
+ @path = _path
11
+
12
+ super(_ttl) do
13
+ json_string = RestClient.get _url
14
+ json = JSON.parse json_string
15
+ value = _path.inject(json) { |r, k| r[k] }
16
+ value.to_f
17
+ end
18
+ end
19
+ end
20
+ end
@@ -1,7 +1,5 @@
1
1
  module Trader
2
-
3
2
  class SyncConverter < Converter
4
-
5
3
  attr_reader :ttl
6
4
 
7
5
  def initialize(_ttl, &_block)
@@ -13,14 +11,7 @@ module Trader
13
11
  @last_sync < Time.new - @ttl
14
12
  end
15
13
 
16
- def convert(_value, _from, _to)
17
- rate = sync_rate
18
- _value * rate
19
- end
20
-
21
- private
22
-
23
- def sync_rate
14
+ def rate
24
15
  if @last_sync.nil? or expired?
25
16
  @rate = @block.call
26
17
  @last_sync = Time.now
@@ -28,6 +19,5 @@ module Trader
28
19
 
29
20
  @rate
30
21
  end
31
-
32
22
  end
33
23
  end
@@ -0,0 +1,75 @@
1
+ module Trader
2
+ class Account
3
+ include MarketLoader
4
+
5
+ attr_reader :backend
6
+
7
+ def initialize(_backend, _session)
8
+ @backend = _backend
9
+ @session = _session
10
+ end
11
+
12
+ def using(_pair, _quote=nil, &_block)
13
+ _pair = CurrencyPair.for_code _pair, _quote
14
+
15
+ proxy_market = find_exact_market _pair
16
+ proxy_market = find_compatible_market _pair if proxy_market.nil?
17
+ raise StandardError, 'market not supported' if proxy_market.nil?
18
+
19
+ proxy = AccountProxy.new self, proxy_market, _pair
20
+
21
+ return proxy if _block.nil?
22
+ _block.call proxy
23
+ end
24
+
25
+ def using_default_pair(&_block)
26
+ using available_markets.first, &_block
27
+ end
28
+
29
+ def balance_for(_currency)
30
+ _currency = Currency.for_code _currency
31
+ ensure_supported_currency _currency
32
+
33
+ # IDEA: cache balances by currency code.
34
+ balance = Balance.new backend, session, _currency
35
+ balance.refresh!
36
+ balance
37
+ end
38
+
39
+ def create_order(_order)
40
+ ensure_supported_market _order.pair
41
+ raw_order = backend.create_order(
42
+ session,
43
+ _order.pair,
44
+ _order.volume.amount,
45
+ _order.limit? ? _order.price.amount : nil,
46
+ _order.instruction
47
+ )
48
+
49
+ AccountOrder.new backend, session, raw_order
50
+ end
51
+
52
+ def find_order(_id)
53
+ raw_order = backend.fetch_order session, _id
54
+ # TODO: maybe Position.new(backend, session, Market.new(backend, raw_order.pair), raw_order)
55
+ AccountOrder.new backend, session, raw_order
56
+ end
57
+
58
+ def list_orders
59
+ # TODO.
60
+ end
61
+
62
+ private
63
+
64
+ attr_reader :session
65
+
66
+ def find_exact_market(_pair)
67
+ available_markets.find { |p| p == _pair }
68
+ end
69
+
70
+ def find_compatible_market(_pair)
71
+ available_markets.find { |p| p.compatible_with? _pair }
72
+ end
73
+
74
+ end
75
+ end
@@ -0,0 +1,99 @@
1
+ module Trader
2
+ class AccountOrder
3
+
4
+ PENDING = :pending
5
+ OPEN = :open
6
+ CLOSED = :closed
7
+ CANCELED = :canceled
8
+
9
+ def initialize(_backend, _session, _raw, _forced_pair=nil)
10
+ @backend = _backend
11
+ @session = _session
12
+ @raw = _raw
13
+ @forced_pair = _forced_pair
14
+ end
15
+
16
+ def convert_to(_pair, _quote=nil)
17
+ forced_pair = CurrencyPair.for_code _pair, _quote
18
+
19
+ return self if forced_pair == pair
20
+
21
+ self.class.new backend, session, @raw, forced_pair
22
+ end
23
+
24
+ def id
25
+ @raw.id
26
+ end
27
+
28
+ def pair
29
+ forced_pair || original_pair
30
+ end
31
+
32
+ def original_pair
33
+ @raw.pair
34
+ end
35
+
36
+ def status
37
+ @raw.status
38
+ end
39
+
40
+ def limit?
41
+ @raw.limit?
42
+ end
43
+
44
+ def instruction
45
+ @raw.instruction
46
+ end
47
+
48
+ def price
49
+ if limit?
50
+ convert_quote original_pair.quote.pack @raw.price
51
+ else
52
+ nil
53
+ end
54
+ end
55
+
56
+ # TODO: executed_price
57
+
58
+ def volume
59
+ convert_base original_pair.base.pack @raw.volume
60
+ end
61
+
62
+ def executed_volume
63
+ convert_base original_pair.base.pack @raw.executed_volume
64
+ end
65
+
66
+ def pending_volume
67
+ convert_base(volume - executed_volume)
68
+ end
69
+
70
+ def refresh!
71
+ @raw = backend.fetch_order(session, id)
72
+ self
73
+ end
74
+
75
+ def cancel!
76
+ @raw = backend.cancel_order(session, id)
77
+ self
78
+ end
79
+
80
+ def finished?
81
+ status == CLOSED or status == CANCELED
82
+ end
83
+
84
+ private
85
+
86
+ attr_reader :backend, :session, :forced_pair
87
+
88
+ def convert_base(_price)
89
+ return _price if forced_pair.nil?
90
+ _price.convert_to forced_pair.base
91
+ end
92
+
93
+ def convert_quote(_price)
94
+ return _price if forced_pair.nil?
95
+ _price.convert_to forced_pair.quote
96
+ end
97
+
98
+ end
99
+ end
@@ -0,0 +1,48 @@
1
+ module Trader
2
+ class AccountProxy
3
+
4
+ attr_reader :account, :proxy, :pair
5
+
6
+ def initialize(_account, _proxy, _pair)
7
+ @account = _account
8
+ @proxy = _proxy
9
+ @pair = _pair
10
+ end
11
+
12
+ def backend
13
+ account.backend
14
+ end
15
+
16
+ def market
17
+ account.market_for proxy # TODO: market conversion
18
+ end
19
+
20
+ def base_balance
21
+ balance = account.balance_for proxy.base
22
+ balance.convert_to pair.base
23
+ end
24
+
25
+ def quote_balance
26
+ balance = account.balance_for proxy.quote
27
+ balance.convert_to pair.quote
28
+ end
29
+
30
+ def bid(_volume, _price=nil)
31
+ order = Order.new_bid(pair, _volume, _price).convert_to proxy
32
+ account.create_order(order).convert_to pair
33
+ end
34
+
35
+ def ask(_volume, _price=nil)
36
+ order = Order.new_ask(pair, _volume, _price).convert_to proxy
37
+ account.create_order(order).convert_to pair
38
+ end
39
+
40
+ def find_order(_id)
41
+ account.find_order(_id).convert_to pair
42
+ end
43
+
44
+ def list_orders()
45
+ # TODO
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,57 @@
1
+ module Trader
2
+ class Balance
3
+
4
+ def initialize(_backend, _session, _currency, _forced_currency=nil)
5
+ @backend = _backend
6
+ @session = _session
7
+ @currency = _currency
8
+ @forced_currency = _forced_currency
9
+ @raw = nil
10
+ end
11
+
12
+ def currency
13
+ forced_currency || @currency
14
+ end
15
+
16
+ def original_currency
17
+ @currency
18
+ end
19
+
20
+ def convert_to(_currency)
21
+ return self if _currency == currency
22
+ copy = self.class.new backend, session, original_currency, _currency
23
+ copy.raw = raw unless raw.nil?
24
+ copy
25
+ end
26
+
27
+ def amount
28
+ convert original_currency.pack raw.amount
29
+ end
30
+
31
+ def available_amount
32
+ convert original_currency.pack raw.available_amount
33
+ end
34
+
35
+ def frozen_amount
36
+ convert(amount - available_amount)
37
+ end
38
+
39
+ def refresh!
40
+ self.raw = backend.get_balance(session, currency)
41
+ self
42
+ end
43
+
44
+ protected
45
+
46
+ attr_accessor :raw
47
+
48
+ private
49
+
50
+ attr_reader :backend, :session, :forced_currency
51
+
52
+ def convert(_price)
53
+ return _price if forced_currency.nil?
54
+ _price.convert_to forced_currency
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,19 @@
1
+ module Trader
2
+ class Exchange
3
+ include MarketLoader
4
+
5
+ def initialize(_backend)
6
+ @backend = _backend
7
+ end
8
+
9
+ def login(_crendentials)
10
+ session = backend.get_session(_crendentials)
11
+ Account.new backend, session
12
+ end
13
+
14
+ private
15
+
16
+ attr_reader :backend
17
+
18
+ end
19
+ end
@@ -0,0 +1,29 @@
1
+ module Trader
2
+ class Market < Book
3
+
4
+ attr_reader :backend
5
+
6
+ def initialize(_backend, _pair)
7
+ super _pair
8
+ @backend = _backend
9
+ end
10
+
11
+ def book
12
+ self
13
+ end
14
+
15
+ def base_currency
16
+ pair.base
17
+ end
18
+
19
+ def quote_currency
20
+ pair.quote
21
+ end
22
+
23
+ def refresh!
24
+ backend.fill_book(book)
25
+ self
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ module Trader
2
+ module MarketLoader
3
+ def market_for(_pair, _quote=nil)
4
+ _pair = CurrencyPair.for_code _pair, _quote
5
+ ensure_supported_market _pair
6
+ Market.new(backend, _pair).refresh!
7
+ end
8
+
9
+ def has_market_for?(_pair, _quote=nil)
10
+ _pair = CurrencyPair.for_code _pair, _quote
11
+ available_markets.include? _pair
12
+ end
13
+
14
+ def available_markets
15
+ @markets ||= backend.get_available_markets
16
+ end
17
+
18
+ private
19
+
20
+ def ensure_supported_market(_pair)
21
+ raise ArgumentError, 'market not supported' unless has_market_for? _pair
22
+ end
23
+
24
+ def ensure_supported_currency(_currency)
25
+ raise ArgumentError, 'currency not supported' unless available_markets.any? { |m| m.quote == _currency || m.base == _currency }
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,46 @@
1
+ module Trader
2
+ class BackendFactory
3
+ def self.build(_backend)
4
+ case _backend
5
+ when String, Symbol
6
+ self.new(_backend).build
7
+ else
8
+ _backend
9
+ end
10
+ end
11
+
12
+ attr_reader :name
13
+
14
+ def initialize(_name)
15
+ @name = _name
16
+ end
17
+
18
+ def build
19
+ builder = "build_#{name}"
20
+ raise "Invalid backend #{name}" unless self.class.private_method_defined? builder
21
+ send builder
22
+ end
23
+
24
+ private
25
+
26
+ def build_bitstamp
27
+ require_relative '../adapters/bitstamp_backend'
28
+ BitstampBackend.new
29
+ end
30
+
31
+ def build_surbtc
32
+ require_relative '../adapters/surbtc_backend'
33
+ SurbtcBackend.new
34
+ end
35
+
36
+ def build_itbit
37
+ require_relative '../adapters/itbit_backend'
38
+ ItbitBackend.new
39
+ end
40
+
41
+ def build_fake
42
+ require_relative '../adapters/fake_backend'
43
+ FakeBackend.instance
44
+ end
45
+ end
46
+ end
@@ -1,7 +1,10 @@
1
1
  module Trader
2
2
  class AskSlope < Slope
3
+ private
3
4
 
4
- private
5
+ def instruction
6
+ Order::ASK
7
+ end
5
8
 
6
9
  def compare(_price_a, _price_b)
7
10
  _price_a <=> _price_b
@@ -1,7 +1,10 @@
1
1
  module Trader
2
2
  class BidSlope < Slope
3
+ private
3
4
 
4
- private
5
+ def instruction
6
+ Order::BID
7
+ end
5
8
 
6
9
  def compare(_price_a, _price_b)
7
10
  _price_b <=> _price_a
@@ -0,0 +1,52 @@
1
+ module Trader
2
+ class Book
3
+ attr_reader :pair, :bid_slope, :ask_slope
4
+
5
+ def initialize(_pair)
6
+ @pair = _pair
7
+ @bid_slope = BidSlope.new _pair
8
+ @ask_slope = AskSlope.new _pair
9
+ end
10
+
11
+ def prepare(_ref_time=nil)
12
+ @ref_time = _ref_time || Time.now
13
+ @transactions = []
14
+ @bid_slope.reset
15
+ @ask_slope.reset
16
+ end
17
+
18
+ def add_transaction(_price, _volume, _time)
19
+ @transactions << Transaction.new(
20
+ Price.new(pair.quote, _price),
21
+ Price.new(pair.base, _volume),
22
+ _time
23
+ )
24
+ nil
25
+ end
26
+
27
+ def add_bid(_price, _volume)
28
+ bid_slope.add_order _price, _volume
29
+ end
30
+
31
+ def add_ask(_price, _volume)
32
+ ask_slope.add_order _price, _volume
33
+ end
34
+
35
+ def volume(_period_seconds)
36
+ ref_time = @ref_time - _period_seconds
37
+ amount = @transactions.inject(0.0) do |r, trx|
38
+ if trx.time >= ref_time
39
+ r + trx.volume.amount
40
+ else
41
+ r
42
+ end
43
+ end
44
+
45
+ Price.new pair.base, amount
46
+ end
47
+
48
+ private
49
+
50
+ attr_reader :transactions
51
+ end
52
+ end
@@ -1,8 +1,7 @@
1
1
  module Trader
2
2
  class Converter
3
-
4
- def convert(_value, _from, _to)
5
- raise NotImplementedError, 'convert method not implemented'
3
+ def rate
4
+ raise NotImplementedError, 'rate method not implemented'
6
5
  end
7
6
  end
8
7
  end