bitex_bot 0.3.7 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +63 -0
  3. data/.rubocop.yml +33 -0
  4. data/Gemfile +1 -1
  5. data/Rakefile +1 -1
  6. data/bin/bitex_bot +1 -1
  7. data/bitex_bot.gemspec +34 -34
  8. data/lib/bitex_bot/database.rb +67 -67
  9. data/lib/bitex_bot/models/api_wrappers/api_wrapper.rb +142 -0
  10. data/lib/bitex_bot/models/api_wrappers/bitstamp/bitstamp_api_wrapper.rb +137 -0
  11. data/lib/bitex_bot/models/api_wrappers/itbit/itbit_api_wrapper.rb +116 -0
  12. data/lib/bitex_bot/models/api_wrappers/kraken/kraken_api_wrapper.rb +111 -0
  13. data/lib/bitex_bot/models/api_wrappers/kraken/kraken_order.rb +117 -0
  14. data/lib/bitex_bot/models/buy_closing_flow.rb +23 -16
  15. data/lib/bitex_bot/models/buy_opening_flow.rb +48 -54
  16. data/lib/bitex_bot/models/close_buy.rb +2 -2
  17. data/lib/bitex_bot/models/closing_flow.rb +98 -79
  18. data/lib/bitex_bot/models/open_buy.rb +11 -10
  19. data/lib/bitex_bot/models/open_sell.rb +11 -10
  20. data/lib/bitex_bot/models/opening_flow.rb +157 -99
  21. data/lib/bitex_bot/models/order_book_simulator.rb +62 -67
  22. data/lib/bitex_bot/models/sell_closing_flow.rb +25 -20
  23. data/lib/bitex_bot/models/sell_opening_flow.rb +47 -54
  24. data/lib/bitex_bot/models/store.rb +3 -1
  25. data/lib/bitex_bot/robot.rb +203 -176
  26. data/lib/bitex_bot/settings.rb +71 -12
  27. data/lib/bitex_bot/version.rb +1 -1
  28. data/lib/bitex_bot.rb +40 -16
  29. data/settings.rb.sample +43 -66
  30. data/spec/bitex_bot/settings_spec.rb +87 -15
  31. data/spec/factories/bitex_buy.rb +3 -3
  32. data/spec/factories/bitex_sell.rb +3 -3
  33. data/spec/factories/buy_opening_flow.rb +1 -1
  34. data/spec/factories/open_buy.rb +12 -10
  35. data/spec/factories/open_sell.rb +12 -10
  36. data/spec/factories/sell_opening_flow.rb +1 -1
  37. data/spec/models/api_wrappers/bitstamp_api_wrapper_spec.rb +200 -0
  38. data/spec/models/api_wrappers/itbit_api_wrapper_spec.rb +176 -0
  39. data/spec/models/api_wrappers/kraken_api_wrapper_spec.rb +209 -0
  40. data/spec/models/bitex_api_spec.rb +1 -1
  41. data/spec/models/buy_closing_flow_spec.rb +140 -71
  42. data/spec/models/buy_opening_flow_spec.rb +126 -56
  43. data/spec/models/order_book_simulator_spec.rb +10 -10
  44. data/spec/models/robot_spec.rb +61 -47
  45. data/spec/models/sell_closing_flow_spec.rb +130 -62
  46. data/spec/models/sell_opening_flow_spec.rb +129 -60
  47. data/spec/spec_helper.rb +19 -16
  48. data/spec/support/bitex_stubs.rb +13 -14
  49. data/spec/support/bitstamp/bitstamp_api_wrapper_stubs.rb +35 -0
  50. data/spec/support/bitstamp/bitstamp_stubs.rb +91 -0
  51. metadata +60 -42
  52. data/lib/bitex_bot/models/bitfinex_api_wrapper.rb +0 -118
  53. data/lib/bitex_bot/models/bitstamp_api_wrapper.rb +0 -82
  54. data/lib/bitex_bot/models/itbit_api_wrapper.rb +0 -68
  55. data/lib/bitex_bot/models/kraken_api_wrapper.rb +0 -188
  56. data/spec/models/bitfinex_api_wrapper_spec.rb +0 -17
  57. data/spec/models/bitstamp_api_wrapper_spec.rb +0 -15
  58. data/spec/models/itbit_api_wrapper_spec.rb +0 -15
  59. data/spec/support/bitstamp_stubs.rb +0 -110
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c1bcc53a7b4d8a6382173bdd864dd306b3a2bdc5
4
- data.tar.gz: e4254d0ffe025a95c5eba3429814b2b60405abb7
3
+ metadata.gz: '027380d0b5310a8656d1e4ea0357617f1c750c1a'
4
+ data.tar.gz: 66974805bb9b7b130181e737f688512d8f234d17
5
5
  SHA512:
6
- metadata.gz: 0b64dae4070f6fb5ce595bfb0d5cac5c88d9d853264b948ae62241e812a1946ed83e9f3af344b4fa8fb7a215d61f9cc8c2b960c388b820d2b880f3dc72cc32b7
7
- data.tar.gz: a8ac2e4ace3edaa00b529d6aafd1808d4bb8cfb8139b6588789a80e8455b38505be31afb6a813048905d9f8c32f3738b979c23c45a1ab1340a49454180347ecc
6
+ metadata.gz: d70e171eb1bae487ab0866d441c7c7ac3711d129976a4bf0351d1a1848834dbb9b4f2cb9227af7d2f73df7a3aa200052dc48028b8a8b0c0fa753d775ba21070a
7
+ data.tar.gz: 4bc5ae223b7ddde9b07b8397ce2c5fcbc25b89b289084b57bc357448c8d6c822e301a0814f657ccf89a9d12a57f6d56d5cabdd1d2d88f7393a36b00e18c1480b
@@ -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_bot.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_bot.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
data/.rubocop.yml ADDED
@@ -0,0 +1,33 @@
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: 25
15
+
16
+ Metrics/ParameterLists:
17
+ Max: 7
18
+
19
+ AllCops:
20
+ TargetRubyVersion: 2.4
21
+ Exclude:
22
+ - 'Gemfile'
23
+ - 'Gemfile.lock'
24
+ - 'LICENSE.txt'
25
+ - 'README.md'
26
+ - 'Rakefile'
27
+ - 'bin/*'
28
+ - 'bitex_bot.gemspec'
29
+ - 'settings.rb.sample'
30
+ - 'spec/**/*'
31
+ - 'vendor/**/*'
32
+ - 'lib/bitex_bot/database.rb'
33
+ - 'lib/bitex_bot/version.rb'
data/Gemfile CHANGED
@@ -2,4 +2,4 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in bitex_bot.gemspec
4
4
  gemspec
5
- gem 'bitstamp', git: 'https://github.com/bitex-la/bitstamp.git'
5
+ gem 'bitstamp', github: 'bitex-la/bitstamp'
data/Rakefile CHANGED
@@ -1 +1 @@
1
- require "bundler/gem_tasks"
1
+ require 'bundler/gem_tasks'
data/bin/bitex_bot CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require "bitex_bot/settings"
3
+ require 'bitex_bot/settings'
4
4
  BitexBot::Settings.load_default
5
5
 
6
6
  require 'bitex_bot'
data/bitex_bot.gemspec CHANGED
@@ -1,44 +1,44 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ lib = File.expand_path('lib', __dir__)
3
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
3
  require 'bitex_bot/version'
5
4
 
6
5
  Gem::Specification.new do |spec|
7
- spec.name = "bitex_bot"
6
+ spec.name = 'bitex_bot'
8
7
  spec.version = BitexBot::VERSION
9
- spec.authors = ["Nubis", "Eromirou"]
10
- spec.email = ["nb@bitex.la", "tr@bitex.la"]
11
- spec.description = %q{Both a trading robot and a library to build trading
12
- robots. The bitex-bot lets you buy cheap on bitex and
13
- sell on another exchange and vice versa.}
14
- spec.summary = %q{A trading robot to do arbitrage between bitex.la and
15
- other exchanges!}
16
- spec.homepage = ""
17
- spec.license = "MIT"
8
+ spec.authors = %w[Nubis Eromirou]
9
+ spec.email = %w[nb@bitex.la tr@bitex.la]
10
+ spec.description = %(Both a trading robot and a library to build trading robots. The bitex-bot lets you buy cheap on bitex
11
+ and sell on another exchange and vice versa.)
12
+ spec.summary = %(A trading robot to do arbitrage between bitex.la and other exchanges!)
13
+ spec.homepage = ''
14
+ spec.license = 'MIT'
18
15
 
19
- spec.files = `git ls-files`.split($/)
16
+ spec.files = `git ls-files`.split($RS)
20
17
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
21
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
22
- spec.require_paths = ["lib"]
23
-
24
- spec.add_dependency "activerecord", "~> 4.2"
25
- spec.add_dependency "sqlite3"
26
- spec.add_dependency "bitstamp"
27
- spec.add_dependency "bitex", "0.3"
28
- spec.add_dependency "itbit", "0.0.6"
29
- spec.add_dependency "bitfinex-rb", "0.0.6"
30
- spec.add_dependency "kraken_client", "~> 1.2.1"
31
- spec.add_dependency "mail"
32
- spec.add_dependency "hashie", "~> 3.5.4"
19
+ spec.require_paths = %w[lib]
33
20
 
34
- spec.add_development_dependency "bundler"
35
- spec.add_development_dependency "byebug"
36
- spec.add_development_dependency "rake"
37
- spec.add_development_dependency "rspec"
38
- spec.add_development_dependency "rspec-mocks"
39
- spec.add_development_dependency "database_cleaner"
40
- spec.add_development_dependency "factory_girl"
41
- spec.add_development_dependency "timecop"
42
- spec.add_development_dependency "shoulda-matchers"
43
- spec.add_development_dependency "webmock"
21
+ spec.add_dependency 'activerecord', '~> 4.2'
22
+ spec.add_dependency 'hashie', '~> 3.5.4'
23
+ spec.add_dependency 'mail'
24
+ spec.add_dependency 'sqlite3'
25
+
26
+ spec.add_dependency 'bitex', '0.5'
27
+ spec.add_dependency 'bitstamp'
28
+ spec.add_dependency 'itbit', '0.0.6'
29
+ spec.add_dependency 'kraken_client', '~> 1.2.1'
30
+
31
+ spec.add_development_dependency 'bundler'
32
+ spec.add_development_dependency 'rake'
33
+
34
+ spec.add_development_dependency 'byebug'
35
+ spec.add_development_dependency 'database_cleaner'
36
+ spec.add_development_dependency 'factory_bot'
37
+ spec.add_development_dependency 'rspec'
38
+ spec.add_development_dependency 'rspec-mocks'
39
+ spec.add_development_dependency 'rspec_junit_formatter'
40
+ spec.add_development_dependency 'rubocop'
41
+ spec.add_development_dependency 'shoulda-matchers'
42
+ spec.add_development_dependency 'timecop'
43
+ spec.add_development_dependency 'webmock'
44
44
  end
@@ -1,113 +1,113 @@
1
1
  module BitexBot
2
2
  module Database
3
- #ActiveRecord::Base.logger = Logger.new(File.open('database.log', 'w'))
4
3
  ActiveRecord::Base.establish_connection(Settings.database)
5
4
 
6
5
  ActiveRecord::Schema.define(version: 1) do
7
6
  if ActiveRecord::Base.connection.tables.empty?
8
7
  create_table :buy_opening_flows do |t|
9
- t.decimal :price, precision: 30, scale: 15
10
- t.decimal :value_to_use, precision: 30, scale: 15
11
- t.decimal :suggested_closing_price, precision: 30, scale: 15
12
- t.integer :order_id
13
- t.string :status, null: false, default: 'executing'
14
- t.index :status
15
- t.timestamps
8
+ t.decimal :price, precision: 30, scale: 15
9
+ t.decimal :value_to_use, precision: 30, scale: 15
10
+ t.decimal :suggested_closing_price, precision: 30, scale: 15
11
+ t.integer :order_id
12
+ t.string :status, null: false, default: 'executing'
13
+ t.index :status
14
+ t.timestamps null: true
16
15
  end
17
16
  add_index :buy_opening_flows, :order_id
18
17
 
19
18
  create_table :sell_opening_flows do |t|
20
- t.decimal :price, precision: 30, scale: 15
21
- t.decimal :value_to_use, precision: 30, scale: 15
22
- t.decimal :suggested_closing_price, precision: 30, scale: 15
23
- t.integer :order_id
24
- t.string :status, null: false, default: 'executing'
25
- t.index :status
26
- t.timestamps
19
+ t.decimal :price, precision: 30, scale: 15
20
+ t.decimal :value_to_use, precision: 30, scale: 15
21
+ t.decimal :suggested_closing_price, precision: 30, scale: 15
22
+ t.integer :order_id
23
+ t.string :status, null: false, default: 'executing'
24
+ t.index :status
25
+ t.timestamps null: true
27
26
  end
28
27
  add_index :sell_opening_flows, :order_id
29
-
28
+
30
29
  create_table :open_buys do |t|
31
30
  t.belongs_to :opening_flow
32
31
  t.belongs_to :closing_flow
33
- t.decimal :price, precision: 30, scale: 15
34
- t.decimal :amount, precision: 30, scale: 15
35
- t.decimal :quantity, precision: 30, scale: 15
36
- t.integer :transaction_id
37
- t.timestamps
32
+ t.decimal :price, precision: 30, scale: 15
33
+ t.decimal :amount, precision: 30, scale: 15
34
+ t.decimal :quantity, precision: 30, scale: 15
35
+ t.integer :transaction_id
36
+ t.timestamps null: true
38
37
  end
39
38
  add_index :open_buys, :transaction_id
40
39
 
41
40
  create_table :open_sells do |t|
42
41
  t.belongs_to :opening_flow
43
42
  t.belongs_to :closing_flow
44
- t.decimal :price, precision: 30, scale: 15
45
- t.decimal :quantity, precision: 30, scale: 15
46
- t.decimal :amount, precision: 30, scale: 15
47
- t.integer :transaction_id
48
- t.timestamps
43
+ t.decimal :price, precision: 30, scale: 15
44
+ t.decimal :quantity, precision: 30, scale: 15
45
+ t.decimal :amount, precision: 30, scale: 15
46
+ t.integer :transaction_id
47
+ t.timestamps null: true
49
48
  end
50
49
  add_index :open_sells, :transaction_id
51
-
50
+
52
51
  create_table :buy_closing_flows do |t|
53
- t.decimal :desired_price, precision: 30, scale: 15
54
- t.decimal :quantity, precision: 30, scale: 15
55
- t.decimal :amount, precision: 30, scale: 15
56
- t.boolean :done, null: false, default: false
57
- t.decimal :btc_profit, precision: 30, scale: 15
58
- t.decimal :usd_profit, precision: 30, scale: 15
59
- t.timestamps
52
+ t.decimal :desired_price, precision: 30, scale: 15
53
+ t.decimal :quantity, precision: 30, scale: 15
54
+ t.decimal :amount, precision: 30, scale: 15
55
+ t.boolean :done, null: false, default: false
56
+ t.decimal :btc_profit, precision: 30, scale: 15
57
+ t.decimal :fiat_profit, precision: 30, scale: 15
58
+ t.decimal :fx_rate, precision: 20, scale: 8
59
+ t.timestamps null: true
60
60
  end
61
61
 
62
62
  create_table :sell_closing_flows do |t|
63
- t.decimal :desired_price, precision: 30, scale: 15
64
- t.decimal :quantity, precision: 30, scale: 15
65
- t.decimal :amount, precision: 30, scale: 15
66
- t.boolean :done, null: false, default: false
67
- t.decimal :btc_profit, precision: 30, scale: 15
68
- t.decimal :usd_profit, precision: 30, scale: 15
69
- t.timestamps
63
+ t.decimal :desired_price, precision: 30, scale: 15
64
+ t.decimal :quantity, precision: 30, scale: 15
65
+ t.decimal :amount, precision: 30, scale: 15
66
+ t.boolean :done, null: false, default: false
67
+ t.decimal :btc_profit, precision: 30, scale: 15
68
+ t.decimal :fiat_profit, precision: 30, scale: 15
69
+ t.decimal :fx_rate, precision: 20, scale: 8
70
+ t.timestamps null: true
70
71
  end
71
-
72
+
72
73
  create_table :close_buys do |t|
73
74
  t.belongs_to :closing_flow
74
- t.decimal :amount, precision: 30, scale: 15
75
- t.decimal :quantity, precision: 30, scale: 15
76
- t.string :order_id
77
- t.timestamps
75
+ t.decimal :amount, precision: 30, scale: 15
76
+ t.decimal :quantity, precision: 30, scale: 15
77
+ t.string :order_id
78
+ t.timestamps null: true
78
79
  end
79
80
  add_index :close_buys, :order_id
80
81
 
81
82
  create_table :close_sells do |t|
82
83
  t.belongs_to :closing_flow
83
- t.decimal :amount, precision: 30, scale: 15
84
- t.decimal :quantity, precision: 30, scale: 15
85
- t.string :order_id
86
- t.timestamps
84
+ t.decimal :amount, precision: 30, scale: 15
85
+ t.decimal :quantity, precision: 30, scale: 15
86
+ t.string :order_id
87
+ t.timestamps null: true
87
88
  end
88
89
  add_index :close_sells, :order_id
89
90
  end
90
91
 
91
92
  unless ActiveRecord::Base.connection.table_exists?('stores')
92
- create_table "stores", force: true do |t|
93
- t.decimal "taker_usd", precision: 20, scale: 8
94
- t.decimal "taker_btc", precision: 20, scale: 8
95
- t.boolean "hold", default: false
96
- t.text "log"
97
- t.decimal "usd_stop", precision: 20, scale: 8
98
- t.decimal "usd_warning", precision: 20, scale: 8
99
- t.decimal "btc_stop", precision: 20, scale: 8
100
- t.decimal "btc_warning", precision: 20, scale: 8
101
- t.datetime "last_warning"
102
- t.decimal "buying_profit", precision: 20, scale: 8
103
- t.decimal "selling_profit", precision: 20, scale: 8
104
- t.decimal "buying_amount_to_spend_per_order", precision: 20, scale: 8
105
- t.decimal "selling_quantity_to_sell_per_order", precision: 20, scale: 8
106
- t.timestamps
93
+ create_table :stores, force: true do |t|
94
+ t.decimal :taker_fiat, precision: 20, scale: 8
95
+ t.decimal :taker_btc, precision: 20, scale: 8
96
+ t.boolean :hold, default: false
97
+ t.text :log
98
+ t.decimal :fiat_stop, precision: 20, scale: 8
99
+ t.decimal :fiat_warning, precision: 20, scale: 8
100
+ t.decimal :btc_stop, precision: 20, scale: 8
101
+ t.decimal :btc_warning, precision: 20, scale: 8
102
+ t.datetime :last_warning
103
+ t.decimal :buying_profit, precision: 20, scale: 8
104
+ t.decimal :selling_profit, precision: 20, scale: 8
105
+ t.decimal :buying_amount_to_spend_per_order, precision: 20, scale: 8
106
+ t.decimal :selling_quantity_to_sell_per_order, precision: 20, scale: 8
107
+ t.decimal :fx_rate, precision: 20, scale: 8
108
+ t.timestamps null: true
107
109
  end
108
110
  end
109
111
  end
110
-
111
112
  end
112
113
  end
113
-
@@ -0,0 +1,142 @@
1
+ # This class represents the general behaviour for trading platform wrappers.
2
+ class ApiWrapper
3
+ MIN_AMOUNT = 5
4
+
5
+ Transaction = Struct.new(
6
+ :id, # Integer
7
+ :price, # Decimal
8
+ :amount, # Decimal
9
+ :timestamp # Epoch Integer
10
+ )
11
+
12
+ Order = Struct.new(
13
+ :id, # String
14
+ :type, # Symbol
15
+ :price, # Decimal
16
+ :amount, # Decimal
17
+ :timestamp, # Integer
18
+ :raw_order # Actual order object
19
+ ) do
20
+ def method_missing(method_name, *args, &block)
21
+ raw_order.respond_to?(method_name) ? raw_order.send(method_name, *args, &block) : super
22
+ end
23
+
24
+ def respond_to_missing?(method_name, include_private = false)
25
+ raw_order.respond_to?(method_name) || super
26
+ end
27
+ end
28
+
29
+ OrderBook = Struct.new(
30
+ :timestamp, # Integer
31
+ :bids, # [OrderSummary]
32
+ :asks # [OrderSummary]
33
+ )
34
+
35
+ OrderSummary = Struct.new(
36
+ :price, # Decimal
37
+ :quantity # Decimal
38
+ )
39
+
40
+ BalanceSummary = Struct.new(
41
+ :btc, # Balance
42
+ :usd, # Balance
43
+ :fee # Decimal
44
+ )
45
+
46
+ Balance = Struct.new(
47
+ :total, # Decimal
48
+ :reserved, # Decimal
49
+ :available # Decimal
50
+ )
51
+
52
+ UserTransaction = Struct.new(
53
+ :order_id, # Integer
54
+ :usd, # Decimal
55
+ :btc, # Decimal
56
+ :btc_usd, # Decimal
57
+ :fee, # Decimal
58
+ :type, # Integer
59
+ :timestamp # Epoch Integer
60
+ )
61
+
62
+ # @return [Void]
63
+ def self.setup(_settings)
64
+ raise 'self subclass responsibility'
65
+ end
66
+
67
+ # @return [Array<Transaction>]
68
+ def self.transactions
69
+ raise 'self subclass responsibility'
70
+ end
71
+
72
+ # @return [OrderBook]
73
+ def self.order_book(_retries = 20)
74
+ raise 'self subclass responsibility'
75
+ end
76
+
77
+ # @return [BalanceSummary]
78
+ def self.balance
79
+ raise 'self subclass responsibility'
80
+ end
81
+
82
+ # @return [nil]
83
+ def self.cancel
84
+ raise 'self subclass responsibility'
85
+ end
86
+
87
+ # @return [Array<Order>]
88
+ def self.orders
89
+ raise 'self subclass responsibility'
90
+ end
91
+
92
+ # @return [UserTransaction]
93
+ def self.user_transacitions
94
+ raise 'self subclass responsibility'
95
+ end
96
+
97
+ # @param type
98
+ # @param price
99
+ # @param quantity
100
+ def self.place_order(type, price, quantity)
101
+ order = send_order(type, price, quantity)
102
+ return order unless order.nil? || order.id.nil?
103
+
104
+ BitexBot::Robot.log(:debug, "Captured error when placing order on #{self.class.name}")
105
+ # Order may have gone through and be stuck somewhere in Wrapper's pipeline.
106
+ # We just sleep for a bit and then look for the order.
107
+ 20.times do
108
+ BitexBot::Robot.sleep_for(10)
109
+ order = find_lost(type, price, quantity)
110
+ return order if order.present?
111
+ end
112
+ raise OrderNotFound, "Closing: #{type} order not found for #{quantity} BTC @ $#{price}. #{order}"
113
+ end
114
+
115
+ # Hook Method - arguments could not be used in their entirety by the subclasses
116
+ def self.send_order(_type, _price, _quantity)
117
+ raise 'self subclass responsibility'
118
+ end
119
+
120
+ # @param order_method [String] buy|sell
121
+ # @param price [Decimal]
122
+ #
123
+ # Hook Method - arguments could not be used in their entirety by the subclasses
124
+ def self.find_lost(_type, _price, _quantity)
125
+ raise 'self subclass responsibility'
126
+ end
127
+
128
+ # @param order_id
129
+ # @param transactions
130
+ #
131
+ # @return [Array<Decimal, Decimal>]
132
+ def self.amount_and_quantity(_order_id, _transactions)
133
+ raise 'self subclass responsibility'
134
+ end
135
+
136
+ def self.enough_order_size?(quantity, price)
137
+ (quantity * price) > MIN_AMOUNT
138
+ end
139
+ end
140
+
141
+ class OrderNotFound < StandardError; end
142
+ class ApiWrapperError < StandardError; end
@@ -0,0 +1,137 @@
1
+ # Wrapper implementation for Bitstamp API.
2
+ # https://www.bitstamp.net/api/
3
+ class BitstampApiWrapper < ApiWrapper
4
+ def self.setup(settings)
5
+ Bitstamp.setup do |config|
6
+ config.key = settings.api_key
7
+ config.secret = settings.secret
8
+ config.client_id = settings.client_id
9
+ end
10
+ end
11
+
12
+ def self.amount_and_quantity(order_id, transactions)
13
+ closes = transactions.select { |t| t.order_id.to_s == order_id }
14
+ amount = closes.map { |c| c.usd.to_d }.sum.abs
15
+ quantity = closes.map { |c| c.btc.to_d }.sum.abs
16
+
17
+ [amount, quantity]
18
+ end
19
+
20
+ def self.balance
21
+ balance_summary_parser(Bitstamp.balance.symbolize_keys)
22
+ rescue StandardError => e
23
+ raise ApiWrapperError, "Bitstamp balance failed: #{e.message}"
24
+ end
25
+
26
+ def self.cancel(order)
27
+ Bitstamp::Order.new(id: order.id).cancel!
28
+ rescue StandardError => e
29
+ raise ApiWrapperError, "Bitstamp cancel! failed: #{e.message}"
30
+ end
31
+
32
+ def self.find_lost(type, price, _quantity)
33
+ orders.find { |o| o.type == type && o.price == price && o.timestamp >= 5.minutes.ago.to_i }
34
+ end
35
+
36
+ # rubocop:disable Metrics/AbcSize
37
+ def self.order_book(retries = 20)
38
+ book = Bitstamp.order_book.deep_symbolize_keys
39
+ age = Time.now.to_i - book[:timestamp].to_i
40
+ return order_book_parser(book) if age <= 300
41
+
42
+ BitexBot::Robot.log(:info, "Refusing to continue as orderbook is #{age} seconds old")
43
+ order_book(retries)
44
+ rescue StandardError
45
+ raise if retries.zero?
46
+
47
+ BitexBot::Robot.log(:info, "Bitstamp order book failed, retrying #{retries} more times")
48
+ BitexBot::Robot.sleep_for 1
49
+ order_book(retries - 1)
50
+ end
51
+ # rubocop:enable Metrics/AbcSize
52
+
53
+ def self.orders
54
+ Bitstamp.orders.all.map { |o| order_parser(o) }
55
+ rescue StandardError => e
56
+ raise ApiWrapperError, "Bitstamp orders failed: #{e.message}"
57
+ end
58
+
59
+ def self.send_order(type, price, quantity)
60
+ Bitstamp.orders.send(type, amount: quantity.round(4), price: price.round(2))
61
+ end
62
+
63
+ def self.transactions
64
+ Bitstamp.transactions.map { |t| transaction_parser(t) }
65
+ rescue StandardError => e
66
+ raise ApiWrapperError, "Bitstamp transactions failed: #{e.message}"
67
+ end
68
+
69
+ def self.user_transactions
70
+ Bitstamp.user_transactions.all.map { |ut| user_transaction_parser(ut) }
71
+ rescue StandardError => e
72
+ raise ApiWrapperError, "Bitstamp user_transactions failed: #{e.message}"
73
+ end
74
+
75
+ private_class_method
76
+
77
+ # {
78
+ # btc_reserved: '0', btc_available: '0', btc_balance: '0',
79
+ # usd_reserved: '1.02, usd_available: '6952.05', usd_balance: '6953.07',
80
+ # fee: '0.4000'
81
+ # }
82
+ def self.balance_summary_parser(balances)
83
+ BalanceSummary.new(balance_parser(balances, :btc), balance_parser(balances, :usd), balances[:fee].to_d)
84
+ end
85
+
86
+ def self.balance_parser(balances, currency)
87
+ Balance.new(
88
+ balances["#{currency}_balance".to_sym].to_d,
89
+ balances["#{currency}_reserved".to_sym].to_d,
90
+ balances["#{currency}_available".to_sym].to_d
91
+ )
92
+ end
93
+
94
+ # {
95
+ # timestamp: '1380237884',
96
+ # bids: [['124.55', '1.58057006'], ['124.40', '14.91779125']],
97
+ # asks: [['124.56', '0.81888247'], ['124.57', '0.81078911']]
98
+ # }
99
+ def self.order_book_parser(book)
100
+ OrderBook.new(book[:timestamp].to_i, order_summary_parser(book[:bids]), order_summary_parser(book[:asks]))
101
+ end
102
+
103
+ def self.order_is_done?(order)
104
+ order.nil?
105
+ end
106
+
107
+ # <Bitstamp::Order @id=76, @type=0, @price='1.1', @amount='1.0', @datetime='2013-09-26 23:15:04'>
108
+ def self.order_parser(order)
109
+ type = order.type.zero? ? :buy : :sell
110
+ Order.new(order.id.to_s, type, order.price.to_d, order.amount.to_d, order.datetime.to_datetime.to_i, order)
111
+ end
112
+
113
+ def self.order_summary_parser(orders)
114
+ orders.map { |order| OrderSummary.new(order[0].to_d, order[1].to_d) }
115
+ end
116
+
117
+ # <Bitstamp::Transactions: @tid=1469074, @price='126.95', @amount='1.10000000', @date='1380648951'>
118
+ def self.transaction_parser(transaction)
119
+ Transaction.new(transaction.tid, transaction.price.to_d, transaction.amount.to_d, transaction.date.to_i)
120
+ end
121
+
122
+ # <Bitstamp::UserTransaction:
123
+ # @usd='-373.51', @btc='3.00781124', @btc_usd='124.18', @order_id=7623942, @fee='1.50', @type=2, @id=1444404,
124
+ # @datetime='2013-09-26 13:28:55'
125
+ # >
126
+ def self.user_transaction_parser(user_transaction)
127
+ UserTransaction.new(
128
+ user_transaction.order_id,
129
+ user_transaction.usd.to_d,
130
+ user_transaction.btc.to_d,
131
+ user_transaction.btc_usd.to_d,
132
+ user_transaction.fee.to_d,
133
+ user_transaction.type,
134
+ Time.new(user_transaction.datetime).to_i
135
+ )
136
+ end
137
+ end