bitex 0.3 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +63 -0
  3. data/.rubocop.yml +32 -0
  4. data/.ruby-version +1 -0
  5. data/bitex.gemspec +21 -18
  6. data/lib/bitex.rb +7 -1
  7. data/lib/bitex/api.rb +34 -41
  8. data/lib/bitex/ask.rb +74 -0
  9. data/lib/bitex/base_order.rb +106 -0
  10. data/lib/bitex/bid.rb +72 -0
  11. data/lib/bitex/buy.rb +8 -5
  12. data/lib/bitex/kyc_file.rb +31 -9
  13. data/lib/bitex/kyc_profile.rb +113 -38
  14. data/lib/bitex/{market.rb → market_data.rb} +3 -3
  15. data/lib/bitex/match.rb +30 -15
  16. data/lib/bitex/order.rb +6 -238
  17. data/lib/bitex/payment.rb +30 -18
  18. data/lib/bitex/rates.rb +6 -8
  19. data/lib/bitex/sell.rb +5 -5
  20. data/lib/bitex/specie_deposit.rb +9 -4
  21. data/lib/bitex/specie_withdrawal.rb +29 -28
  22. data/lib/bitex/trade.rb +4 -5
  23. data/lib/bitex/transaction.rb +7 -8
  24. data/lib/bitex/usd_deposit.rb +46 -47
  25. data/lib/bitex/usd_withdrawal.rb +33 -34
  26. data/lib/bitex/version.rb +1 -1
  27. data/spec/ask_spec.rb +17 -5
  28. data/spec/bid_spec.rb +17 -5
  29. data/spec/buy_spec.rb +14 -4
  30. data/spec/kyc_file_spec.rb +34 -18
  31. data/spec/kyc_profile_spec.rb +158 -122
  32. data/spec/order_spec.rb +1 -1
  33. data/spec/payment_spec.rb +51 -45
  34. data/spec/sell_spec.rb +14 -4
  35. data/spec/spec_helper.rb +7 -6
  36. data/spec/specie_deposit_spec.rb +10 -4
  37. data/spec/specie_withdrawal_spec.rb +26 -25
  38. data/spec/support/from_json_shared_examples.rb +20 -22
  39. data/spec/support/order_shared_examples.rb +14 -17
  40. data/spec/support/request_stubs.rb +18 -12
  41. data/spec/trade_spec.rb +5 -5
  42. data/spec/transaction_spec.rb +12 -13
  43. data/spec/usd_deposit_spec.rb +120 -105
  44. data/spec/usd_withdrawal_spec.rb +89 -79
  45. metadata +57 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5585d9b3c87459de35ecbe8c0f6f215597be0976
4
- data.tar.gz: 277eaa74fe7d87546334a7f6fc40c6a9e42df3f0
3
+ metadata.gz: 151ad0a112110c7ce40e5ce09c5f753a10253bcb
4
+ data.tar.gz: aecf7f5c80fe3afba0da32ddb4f820f8d4fb7bc4
5
5
  SHA512:
6
- metadata.gz: afcc613f373bba455e4014491d5649f06b748a64eb055d0b3fe0f53534295e42a4d5a82bb2d873891b0837870aa7ad8ffe4c3cded6a123b822be7a3b5c72e80d
7
- data.tar.gz: 0b07c253e3848e571514e75487dc520c5007d7994c1b7cbba146e56e23047a889e5cef753e385d0021975d81492b7e5d74690fb683aee284cc799a99b4b9c8aa
6
+ metadata.gz: d9785a0c2b14f157e6cdef69079c12c4be59c9463bdff76bf865aa1931f98d46eff6d6b376c1392cf2bd0452870296c58e8e76475b0994ca3e0ce8dfaf6b1b26
7
+ data.tar.gz: cb426c0e8d50ddd8982e8505d3696374ab637b0822693cfc98d53145dacd02de0fc220389db4195f38d2b0a9f9327b5072148d4505a2f65a2695f30d0f2f4193
@@ -0,0 +1,63 @@
1
+ # Ruby CircleCI 2.0 configuration file
2
+ #
3
+ # Check https://circleci.com/docs/2.0/language-ruby/ for more details
4
+ #
5
+ version: 2
6
+ jobs:
7
+ build:
8
+ docker:
9
+ # specify the version you desire here
10
+ - image: circleci/ruby:2.4.1-node-browsers
11
+
12
+ # Specify service dependencies here if necessary
13
+ # CircleCI maintains a library of pre-built images
14
+ # documented at https://circleci.com/docs/2.0/circleci-images/
15
+ # - image: circleci/postgres:9.4
16
+
17
+ working_directory: ~/repo
18
+
19
+ steps:
20
+ - checkout
21
+
22
+ # Download and cache dependencies
23
+ - restore_cache:
24
+ keys:
25
+ - v1-dependencies-{{ checksum "bitex.gemspec" }}-{{ checksum "Gemfile" }}
26
+ # fallback to using the latest cache if no exact match is found
27
+ - v1-dependencies-
28
+
29
+ - run:
30
+ name: install dependencies
31
+ command: |
32
+ bundle install --jobs=4 --retry=3 --path vendor/bundle
33
+
34
+ - save_cache:
35
+ paths:
36
+ - ./vendor/bundle
37
+ key: v1-dependencies-{{ checksum "bitex.gemspec" }}-{{ checksum "Gemfile" }}
38
+
39
+ # run tests!
40
+ - run:
41
+ name: run tests
42
+ command: |
43
+ mkdir /tmp/test-results
44
+ TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)"
45
+
46
+ bundle exec rspec --format progress \
47
+ --format RspecJunitFormatter \
48
+ --out /tmp/test-results/rspec.xml \
49
+ --format progress \
50
+ $TEST_FILES
51
+
52
+ # run lints
53
+ - run:
54
+ name: run lints
55
+ command: |
56
+ bundle exec rubocop
57
+
58
+ # collect reports
59
+ - store_test_results:
60
+ path: /tmp/test-results
61
+ - store_artifacts:
62
+ path: /tmp/test-results
63
+ destination: test-results
@@ -0,0 +1,32 @@
1
+ Style/FrozenStringLiteralComment:
2
+ EnforcedStyle: never
3
+
4
+ Metrics/AbcSize:
5
+ Max: 16
6
+
7
+ Metrics/ClassLength:
8
+ Max: 130
9
+
10
+ Metrics/LineLength:
11
+ Max: 130
12
+
13
+ Metrics/MethodLength:
14
+ Max: 20
15
+
16
+ Metrics/MethodLength:
17
+ Max: 20
18
+
19
+ Metrics/ParameterLists:
20
+ Max: 7
21
+
22
+ AllCops:
23
+ TargetRubyVersion: 2.4
24
+ Exclude:
25
+ - 'Gemfile'
26
+ - 'Gemfile.lock'
27
+ - 'LICENSE.txt'
28
+ - 'README.md'
29
+ - 'Rakefile'
30
+ - 'bitex.gemspec'
31
+ - 'spec/**/*'
32
+ - 'vendor/**/*'
@@ -0,0 +1 @@
1
+ 2.4.1
@@ -4,30 +4,33 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
  require 'bitex/version'
5
5
 
6
6
  Gem::Specification.new do |spec|
7
- spec.name = "bitex"
7
+ spec.name = 'bitex'
8
8
  spec.version = Bitex::VERSION
9
- spec.authors = ["Nubis", "Eromirou"]
10
- spec.email = ["nb@bitex.la", "tr@bitex.la"]
11
- spec.description = %q{API client library for bitex.la. Fetch public market
12
- data and build trading robots}
13
- spec.summary = "API client library for bitex.la"
14
- spec.homepage = "http://bitex.la/developers"
15
- spec.license = "MIT"
9
+ spec.authors = ['Nubis', 'Eromirou']
10
+ spec.email = ['nb@bitex.la', 'tr@bitex.la']
11
+ spec.description = %q{API client library for bitex.la. Fetch public market data and build trading robots}
12
+ spec.summary = 'API client library for bitex.la'
13
+ spec.homepage = 'http://bitex.la/developers'
14
+ spec.license = 'MIT'
16
15
 
17
16
  spec.files = `git ls-files`.split($/)
18
17
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
- spec.require_paths = ["lib"]
19
+ spec.require_paths = ['lib']
21
20
 
22
- spec.add_dependency "activesupport"
23
- spec.add_dependency "curb", "~> 0.9.3"
21
+ spec.add_dependency 'activesupport'
22
+ spec.add_dependency 'curb', '~> 0.9.3'
24
23
 
25
24
  spec.required_ruby_version = '>= 1.9.3'
26
- spec.add_development_dependency "bundler", "~> 1.3"
27
- spec.add_development_dependency "rake"
28
- spec.add_development_dependency "rspec"
29
- spec.add_development_dependency "rspec-mocks"
30
- spec.add_development_dependency "timecop"
31
- spec.add_development_dependency "webmock"
32
- spec.add_development_dependency "shoulda-matchers"
25
+ spec.add_development_dependency 'bundler', '~> 1.3'
26
+ spec.add_development_dependency 'rake'
27
+
28
+ spec.add_development_dependency 'byebug'
29
+ spec.add_development_dependency 'rspec'
30
+ spec.add_development_dependency 'rspec_junit_formatter'
31
+ spec.add_development_dependency 'rspec-mocks'
32
+ spec.add_development_dependency 'rubocop'
33
+ spec.add_development_dependency 'shoulda-matchers'
34
+ spec.add_development_dependency 'timecop'
35
+ spec.add_development_dependency 'webmock'
33
36
  end
@@ -3,9 +3,15 @@ require 'active_support/core_ext'
3
3
  require 'json'
4
4
  require 'curl'
5
5
  require 'bigdecimal'
6
+ require 'bigdecimal/util'
6
7
  require 'bitex/match'
7
- Dir[File.expand_path("../bitex/*.rb", __FILE__)].each {|f| require f}
8
+ require 'bitex/base_order'
8
9
 
10
+ Dir[File.expand_path('bitex/*.rb', __dir__)].each { |f| require f }
11
+
12
+ ##
13
+ # Documentation here!
14
+ #
9
15
  module Bitex
10
16
  mattr_accessor :api_key
11
17
  mattr_accessor :sandbox
@@ -1,5 +1,10 @@
1
1
  module Bitex
2
- class ApiError < StandardError; end
2
+ class ApiError < StandardError
3
+ end
4
+
5
+ ##
6
+ # Documentation here!
7
+ #
3
8
  class Api
4
9
  def self.grab_curl
5
10
  if @curl
@@ -9,34 +14,27 @@ module Bitex
9
14
  end
10
15
 
11
16
  @curl.ssl_version = Curl::CURL_SSLVERSION_TLSv1
12
- if Bitex.debug
13
- @curl.on_debug do |t,d|
14
- if d.to_s.size < 300
15
- puts "DEBUG SSL #{t}, #{d}"
16
- end
17
- end
18
- end
19
-
17
+ @curl.on_debug { |t, d| puts "DEBUG SSL #{t}, #{d}" if d.to_s.size < 300 } if Bitex.debug
20
18
  @curl.connect_timeout = 30
21
19
  @curl.timeout = 30
22
20
  @curl
23
21
  end
24
22
 
25
- def self.curl(verb, path, options={}, files={})
23
+ def self.curl(verb, path, options = {}, files = {})
26
24
  verb = verb.upcase.to_sym
27
25
  query = verb == :GET ? "?#{options.to_query}" : ''
28
26
  prefix = Bitex.sandbox ? 'sandbox.' : ''
29
27
 
30
28
  curl = grab_curl
31
29
  curl.url = "https://#{prefix}bitex.la/api-v1/rest#{path}#{query}"
32
-
30
+
33
31
  if verb == :POST
34
32
  fields = []
35
33
  unless files.empty?
36
- fields += files.collect{|k, v| Curl::PostField.file(k.to_s, v) }
34
+ fields += files.map { |k, v| Curl::PostField.file(k.to_s, v) }
37
35
  curl.multipart_form_post = true
38
36
  end
39
- fields += options.collect do |k,v|
37
+ fields += options.map do |k, v|
40
38
  next unless v
41
39
  Curl::PostField.content(k.to_s, v)
42
40
  end.compact
@@ -48,50 +46,45 @@ module Bitex
48
46
  code = curl.response_code
49
47
 
50
48
  unless [200, 201, 202].include?(code)
51
- raise ApiError.new("Got #{code} fetching #{path} with
52
- #{options}\n\n#{curl.head}\n\n#{curl.body}")
49
+ raise ApiError, "Got #{code} fetching #{path} with #{options}\n\n#{curl.head}\n\n#{curl.body}"
53
50
  end
54
51
 
55
- return curl
52
+ curl
56
53
  end
57
-
58
- def self.public(path, options={})
59
- c = curl(:GET, path)
60
- JSON.parse(c.body)
54
+
55
+ def self.public(path, _options = {})
56
+ response = curl(:GET, path)
57
+ JSON.parse(response.body)
61
58
  end
62
-
63
- def self.private(verb, path, options={}, files={})
64
- if Bitex.api_key.nil?
65
- raise StandardError.new("No api_key available to make private key calls")
66
- end
67
- c = curl(verb, path, options.merge(api_key: Bitex.api_key), files)
68
- JSON.parse(c.body)
59
+
60
+ def self.private(verb, path, options = {}, files = {})
61
+ raise StandardError, 'No api_key available to make private key calls' if Bitex.api_key.nil?
62
+ response = curl(verb, path, options.merge(api_key: Bitex.api_key), files)
63
+ JSON.parse(response.body)
69
64
  end
70
-
71
- # Deserialize a single object from a json representation as specified on the
72
- # bitex API class reference
65
+
66
+ # Deserialize a single object from a json representation as specified on the bitex API class reference
73
67
  # @see https://bitex.la/developers#api-class-reference
74
68
  def self.deserialize(object)
75
- { 1 => Bid,
69
+ {
70
+ 1 => Bid,
76
71
  2 => Ask,
77
72
  3 => Buy,
78
73
  4 => Sell,
79
74
  5 => SpecieDeposit,
80
75
  6 => SpecieWithdrawal,
81
76
  7 => UsdDeposit,
82
- 8 => UsdWithdrawal,
83
- }[object.first].from_json(object)
77
+ 8 => UsdWithdrawal
78
+ }[object[0]].from_json(object)
84
79
  end
85
-
80
+
86
81
  # @visibility private
87
- def self.from_json(thing, json, with_specie=false, &block)
88
- thing.id = json[1]
89
- thing.created_at = Time.at(json[2])
90
- if with_specie
91
- thing.specie = {1 => :btc, 2 => :ltc}[json[3]]
82
+ def self.from_json(thing, json, &block)
83
+ thing.tap do |t|
84
+ t.id = json[1]
85
+ t.created_at = Time.at(json[2])
86
+ block.call(t)
92
87
  end
93
- block.call(thing)
94
- return thing
95
88
  end
96
89
  end
97
90
  end
@@ -0,0 +1,74 @@
1
+ module Bitex
2
+ # An Ask is an order to sell a given orderbook.
3
+ # @see BaseOrder
4
+ class Ask < BaseOrder
5
+ # @!attribute id
6
+ # @return [Integer] This Ask's unique ID.
7
+
8
+ # @!attribute created_at
9
+ # @return [Time] Time when this Ask was created.
10
+
11
+ # @!attribute orderbook
12
+ # @return [Symbol] :btc_usd or :btc_ars
13
+
14
+ # @!attribute quantity
15
+ # TODO: rever esta documentacion
16
+ # @return [BigDecimal] Quantity of specie to sell in this Ask.
17
+ attr_accessor :quantity
18
+
19
+ # @!attribute remaining_quantity
20
+ # TODO: rever esta documentacion
21
+ # @return [BigDecimal] Quantity of specie left to sell in this Ask.
22
+ attr_accessor :remaining_quantity
23
+
24
+ # @!attribute price
25
+ # @return [BigDecimal] Minimum price to charge per unit.
26
+
27
+ # @!attribute status
28
+ # The status of this Ask in its lifecycle.
29
+ # * :received queued to check if you have enough funds.
30
+ # * :executing available in our ourderbook waiting to be matched.
31
+ # * :cancelling To be cancelled as soon as our trading engine unlocks it.
32
+ # * :cancelled no further executed. May have some Remaining Quantity.
33
+ # * :completed Fully executed, Remaining Quantity should be 0.
34
+
35
+ # @!attribute reason
36
+ # The cancellation reason of this Ask.
37
+ # * :not_cancelled Has not been cancelled.
38
+ # * :not_enough_funds Not enough funds to place this order.
39
+ # * :user_cancelled Cancelled per user's request.
40
+ # * :system_cancelled Bitex cancelled this order, for a good reason.
41
+
42
+ # @!attribute produced_amount
43
+ # @return [BigDecimal] Amount of USD produced from this sale
44
+ attr_accessor :produced_amount
45
+
46
+ # @!attribute issuer
47
+ # @return [String] The issuer of this order, helps you tell apart orders created from the web UI and the API.
48
+
49
+ # @visibility private
50
+ def self.base_path
51
+ '/asks'
52
+ end
53
+
54
+ # TODO: rever esta documentacion
55
+ # Create a new Ask for selling a Quantity of specie charging no less than Price per each.
56
+ # @param orderbook [Symbol] :btc_usd or :btc_ars, whatever you're selling.
57
+ # @param quantity [BigDecimal] Quantity to sell.
58
+ # @param price [BigDecimal] Minimum price to charge when selling.
59
+ # @param wait [Boolean] Block the process and wait until this ask moves out of the :received state, defaults to false.
60
+ # @see https://bitex.la/developers#create-ask
61
+ def self.create!(orderbook, quantity, price, wait = false)
62
+ super
63
+ end
64
+
65
+ # @visibility private
66
+ def self.from_json(json, order = nil)
67
+ super(json, order).tap do |thing|
68
+ thing.quantity = (json[4].presence || 0).to_d
69
+ thing.remaining_quantity = (json[5].presence || 0).to_d
70
+ thing.produced_amount = (json[9].presence || 0).to_d
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,106 @@
1
+ module Bitex
2
+ # Base class for Bids and Asks
3
+ class BaseOrder
4
+ # @!attribute id
5
+ # @return [Integer] This Bid's unique ID.
6
+ attr_accessor :id
7
+
8
+ # @!attribute created_at
9
+ # @return [Time] Time when this Bid was created.
10
+ attr_accessor :created_at
11
+
12
+ # @!attribute orderbook
13
+ # @return [Symbol] :btc_usd or :btc_ars
14
+ attr_accessor :orderbook
15
+
16
+ # @!attribute price
17
+ # @return [BigDecimal] Maximum price to pay per unit.
18
+ attr_accessor :price
19
+
20
+ # @!attribute status
21
+ # The status in its lifecycle, was defined into Ask/Bid subclass.
22
+ attr_accessor :status
23
+
24
+ # @!attribute reason
25
+ # The cancellation reason for your Ask/Bid subclass, if any.
26
+ # * :not_cancelled Has not been cancelled.
27
+ # * :not_enough_funds Not enough funds to place this order.
28
+ # * :user_cancelled Cancelled per user's request
29
+ # * :system_cancelled Bitex cancelled this order, for a good reason.
30
+ attr_accessor :reason
31
+
32
+ # @!attribute issuer
33
+ # @return [String] The issuer of this order, helps you tell apart orders created from the web UI and the API.
34
+ attr_accessor :issuer
35
+
36
+ # Returns an array with all your active orders of this type and another order of this type that was active the last 2 hours.
37
+ # Uses {Order.all} under the hood.
38
+ def self.all
39
+ Order.all.select { |o| o.is_a?(self) }
40
+ end
41
+
42
+ # Returns an array with all your active orders of this type.
43
+ # Uses {Order.active} under the hood.
44
+ def self.active
45
+ Order.active.select { |o| o.is_a?(self) }
46
+ end
47
+
48
+ # Find an order in your list of active orders.
49
+ # Uses {Order.active} under the hood.
50
+ def self.find(id)
51
+ from_json(Api.private(:get, "/private#{base_path}/#{id}"))
52
+ end
53
+
54
+ # @visibility private
55
+ def self.create!(orderbook, amount, price, wait = false)
56
+ params = { amount: amount, price: price, orderbook: { btc_usd: 1, btc_ars: 5 }[orderbook] }
57
+
58
+ order = from_json(Api.private(:post, "/private#{base_path}", params))
59
+ retries = 0
60
+ while wait && order.status == :received
61
+ sleep(0.2)
62
+ order = find_order(order)
63
+ retries += 1
64
+ # Wait 15 minutes for the order to be accepted.
65
+ raise StandardError, "Timed out waiting for #{base_path} ##{order.id}" if retries > 5000
66
+ end
67
+ order
68
+ end
69
+
70
+ def cancel!
71
+ path = "/private#{self.class.base_path}/#{id}/cancel"
72
+ self.class.from_json(Api.private(:post, path), self)
73
+ end
74
+
75
+ # @visibility private
76
+ def self.from_json(json, order = nil)
77
+ Api.from_json(order || new, json) do |thing|
78
+ thing.orderbook = orderbooks[json[3]]
79
+ thing.price = json[6].to_s.to_d
80
+ thing.status = statuses[json[7]]
81
+ thing.reason = reasons[json[8]]
82
+ thing.issuer = json[10]
83
+ end
84
+ end
85
+
86
+ private_class_method
87
+
88
+ def self.find_order(order)
89
+ find(order.id)
90
+ rescue StandardError
91
+ order
92
+ end
93
+
94
+ def self.orderbooks
95
+ { 1 => :btc_usd, 5 => :btc_ars }
96
+ end
97
+
98
+ def self.reasons
99
+ { 0 => :not_cancelled, 1 => :not_enough_funds, 2 => :user_cancelled, 3 => :system_cancelled }
100
+ end
101
+
102
+ def self.statuses
103
+ { 1 => :received, 2 => :executing, 3 => :cancelling, 4 => :cancelled, 5 => :completed }
104
+ end
105
+ end
106
+ end