cryptum 0.0.230

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/main.yml +16 -0
  3. data/.gitignore +30 -0
  4. data/.rspec +3 -0
  5. data/.rspec_status +0 -0
  6. data/.rubocop.yml +5 -0
  7. data/.rubocop_todo.yml +250 -0
  8. data/.ruby-gemset +1 -0
  9. data/.ruby-version +1 -0
  10. data/CODE_OF_CONDUCT.md +84 -0
  11. data/Gemfile +36 -0
  12. data/LICENSE +674 -0
  13. data/README.md +72 -0
  14. data/Rakefile +19 -0
  15. data/bin/cryptum +72 -0
  16. data/bin/cryptum-forecast +199 -0
  17. data/bin/cryptum-repl +73 -0
  18. data/bin/cryptum_autoinc_version +38 -0
  19. data/build_cryptum_gem.sh +52 -0
  20. data/cryptum.gemspec +50 -0
  21. data/cryptum_container.sh +1 -0
  22. data/docker/cryptum.json +60 -0
  23. data/docker/cryptum_container.sh +59 -0
  24. data/docker/packer_secrets.json.EXAMPLE +7 -0
  25. data/docker/provisioners/cryptum.sh +11 -0
  26. data/docker/provisioners/docker_bashrc.sh +2 -0
  27. data/docker/provisioners/docker_rvm.sh +22 -0
  28. data/docker/provisioners/init_image.sh +28 -0
  29. data/docker/provisioners/post_install.sh +6 -0
  30. data/docker/provisioners/ruby.sh +16 -0
  31. data/docker/provisioners/upload_globals.sh +49 -0
  32. data/etc/bot_confs/.gitkeep +0 -0
  33. data/etc/bot_confs/BOT_CONF.TEMPLATE +10 -0
  34. data/etc/coinbase_pro.yaml.EXAMPLE +8 -0
  35. data/git_commit.sh +22 -0
  36. data/lib/cryptum/api.rb +693 -0
  37. data/lib/cryptum/bot_conf.rb +76 -0
  38. data/lib/cryptum/event/buy.rb +144 -0
  39. data/lib/cryptum/event/cancel.rb +49 -0
  40. data/lib/cryptum/event/history.rb +64 -0
  41. data/lib/cryptum/event/key_press.rb +64 -0
  42. data/lib/cryptum/event/sell.rb +120 -0
  43. data/lib/cryptum/event.rb +168 -0
  44. data/lib/cryptum/log.rb +34 -0
  45. data/lib/cryptum/matrix.rb +181 -0
  46. data/lib/cryptum/option/choice.rb +26 -0
  47. data/lib/cryptum/option.rb +161 -0
  48. data/lib/cryptum/order_book/generate.rb +111 -0
  49. data/lib/cryptum/order_book/indicator.rb +16 -0
  50. data/lib/cryptum/order_book/market_trend.rb +161 -0
  51. data/lib/cryptum/order_book/profit_margin.rb +55 -0
  52. data/lib/cryptum/order_book/weighted_avg.rb +157 -0
  53. data/lib/cryptum/order_book.rb +156 -0
  54. data/lib/cryptum/portfolio/balance.rb +123 -0
  55. data/lib/cryptum/portfolio.rb +15 -0
  56. data/lib/cryptum/ui/command.rb +274 -0
  57. data/lib/cryptum/ui/key_press_event.rb +22 -0
  58. data/lib/cryptum/ui/market_trend.rb +117 -0
  59. data/lib/cryptum/ui/order_execution.rb +478 -0
  60. data/lib/cryptum/ui/order_plan.rb +376 -0
  61. data/lib/cryptum/ui/order_timer.rb +119 -0
  62. data/lib/cryptum/ui/portfolio.rb +231 -0
  63. data/lib/cryptum/ui/signal_engine.rb +122 -0
  64. data/lib/cryptum/ui/terminal_window.rb +95 -0
  65. data/lib/cryptum/ui/ticker.rb +317 -0
  66. data/lib/cryptum/ui.rb +306 -0
  67. data/lib/cryptum/version.rb +5 -0
  68. data/lib/cryptum/web_sock/coinbase.rb +94 -0
  69. data/lib/cryptum/web_sock/event_machine.rb +182 -0
  70. data/lib/cryptum/web_sock.rb +16 -0
  71. data/lib/cryptum.rb +183 -0
  72. data/order_books/.gitkeep +0 -0
  73. data/reinstall_cryptum_gemset.sh +29 -0
  74. data/spec/lib/cryptum/api_spec.rb +10 -0
  75. data/spec/lib/cryptum/event_spec.rb +10 -0
  76. data/spec/lib/cryptum/log_spec.rb +10 -0
  77. data/spec/lib/cryptum/option_spec.rb +10 -0
  78. data/spec/lib/cryptum/order_book/generate_spec.rb +10 -0
  79. data/spec/lib/cryptum/order_book/market_trend_spec.rb +10 -0
  80. data/spec/lib/cryptum/order_book_spec.rb +10 -0
  81. data/spec/lib/cryptum/ui/command_spec.rb +10 -0
  82. data/spec/lib/cryptum/ui/ticker_spec.rb +10 -0
  83. data/spec/lib/cryptum/ui_spec.rb +10 -0
  84. data/spec/lib/cryptum/web_sock_spec.rb +10 -0
  85. data/spec/lib/cryptum_spec.rb +10 -0
  86. data/spec/spec_helper.rb +3 -0
  87. data/upgrade_Gemfile_gems.sh +20 -0
  88. data/upgrade_cryptum.sh +13 -0
  89. data/upgrade_gem.sh +4 -0
  90. data/upgrade_ruby.sh +46 -0
  91. metadata +472 -0
@@ -0,0 +1,693 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'addressable'
4
+ require 'base64'
5
+ require 'json'
6
+ require 'openssl'
7
+ require 'rest-client'
8
+
9
+ module Cryptum
10
+ # This plugin is used to interact withbtje Coinbase REST API
11
+ module API
12
+ # Supported Method Parameters::
13
+ # Cryptum::API.generate_signature(
14
+ # )
15
+
16
+ public_class_method def self.generate_signature(opts = {})
17
+ api_secret = opts[:api_secret]
18
+
19
+ http_method = if opts[:http_method].nil?
20
+ :GET
21
+ else
22
+ opts[:http_method].to_s.upcase.scrub.strip.chomp.to_sym
23
+ end
24
+
25
+ api_call = opts[:api_call].to_s.scrub.strip.chomp
26
+ api_call = '/users/self/verify' if opts[:api_call].nil?
27
+
28
+ if opts[:params].nil?
29
+ path = api_call
30
+ else
31
+ uri = Addressable::URI.new
32
+ uri.query_values = opts[:params]
33
+ params = uri.query
34
+ path = "#{api_call}?#{params}"
35
+ end
36
+
37
+ http_body = opts[:http_body].to_s.scrub.strip.chomp
38
+
39
+ api_timestamp = Time.now.utc.to_i.to_s
40
+
41
+ api_signature = Base64.strict_encode64(
42
+ OpenSSL::HMAC.digest(
43
+ 'sha256',
44
+ Base64.strict_decode64(api_secret),
45
+ "#{api_timestamp}#{http_method}#{path}#{http_body}"
46
+ )
47
+ )
48
+
49
+ if http_body == ''
50
+ api_signature = Base64.strict_encode64(
51
+ OpenSSL::HMAC.digest(
52
+ 'sha256',
53
+ Base64.strict_decode64(api_secret),
54
+ "#{api_timestamp}#{http_method}#{path}"
55
+ )
56
+ )
57
+ end
58
+
59
+ api_signature_response = {}
60
+ api_signature_response[:api_timestamp] = api_timestamp
61
+ api_signature_response[:api_signature] = api_signature
62
+
63
+ api_signature_response
64
+ rescue RestClient::ExceptionWithResponse => e
65
+ File.open('/tmp/cryptum-errors.txt', 'a') do |f|
66
+ f.puts Time.now.strftime('%Y-%m-%d %H:%M:%S.%N %z')
67
+ f.puts "Module: #{self}"
68
+ f.puts "URL: #{api_endpoint}#{api_call}"
69
+ f.puts "PARAMS: #{params.inspect}"
70
+ f.puts "HTTP POST BODY: #{http_body.inspect}" if http_body != ''
71
+ f.puts "#{e}\n#{e.response}\n\n\n"
72
+ end
73
+ rescue StandardError => e
74
+ raise e
75
+ end
76
+
77
+ private_class_method def self.rest_api_call(opts = {})
78
+ env = opts[:env]
79
+ option_choice = opts[:option_choice]
80
+ order_type = opts[:order_type]
81
+ event_notes = opts[:event_notes]
82
+ api_endpoint = opts[:api_endpoint]
83
+ base_increment = opts[:base_increment].to_f
84
+
85
+ api_key = env[:api_key]
86
+ api_secret = env[:api_secret]
87
+ api_passphrase = env[:api_passphrase]
88
+ api_endpoint = 'https://api.pro.coinbase.com'
89
+ api_endpoint = 'https://api-public.sandbox.pro.coinbase.com' if env[:env] == :sandbox
90
+ api_endpoint = opts[:api_endpoint] if opts[:api_endpoint]
91
+
92
+ http_method = if opts[:http_method].nil?
93
+ :GET
94
+ else
95
+ opts[:http_method].to_s.upcase.scrub.strip.chomp.to_sym
96
+ end
97
+ api_call = opts[:api_call].to_s.scrub
98
+ params = opts[:params]
99
+ http_body = opts[:http_body].to_s.scrub.strip.chomp
100
+
101
+ max_conn_attempts = 30
102
+ conn_attempt = 0
103
+
104
+ begin
105
+ conn_attempt += 1
106
+ if option_choice.proxy
107
+ rest_client = RestClient
108
+ rest_client.proxy = option_choice.proxy
109
+ rest_client_request = rest_client::Request
110
+ else
111
+ rest_client_request = RestClient::Request
112
+ end
113
+
114
+ api_signature_response = generate_signature(
115
+ api_secret: api_secret,
116
+ http_method: http_method,
117
+ api_call: api_call,
118
+ params: params,
119
+ http_body: http_body
120
+ )
121
+ api_signature = api_signature_response[:api_signature]
122
+ api_timestamp = api_signature_response[:api_timestamp]
123
+
124
+ case http_method
125
+ when :GET
126
+ headers = {
127
+ content_type: 'application/json; charset=UTF-8',
128
+ CB_ACCESS_TIMESTAMP: api_timestamp,
129
+ CB_ACCESS_PASSPHRASE: api_passphrase,
130
+ CB_ACCESS_KEY: api_key,
131
+ CB_ACCESS_SIGN: api_signature
132
+ }
133
+
134
+ headers[:params] = params if params
135
+ headers[:ORDER_TYPE] = order_type if order_type
136
+ headers[:EVENT_NOTES] = event_notes if event_notes
137
+
138
+ response = rest_client_request.execute(
139
+ method: :GET,
140
+ url: "#{api_endpoint}#{api_call}",
141
+ headers: headers,
142
+ verify_ssl: false
143
+ )
144
+
145
+ when :DELETE
146
+ headers = {
147
+ content_type: 'application/json; charset=UTF-8',
148
+ CB_ACCESS_TIMESTAMP: api_timestamp,
149
+ CB_ACCESS_PASSPHRASE: api_passphrase,
150
+ CB_ACCESS_KEY: api_key,
151
+ CB_ACCESS_SIGN: api_signature
152
+ }
153
+
154
+ headers[:params] = params if params
155
+ headers[:ORDER_TYPE] = order_type if order_type
156
+ headers[:EVENT_NOTES] = event_notes if event_notes
157
+
158
+ response = rest_client_request.execute(
159
+ method: :DELETE,
160
+ url: "#{api_endpoint}#{api_call}",
161
+ headers: headers,
162
+ verify_ssl: false
163
+ )
164
+
165
+ when :POST
166
+ headers = {
167
+ content_type: 'application/json; charset=UTF-8',
168
+ CB_ACCESS_TIMESTAMP: api_timestamp,
169
+ CB_ACCESS_PASSPHRASE: api_passphrase,
170
+ CB_ACCESS_KEY: api_key,
171
+ CB_ACCESS_SIGN: api_signature
172
+ }
173
+
174
+ headers[:params] = params if params
175
+ headers[:ORDER_TYPE] = order_type if order_type
176
+ headers[:EVENT_NOTES] = event_notes if event_notes
177
+
178
+ response = rest_client_request.execute(
179
+ method: :POST,
180
+ url: "#{api_endpoint}#{api_call}",
181
+ headers: headers,
182
+ payload: http_body,
183
+ verify_ssl: false
184
+ )
185
+
186
+ else
187
+ raise @@logger.error("Unsupported HTTP Method #{http_method} for #{self} Plugin")
188
+ end
189
+
190
+ JSON.parse(response, symbolize_names: true)
191
+ rescue RestClient::Unauthorized => e
192
+ File.open('/tmp/cryptum-errors.txt', 'a') do |f|
193
+ f.puts Time.now.strftime('%Y-%m-%d %H:%M:%S.%N %z')
194
+ f.puts "#{self}\n#{e}\n\n\n"
195
+ end
196
+
197
+ raise e if conn_attempt > max_conn_attempts
198
+
199
+ sleep 60
200
+ retry
201
+ end
202
+ rescue RestClient::ExceptionWithResponse => e
203
+ File.open('/tmp/cryptum-errors.txt', 'a') do |f|
204
+ f.puts Time.now.strftime('%Y-%m-%d %H:%M:%S.%N %z')
205
+ f.puts "Module: #{self}"
206
+ f.puts "URL: #{api_endpoint}#{api_call}"
207
+ f.puts "PARAMS: #{params.inspect}"
208
+ f.puts "HTTP POST BODY: #{http_body.inspect}" if http_body != ''
209
+ f.puts "#{e}\n#{e.response}\n\n\n"
210
+ end
211
+
212
+ insufficient_funds = '{"message":"Insufficient funds"}'
213
+ size -= base_increment if e.response == insufficient_funds
214
+
215
+ sleep 0.3
216
+ retry
217
+ rescue RestClient::TooManyRequests => e
218
+ File.open('/tmp/cryptum-errors.txt', 'a') do |f|
219
+ f.puts Time.now.strftime('%Y-%m-%d %H:%M:%S.%N %z')
220
+ f.puts "Module: #{self}"
221
+ f.puts "URL: #{api_endpoint}#{api_call}"
222
+ f.puts "PARAMS: #{params.inspect}"
223
+ f.puts "HTTP POST BODY: #{http_body.inspect}" if http_body != ''
224
+ f.puts "#{e}\n#{e.response}\n\n\n"
225
+ end
226
+
227
+ sleep 1
228
+ retry
229
+ end
230
+
231
+ public_class_method def self.submit_limit_order(opts = {})
232
+ option_choice = opts[:option_choice]
233
+ env = opts[:env]
234
+ price = opts[:price]
235
+ size = opts[:size]
236
+ buy_or_sell = opts[:buy_or_sell]
237
+ event_history = opts[:event_history]
238
+ bot_conf = opts[:bot_conf]
239
+ buy_order_id = opts[:buy_order_id]
240
+
241
+ tpm = bot_conf[:target_profit_margin_percent].to_f
242
+ tpm_cast_as_decimal = tpm / 100
243
+
244
+ product_id = option_choice.symbol.to_s.gsub('_', '-').upcase
245
+
246
+ this_product = event_history.order_book[:this_product]
247
+ base_min_size = this_product[:base_min_size]
248
+ base_increment = this_product[:base_increment]
249
+ quote_increment = this_product[:quote_increment]
250
+ crypto_smallest_size_to_buy = base_min_size.to_s.split('.')[-1].length
251
+ crypto_smallest_decimal = base_increment.to_s.split('.')[-1].length
252
+ fiat_smallest_decimal = quote_increment.to_s.split('.')[-1].length
253
+
254
+ order_hash = {}
255
+
256
+ order_hash[:type] = 'limit'
257
+ order_hash[:time_in_force] = 'GTC'
258
+
259
+ if buy_or_sell == :buy
260
+ order_hash[:time_in_force] = 'GTT'
261
+ order_hash[:cancel_after] = 'min'
262
+ end
263
+
264
+ order_hash[:size] = size
265
+ order_hash[:price] = price
266
+ order_hash[:side] = buy_or_sell
267
+ order_hash[:product_id] = product_id
268
+
269
+ http_body = order_hash.to_json
270
+
271
+ limit_order_resp = rest_api_call(
272
+ option_choice: option_choice,
273
+ env: env,
274
+ http_method: :POST,
275
+ api_call: '/orders',
276
+ http_body: http_body,
277
+ base_increment: base_increment
278
+ )
279
+
280
+ # Populate Order ID on the Buy
281
+ # to know what to do on the Sell
282
+ case buy_or_sell
283
+ when :buy
284
+ this_order = event_history.order_book[:order_plan].shift
285
+ this_order[:buy_order_id] = limit_order_resp[:id]
286
+ this_order[:price] = price
287
+ targ_price = price.to_f + (price.to_f * tpm_cast_as_decimal)
288
+ this_order[:tpm] = format(
289
+ '%0.2f',
290
+ tpm
291
+ )
292
+ this_order[:target_price] = format(
293
+ "%0.#{fiat_smallest_decimal}f",
294
+ targ_price
295
+ )
296
+
297
+ this_order[:size] = size
298
+
299
+ this_order[:color] = :cyan
300
+ event_history.order_book[:order_history_meta].push(this_order)
301
+ when :sell
302
+ sell_order_id = limit_order_resp[:id]
303
+ event_history.order_book[:order_history_meta].each do |meta|
304
+ if meta[:buy_order_id] == buy_order_id
305
+ meta[:sell_order_id] = sell_order_id
306
+ meta[:color] = :yellow
307
+ end
308
+ end
309
+ end
310
+
311
+ event_history
312
+ rescue StandardError => e
313
+ raise e
314
+ end
315
+
316
+ public_class_method def self.gtfo(opts = {})
317
+ option_choice = opts[:option_choice]
318
+ env = opts[:env]
319
+ event_history = opts[:event_history]
320
+
321
+ # Cancel all open orders
322
+ cancel_all_open_orders(
323
+ env: env,
324
+ option_choice: option_choice
325
+ )
326
+
327
+ product_id = option_choice.symbol.to_s.gsub('_', '-').upcase
328
+
329
+ this_product = event_history.order_book[:this_product]
330
+ base_min_size = this_product[:base_min_size]
331
+ base_increment = this_product[:base_increment]
332
+ quote_increment = this_product[:quote_increment]
333
+ crypto_smallest_size_to_buy = base_min_size.to_s.split('.')[-1].length
334
+ crypto_smallest_decimal = base_increment.to_s.split('.')[-1].length
335
+ fiat_smallest_decimal = quote_increment.to_s.split('.')[-1].length
336
+
337
+ # TODO: Calculate / Price / Size
338
+ last_three_prices_arr = []
339
+ last_ticker_price = event_history.order_book[:ticker_price].to_f
340
+ second_to_last_ticker_price = event_history.order_book[:ticker_price_second_to_last].to_f
341
+ third_to_last_ticker_price = event_history.order_book[:ticker_price_third_to_last].to_f
342
+ last_three_prices_arr.push(last_ticker_price)
343
+ last_three_prices_arr.push(second_to_last_ticker_price)
344
+ last_three_prices_arr.push(third_to_last_ticker_price)
345
+ limit_price = last_three_prices_arr.sort[1]
346
+ price = format(
347
+ "%0.#{fiat_smallest_decimal}f",
348
+ limit_price
349
+ )
350
+
351
+ crypto_currency = option_choice.symbol.to_s.upcase.split('_').first.to_sym
352
+ portfolio = event_history.order_book[:portfolio]
353
+ this_account = portfolio.select do |account|
354
+ account[:currency] == crypto_currency.to_s
355
+ end
356
+ balance = format(
357
+ "%0.#{crypto_smallest_decimal}f",
358
+ this_account.first[:balance]
359
+ )
360
+
361
+ current_crypto_fiat_value = format(
362
+ '%0.2f',
363
+ balance.to_f * price.to_f
364
+ )
365
+
366
+
367
+ order_hash = {}
368
+
369
+ order_hash[:type] = 'limit'
370
+ order_hash[:time_in_force] = 'GTT'
371
+ order_hash[:cancel_after] = 'min'
372
+
373
+ order_hash[:size] = balance
374
+ order_hash[:price] = price
375
+ order_hash[:side] = :sell
376
+ order_hash[:product_id] = product_id
377
+
378
+ http_body = order_hash.to_json
379
+
380
+ limit_order_resp = rest_api_call(
381
+ option_choice: option_choice,
382
+ env: env,
383
+ http_method: :POST,
384
+ api_call: '/orders',
385
+ http_body: http_body,
386
+ base_increment: base_increment
387
+ )
388
+
389
+ # Populate Order ID on the Buy
390
+ # to know what to do on the Sell
391
+ this_order = {}
392
+ this_order[:plan_no] = '0.0'
393
+ this_order[:fiat_available] = '0.00'
394
+ this_order[:risk_alloc] = current_crypto_fiat_value
395
+ this_order[:allocation_decimal] = '1.0'
396
+ this_order[:allocation_percent] = '100.0'
397
+ this_order[:invest] = current_crypto_fiat_value
398
+ this_order[:return] = current_crypto_fiat_value
399
+ this_order[:profit] = '0.0'
400
+ this_order[:buy_order_id] = 'N/A'
401
+ this_order[:price] = price
402
+ this_order[:tpm] = '0.00'
403
+ this_order[:target_price] = current_crypto_fiat_value
404
+ this_order[:size] = balance
405
+ this_order[:color] = :magenta
406
+ this_order[:sell_order_id] = limit_order_resp[:id]
407
+
408
+ event_history.order_book[:order_history_meta].push(this_order)
409
+
410
+ event_history
411
+ rescue StandardError => e
412
+ raise e
413
+ end
414
+
415
+ public_class_method def self.cancel_open_order(opts = {})
416
+ env = opts[:env]
417
+ option_choice = opts[:option_choice]
418
+ order_id = opts[:order_id]
419
+ order_type = opts[:order_type]
420
+
421
+ product_id = option_choice.symbol.to_s.gsub('_', '-').upcase
422
+
423
+ order_hash = {}
424
+ order_hash[:product_id] = product_id
425
+
426
+ params = order_hash
427
+
428
+ rest_api_call(
429
+ env: env,
430
+ http_method: :DELETE,
431
+ api_call: "/orders/#{order_id}",
432
+ option_choice: option_choice,
433
+ params: params,
434
+ order_type: order_type
435
+ )
436
+ rescue StandardError => e
437
+ raise e
438
+ end
439
+
440
+ public_class_method def self.cancel_all_open_orders(opts = {})
441
+ env = opts[:env]
442
+ option_choice = opts[:option_choice]
443
+ # order_type = opts[:order_type]
444
+ event_notes = opts[:event_notes]
445
+
446
+ product_id = option_choice.symbol.to_s.gsub('_', '-').upcase
447
+
448
+ order_hash = {}
449
+ order_hash[:product_id] = product_id
450
+
451
+ # http_body = order_hash.to_json
452
+ params = order_hash
453
+
454
+ canceled_order_id_arr = []
455
+ loop do
456
+ canceled_order_id_arr = rest_api_call(
457
+ env: env,
458
+ http_method: :DELETE,
459
+ api_call: '/orders',
460
+ option_choice: option_choice,
461
+ params: params,
462
+ event_notes: event_notes
463
+ )
464
+
465
+ break if canceled_order_id_arr.empty?
466
+ end
467
+ canceled_order_id_arr
468
+ rescue StandardError => e
469
+ raise e
470
+ end
471
+
472
+ public_class_method def self.get_products(opts = {})
473
+ option_choice = opts[:option_choice]
474
+ env = opts[:env]
475
+
476
+ products = rest_api_call(
477
+ option_choice: option_choice,
478
+ env: env,
479
+ http_method: :GET,
480
+ api_call: '/products'
481
+ )
482
+
483
+ if products.length.positive?
484
+ supported_products_filter = products.select do |product|
485
+ product[:id].match?(/USD$/) &&
486
+ product[:status] == 'online' &&
487
+ product[:fx_stablecoin] == false
488
+ end
489
+ sorted_products = supported_products_filter.sort_by { |hash| hash[:id] }
490
+ end
491
+
492
+ sorted_products
493
+ rescue StandardError => e
494
+ raise e
495
+ end
496
+
497
+ public_class_method def self.get_exchange_rates(opts = {})
498
+ option_choice = opts[:option_choice]
499
+ env = opts[:env]
500
+
501
+ api_endpoint = 'https://api.coinbase.com/v2'
502
+ exchange_rates_api_call = '/exchange-rates'
503
+
504
+ # We don't always get fees back from Coinbase...
505
+ # This is a hack to ensure we do.
506
+ exchange = {}
507
+ exchange = rest_api_call(
508
+ option_choice: option_choice,
509
+ env: env,
510
+ http_method: :GET,
511
+ api_endpoint: api_endpoint,
512
+ api_call: exchange_rates_api_call
513
+ )
514
+
515
+ exchange[:data][:rates]
516
+ rescue StandardError => e
517
+ raise e
518
+ end
519
+
520
+ public_class_method def self.get_portfolio(opts = {})
521
+ option_choice = opts[:option_choice]
522
+ env = opts[:env]
523
+ crypto = opts[:crypto]
524
+ fiat = opts[:fiat]
525
+ fiat_portfolio_file = opts[:fiat_portfolio_file]
526
+ event_notes = opts[:event_notes]
527
+
528
+ # Retrieve Exchange Rates
529
+ exchange_rates = get_exchange_rates(
530
+ option_choice: option_choice,
531
+ env: env
532
+ )
533
+
534
+ portfolio_complete_arr = []
535
+ portfolio_complete_arr = rest_api_call(
536
+ option_choice: option_choice,
537
+ env: env,
538
+ http_method: :GET,
539
+ api_call: '/accounts',
540
+ event_notes: event_notes
541
+ )
542
+
543
+ all_products = portfolio_complete_arr.select do |products|
544
+ products if products[:balance].to_f.positive?
545
+ end
546
+
547
+ total_holdings = 0.00
548
+ all_products.each do |product|
549
+ currency = product[:currency].to_sym
550
+ this_exchange_rate = exchange_rates[currency].to_f
551
+ total_holdings += product[:balance].to_f / this_exchange_rate
552
+ end
553
+
554
+ crypto_portfolio = portfolio_complete_arr.select do |product|
555
+ product if product[:currency] == crypto
556
+ end
557
+
558
+ fiat_portfolio = portfolio_complete_arr.select do |product|
559
+ product if product[:currency] == fiat
560
+ end
561
+ fiat_portfolio.last[:total_holdings] = format(
562
+ '%0.8f',
563
+ total_holdings
564
+ )
565
+
566
+ File.open(fiat_portfolio_file, 'w') do |f|
567
+ f.puts fiat_portfolio.to_json
568
+ end
569
+
570
+ crypto_portfolio
571
+ rescue StandardError => e
572
+ raise e
573
+ end
574
+
575
+ public_class_method def self.get_fees(opts = {})
576
+ option_choice = opts[:option_choice]
577
+ env = opts[:env]
578
+
579
+ fees_api_call = '/fees'
580
+
581
+ # We don't always get fees back from Coinbase...
582
+ # This is a hack to ensure we do.
583
+ fees = {}
584
+ fees = rest_api_call(
585
+ option_choice: option_choice,
586
+ env: env,
587
+ http_method: :GET,
588
+ api_call: fees_api_call
589
+ )
590
+
591
+ fees
592
+ rescue StandardError => e
593
+ raise e
594
+ end
595
+
596
+ public_class_method def self.get_profiles(opts = {})
597
+ option_choice = opts[:option_choice]
598
+ env = opts[:env]
599
+
600
+ profiles_api_call = '/profiles'
601
+
602
+ # We don't always get fees back from Coinbase...
603
+ # This is a hack to ensure we do.
604
+ profiles = {}
605
+ # loop do
606
+ profiles = rest_api_call(
607
+ option_choice: option_choice,
608
+ env: env,
609
+ http_method: :GET,
610
+ api_call: profiles_api_call
611
+ )
612
+
613
+ # break unless fees.empty?
614
+
615
+ # sleep 0.3
616
+ # end
617
+
618
+ profiles
619
+ rescue StandardError => e
620
+ raise e
621
+ end
622
+
623
+ public_class_method def self.get_order_history(opts = {})
624
+ option_choice = opts[:option_choice]
625
+ env = opts[:env]
626
+
627
+ product_id = option_choice.symbol.to_s.gsub('_', '-').upcase
628
+
629
+ orders_api_call = '/orders'
630
+ params = {}
631
+ params[:product_id] = product_id
632
+ params[:status] = 'all'
633
+
634
+ # We don't always get order_history back from Coinbase...
635
+ # This is a hack to ensure we do.
636
+ order_history = []
637
+ order_history = rest_api_call(
638
+ option_choice: option_choice,
639
+ env: env,
640
+ http_method: :GET,
641
+ api_call: orders_api_call,
642
+ params: params
643
+ )
644
+
645
+ # Cast UTC Timestamps as local times
646
+ order_history.each do |order|
647
+ order[:created_at] = Time.parse(
648
+ order[:created_at]
649
+ ).localtime.to_s
650
+
651
+ if order[:done_at]
652
+ order[:done_at] = Time.parse(
653
+ order[:done_at]
654
+ ).localtime.to_s
655
+ end
656
+ end
657
+
658
+ order_history
659
+ rescue StandardError => e
660
+ raise e
661
+ end
662
+
663
+ # Display Usage for this Module
664
+
665
+ public_class_method def self.help
666
+ puts "USAGE:
667
+ signature = #{self}.generate_signature(
668
+ api_secret: 'required - Coinbase Pro API Secret',
669
+ http_method: 'optional - Defaults to :GET',
670
+ api_call: 'optional - Defaults to /users/self/verify',
671
+ params: 'optional - HTTP GET Parameters',
672
+ http_body: 'optional HTTP POST Body'
673
+ )
674
+
675
+ profiles = #{self}.get_profiles(
676
+ env: 'required - Coinbase::Option.get_env Object'
677
+ )
678
+
679
+ products = #{self}.get_products(
680
+ env: 'required - Coinbase::Option.get_env Object'
681
+ )
682
+
683
+ portfolio = #{self}.get_portfolio(
684
+ env: 'required - Coinbase::Option.get_env Object'
685
+ )
686
+
687
+ order_history = #{self}.get_order_history(
688
+ env: 'required - Coinbase::Option.get_env Object'
689
+ )
690
+ "
691
+ end
692
+ end
693
+ end