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,478 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module Cryptum
6
+ # This plugin is used to Refresh the Cryptum console UI
7
+ module UI
8
+ module OrderExecution
9
+ # Supported Method Parameters::
10
+ # Cryptum::UI::Candle.refresh(
11
+ # order_book: 'required - Order Book Data Structure',
12
+ # event: 'required - Event from Coinbase Web Socket'
13
+ # )
14
+
15
+ public_class_method def self.refresh(opts = {})
16
+ option_choice = opts[:option_choice]
17
+ order_execute_win = opts[:order_execute_win]
18
+ env = opts[:env]
19
+ event_history = opts[:event_history]
20
+ key_press_event = opts[:key_press_event]
21
+ indicator_status = opts[:indicator_status]
22
+ bot_conf = opts[:bot_conf]
23
+ fiat_portfolio_file = opts[:fiat_portfolio_file]
24
+
25
+ event_type = event_history.event_type if option_choice.autotrade
26
+ event_side = event_history.event[:side].to_s.to_sym if option_choice.autotrade
27
+ event_reason = event_history.event[:reason].to_s.to_sym if option_choice.autotrade
28
+
29
+ ticker_price = event_history.order_book[:ticker_price].to_f
30
+ open_24h = event_history.order_book[:open_24h].to_f
31
+ this_product = event_history.order_book[:this_product]
32
+ base_min_size = this_product[:base_min_size]
33
+ base_increment = this_product[:base_increment]
34
+ quote_increment = this_product[:quote_increment]
35
+ crypto_smallest_size_to_buy = base_min_size.to_s.split('.')[-1].length
36
+ crypto_smallest_decimal = base_increment.to_s.split('.')[-1].length
37
+ fiat_smallest_decimal = quote_increment.to_s.split('.')[-1].length
38
+
39
+ last_three_prices_arr = []
40
+ last_ticker_price = event_history.order_book[:ticker_price].to_f
41
+ second_to_last_ticker_price = event_history.order_book[:ticker_price_second_to_last].to_f
42
+ third_to_last_ticker_price = event_history.order_book[:ticker_price_third_to_last].to_f
43
+ last_three_prices_arr.push(last_ticker_price)
44
+ last_three_prices_arr.push(second_to_last_ticker_price)
45
+ last_three_prices_arr.push(third_to_last_ticker_price)
46
+ limit_price = last_three_prices_arr.sort[1]
47
+ return event_history unless limit_price.positive?
48
+
49
+ tpm = bot_conf[:target_profit_margin_percent].to_f
50
+ tpm_cast_as_decimal = tpm / 100
51
+
52
+ order_history_meta = event_history.order_book[:order_history_meta]
53
+ order_history = event_history.order_book[:order_history] if option_choice.autotrade
54
+
55
+ if event_history.order_book[:order_plan].length.positive?
56
+ if event_history.order_ready
57
+ event_history.order_book[:last_order_exec] = Time.now.strftime(
58
+ '%Y-%m-%d %H:%M:%S.%N%z'
59
+ )
60
+ end
61
+
62
+ # BUY
63
+ margin_percent_open_24h = (1 - (open_24h / ticker_price)) * 100
64
+ cast_margin_to_sec = margin_percent_open_24h * 0.1
65
+
66
+ # Reset times to max or default depending on
67
+ # bullish or bearish trend
68
+ if cast_margin_to_sec.positive?
69
+ event_history.bullish_trend = true
70
+ event_history.time_between_orders = event_history.time_between_orders_max
71
+ end
72
+
73
+ if cast_margin_to_sec.negative? &&
74
+ event_history.bullish_trend
75
+
76
+ event_history.bullish_trend = false
77
+ event_history.time_between_orders = event_history.time_between_orders_reset
78
+ end
79
+
80
+ if event_history.order_ready &&
81
+ indicator_status.action_signal == :buy &&
82
+ !event_history.red_pill
83
+
84
+ if option_choice.autotrade
85
+ this_order = event_history.order_book[:order_plan].first
86
+
87
+ price = format(
88
+ "%0.#{fiat_smallest_decimal}f",
89
+ limit_price
90
+ )
91
+
92
+ size = format(
93
+ "%0.#{crypto_smallest_size_to_buy}f",
94
+ this_order[:invest].to_f / limit_price
95
+ )
96
+
97
+ size = size.to_i.floor if base_increment.to_i >= 1
98
+
99
+ size = base_min_size if size.to_f < base_min_size.to_f
100
+
101
+ fiat_invested_this_order = size.to_f * price.to_f
102
+
103
+ fiat_portfolio = event_history.order_book[:fiat_portfolio]
104
+ fiat_balance = format('%0.2f', fiat_portfolio.first[:balance])
105
+ fiat_avail_for_trade = format('%0.2f', fiat_portfolio.first[:available])
106
+
107
+ event_history.red_pill = true if fiat_invested_this_order > fiat_avail_for_trade.to_f
108
+
109
+
110
+ unless event_history.red_pill
111
+ event_history = Cryptum::API.submit_limit_order(
112
+ option_choice: option_choice,
113
+ env: env,
114
+ price: price,
115
+ size: size,
116
+ buy_or_sell: :buy,
117
+ event_history: event_history,
118
+ bot_conf: bot_conf
119
+ )
120
+ end
121
+ else
122
+ this_order = event_history.order_book[:order_plan].shift
123
+ # Mock Order ID
124
+ this_order[:buy_order_id] = format(
125
+ '%0.6i',
126
+ Random.rand(0..999999)
127
+ )
128
+
129
+ this_order[:price] = limit_price.to_s
130
+ this_order[:size] = format(
131
+ "%0.#{crypto_smallest_size_to_buy}f",
132
+ this_order[:invest].to_f / limit_price
133
+ )
134
+
135
+ targ_price = limit_price + (limit_price * tpm_cast_as_decimal)
136
+ this_order[:tpm] = format(
137
+ '%0.2f',
138
+ tpm
139
+ )
140
+ this_order[:target_price] = format(
141
+ "%0.#{fiat_smallest_decimal}f",
142
+ targ_price
143
+ )
144
+ this_order[:color] = :cyan
145
+ order_history_meta.push(this_order)
146
+ end
147
+ # Increment n Seconds between buys to
148
+ # account for bearish and bullish trends
149
+ dynamic_time_increment = cast_margin_to_sec * -1
150
+
151
+ # Lets also take balance into play
152
+ balance_as_arbitrary_float = fiat_avail_for_trade.to_f / 1_000_000
153
+ tbo = dynamic_time_increment - balance_as_arbitrary_float
154
+
155
+ event_history.time_between_orders += tbo
156
+
157
+ # Time between orders should never
158
+ # be less than event_history.time_between_orders_min
159
+ event_history.time_between_orders = event_history.time_between_orders_min if event_history.time_between_orders < event_history.time_between_orders_min
160
+ # Time between orders should never
161
+ # be more than event_history.time_between_orders_max
162
+ event_history.time_between_orders = event_history.time_between_orders_max if event_history.time_between_orders > event_history.time_between_orders_max
163
+ end
164
+
165
+ # SELL
166
+ # Once buy arders are fulfilled submit a
167
+ # limit sell order for fulfillment
168
+ unless option_choice.autotrade
169
+ # Testing logic via Mock
170
+ event_type_arr = [:received, :open, :done]
171
+ last_et_index = event_type_arr.length - 1
172
+ rand_et_index = Random.rand(0..last_et_index)
173
+ event_type = event_type_arr[rand_et_index]
174
+
175
+ event_side_arr = [:buy, :sell]
176
+ last_es_index = event_side_arr.length - 1
177
+ rand_es_index = Random.rand(0..last_es_index)
178
+ event_side = event_type_arr[rand_es_index].to_s.to_sym
179
+ event_reason = 'mock'
180
+ end
181
+ end
182
+
183
+ # Update Completed Sell Orders w/ Green
184
+ if event_type == :open &&
185
+ event_side == :buy
186
+
187
+ buy_order_id = event_history.event[:order_id]
188
+ order_history_meta.each do |meta|
189
+ meta[:color] = :red if meta[:buy_order_id] == buy_order_id
190
+ end
191
+ end
192
+
193
+ if event_type == :done &&
194
+ event_side == :buy &&
195
+ event_reason == :canceled
196
+
197
+ buy_order_id = event_history.event[:order_id]
198
+ order_history_meta.each do |meta|
199
+ next unless meta[:buy_order_id] == buy_order_id
200
+ buy_done_at_hash_arr = order_history.select do |oh|
201
+ oh[:id] == meta[:buy_order_id]
202
+ end
203
+ meta[:done_at] = Time.now.strftime('%Y-%m-%d %H:%M:%S.%N%z')
204
+ meta[:color] = :black
205
+ end
206
+ end
207
+
208
+ if event_type == :done &&
209
+ event_side == :buy &&
210
+ event_reason != :canceled
211
+
212
+ if option_choice.autotrade
213
+ order_ready_to_sell_arr = order_history_meta.select do |meta|
214
+ meta[:buy_order_id] == event_history.event[:order_id]
215
+ end
216
+ else
217
+ last_index = order_history_meta.length - 1
218
+ rand_index = Random.rand(0..last_index)
219
+ order_ready_to_sell_arr = [
220
+ order_history_meta[rand_index]
221
+ ]
222
+ end
223
+
224
+ if order_ready_to_sell_arr.length.positive?
225
+ order_ready_to_sell = order_ready_to_sell_arr.first
226
+ buy_order_id = order_ready_to_sell[:buy_order_id]
227
+
228
+ if option_choice.autotrade
229
+ price = format(
230
+ "%0.#{fiat_smallest_decimal}f",
231
+ order_ready_to_sell[:target_price]
232
+ )
233
+
234
+ size = order_ready_to_sell[:size]
235
+
236
+ Cryptum::API.submit_limit_order(
237
+ option_choice: option_choice,
238
+ env: env,
239
+ price: price,
240
+ size: size,
241
+ buy_or_sell: :sell,
242
+ event_history: event_history,
243
+ bot_conf: bot_conf,
244
+ buy_order_id: buy_order_id
245
+ )
246
+ else
247
+ sell_order_id = format(
248
+ '%0.2i',
249
+ Random.rand(0..999999)
250
+ )
251
+
252
+ event_history.order_book[:order_history_meta].each do |meta|
253
+ if meta[:buy_order_id] == buy_order_id
254
+ meta[:sell_order_id] = sell_order_id
255
+ meta[:color] = :yellow
256
+ end
257
+ end
258
+ end
259
+ end
260
+ end
261
+
262
+ # Update Canceled Sell Orders w/ Black &&
263
+ # Include done_at Timestamp for 24h gain calc
264
+ if event_type == :done &&
265
+ event_side == :sell &&
266
+ event_reason == :canceled
267
+
268
+ sell_order_id = event_history.event[:order_id]
269
+ order_history_meta.each do |meta|
270
+ next unless meta[:sell_order_id] == sell_order_id
271
+ sell_done_at_hash_arr = order_history.select do |oh|
272
+ oh[:id] == meta[:sell_order_id]
273
+ end
274
+ meta[:done_at] = Time.now.strftime('%Y-%m-%d %H:%M:%S.%N%z')
275
+
276
+ # TODO: Retry sell order if the original sell order expires.
277
+
278
+ # Reinitiate GTFO if the previous GTFO Order Expires.
279
+ terminal_win.key_press_event.key_g = true if meta[:color] == :magenta
280
+ meta[:color] = :black
281
+ end
282
+ end
283
+
284
+ # Update Completed Sell Orders w/ Green &&
285
+ # Include done_at Timestamp for 24h gain calc
286
+ if event_type == :done &&
287
+ event_side == :sell &&
288
+ event_reason != :canceled
289
+
290
+ sell_order_id = event_history.event[:order_id]
291
+ order_history_meta.each do |meta|
292
+ next unless meta[:sell_order_id] == sell_order_id
293
+ sell_done_at_hash_arr = order_history.select do |oh|
294
+ oh[:id] == meta[:sell_order_id]
295
+ end
296
+ meta[:done_at] = Time.now.strftime('%Y-%m-%d %H:%M:%S.%N%z')
297
+ # meta[:done_at] = sell_done_at_hash_arr.first[:done_at] unless sell_done_at_hash_arr.empty?
298
+ meta[:color] = :green
299
+ end
300
+ end
301
+
302
+ # OK, now let's tally up everything...
303
+ twenty_four_hrs_ago = Time.now - 86400
304
+
305
+ # Snag all sold orders
306
+ oh_meta_sold_arr = order_history_meta.select do |ohm|
307
+ ohm[:color].to_sym == :green && ohm.key?(:done_at)
308
+ end
309
+
310
+ # Snag all sold orders within past 24 hrs
311
+ ohm_sold_twenty_four_arr = []
312
+ unless oh_meta_sold_arr.empty?
313
+ ohm_sold_twenty_four_arr = oh_meta_sold_arr.select do |o|
314
+ Time.parse(o[:done_at]) >= twenty_four_hrs_ago
315
+ end
316
+ end
317
+ order_hist_meta_sold = ohm_sold_twenty_four_arr.length
318
+
319
+ # Snag all expired orders
320
+ oh_meta_expired_arr = order_history_meta.select do |ohm|
321
+ ohm[:color].to_sym == :black && ohm.key?(:done_at)
322
+ end
323
+
324
+ # Snag all expired orders within past 24 hrs
325
+ ohm_expire_twenty_four_arr = []
326
+ unless oh_meta_expired_arr.empty?
327
+ ohm_expire_twenty_four_arr = oh_meta_expired_arr.select do |o|
328
+ Time.parse(o[:done_at]) >= twenty_four_hrs_ago
329
+ end
330
+ end
331
+ order_hist_meta_expired = ohm_expire_twenty_four_arr.length
332
+
333
+ # Calculate gains within past 24 hrs
334
+ gains_24h_sum = ohm_sold_twenty_four_arr.map do |ohms|
335
+ ohms[:profit].to_f
336
+ end.sum
337
+
338
+ gains_24h_out = Cryptum.beautify_large_number(
339
+ value: format(
340
+ '%0.2f',
341
+ gains_24h_sum
342
+ )
343
+ )
344
+
345
+ total_to_sell = order_history.select do |oh|
346
+ oh[:status].to_sym == :open &&
347
+ oh[:side].to_sym == :sell
348
+ end.length
349
+ total_to_sell = "#{total_to_sell}+" if total_to_sell == 1000
350
+
351
+ # TODO: Everything Above this Line Needs to be Indicators ^
352
+
353
+ # UI
354
+ col_just1 = (Curses.cols - Cryptum::UI.col_first) - 1
355
+ col_just3 = (Curses.cols - Cryptum::UI.col_third) - 1
356
+
357
+ Cryptum::UI.detect_key_press_in_ui(
358
+ key_press_event: key_press_event,
359
+ ui_win: order_execute_win
360
+ )
361
+
362
+ # ROW 1
363
+ out_line_no = 0
364
+ Cryptum::UI.line(
365
+ ui_win: order_execute_win,
366
+ out_line_no: out_line_no
367
+ )
368
+
369
+ # ROW 2
370
+ out_line_no += 1
371
+ order_execute_win.setpos(out_line_no, Cryptum::UI.col_first)
372
+ order_execute_win.clrtoeol
373
+ Cryptum::UI.colorize(
374
+ ui_win: order_execute_win,
375
+ color: :white,
376
+ style: :bold,
377
+ string: 'Open Sell Orders | 24 Hr Stats:'
378
+ )
379
+
380
+ order_execute_win.setpos(out_line_no, Cryptum::UI.col_third)
381
+ Cryptum::UI.colorize(
382
+ ui_win: order_execute_win,
383
+ color: :white,
384
+ string: "#{total_to_sell} | #{order_hist_meta_expired} Expired | #{order_hist_meta_sold} Sold | $#{gains_24h_out} Gained".rjust(
385
+ col_just3,
386
+ '.'
387
+ )
388
+ )
389
+
390
+ # ROW 3
391
+ out_line_no += 1
392
+ Cryptum::UI.line(
393
+ ui_win: order_execute_win,
394
+ out_line_no: out_line_no
395
+ )
396
+
397
+
398
+ # ROWS 4-10
399
+ order_history_meta.reverse[0..6].each do |meta|
400
+ out_line_no += 1
401
+ style = :normal
402
+ style = :highlight if out_line_no == 1
403
+ risk_alloc_out = Cryptum.beautify_large_number(
404
+ value: meta[:risk_alloc]
405
+ )
406
+ invest_out = Cryptum.beautify_large_number(
407
+ value: meta[:invest]
408
+ )
409
+ price_out = Cryptum.beautify_large_number(
410
+ value: meta[:price]
411
+ )
412
+ size_out = Cryptum.beautify_large_number(
413
+ value: meta[:size]
414
+ )
415
+ target_price_out = Cryptum.beautify_large_number(
416
+ value: meta[:target_price]
417
+ )
418
+ profit_out = Cryptum.beautify_large_number(
419
+ value: meta[:profit]
420
+ )
421
+ plan_no = "#{meta[:plan_no]}|"
422
+
423
+ buy_created_at_hash_arr = order_history.select do |oh|
424
+ oh[:id] == meta[:buy_order_id]
425
+ end
426
+
427
+ buy_created_at = '__-__ __:__:__'
428
+ unless buy_created_at_hash_arr.empty?
429
+ buy_created_at = Time.parse(
430
+ buy_created_at_hash_arr.first[:created_at]
431
+ ).strftime('%m-%d %H:%M:%S')
432
+ end
433
+
434
+ invest = "$#{invest_out} @ "
435
+ tick = "$#{price_out} = "
436
+ size = "*#{size_out} + "
437
+ tpm_out = "#{meta[:tpm]}% = "
438
+ targ_tick = "$#{target_price_out}"
439
+ profit = "|Profit: $#{meta[:profit]}"
440
+
441
+ order_exec_ln = "#{plan_no}#{buy_created_at}|#{invest}#{tick}#{size}#{tpm_out}#{targ_tick}#{profit}"
442
+
443
+ order_execute_win.setpos(out_line_no, Cryptum::UI.col_first)
444
+ order_execute_win.clrtoeol
445
+ Cryptum::UI.colorize(
446
+ ui_win: order_execute_win,
447
+ color: meta[:color],
448
+ style: style,
449
+ string: order_exec_ln.ljust(col_just1, '.')
450
+ )
451
+ end
452
+
453
+ order_execute_win.refresh
454
+
455
+ # Reset Order Ready Boolean
456
+ event_history.order_ready = false
457
+
458
+ event_history
459
+ rescue Interrupt
460
+ # Exit Gracefully if CTRL+C is Pressed During Session
461
+ Cryptum.exit_gracefully(which_self: self)
462
+ rescue StandardError => e
463
+ raise e
464
+ end
465
+
466
+ # Display Usage for this Module
467
+
468
+ public_class_method def self.help
469
+ puts "USAGE:
470
+ #{self}.refresh(
471
+ order_book: 'required - Order Book Data Structure',
472
+ event: 'required - Event from Coinbase Web Socket'
473
+ )
474
+ "
475
+ end
476
+ end
477
+ end
478
+ end