cryptum 0.0.230

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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