cryptum 0.0.381 → 0.0.383

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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/.rubocop_todo.yml +11 -8
  4. data/Gemfile +1 -2
  5. data/bin/cryptum +2 -2
  6. data/lib/cryptum/api/exchange_rates.rb +0 -2
  7. data/lib/cryptum/api/orders.rb +9 -7
  8. data/lib/cryptum/api/portfolio.rb +0 -2
  9. data/lib/cryptum/api/rest.rb +3 -4
  10. data/lib/cryptum/api/signature.rb +0 -4
  11. data/lib/cryptum/api.rb +1 -40
  12. data/lib/cryptum/bot_conf.rb +14 -6
  13. data/lib/cryptum/event/exit.rb +1 -1
  14. data/lib/cryptum/event/history.rb +4 -1
  15. data/lib/cryptum/event/parse.rb +6 -6
  16. data/lib/cryptum/event.rb +0 -2
  17. data/lib/cryptum/log.rb +18 -4
  18. data/lib/cryptum/open_ai.rb +129 -32
  19. data/lib/cryptum/option/choice.rb +1 -1
  20. data/lib/cryptum/option/environment.rb +0 -2
  21. data/lib/cryptum/option/parser.rb +0 -2
  22. data/lib/cryptum/order_book/generate.rb +3 -8
  23. data/lib/cryptum/order_book.rb +0 -3
  24. data/lib/cryptum/portfolio.rb +0 -2
  25. data/lib/cryptum/ui/command.rb +1 -3
  26. data/lib/cryptum/ui/exit.rb +43 -0
  27. data/lib/cryptum/ui/key_press_event.rb +1 -1
  28. data/lib/cryptum/ui/market_trend.rb +4 -3
  29. data/lib/cryptum/ui/matrix.rb +185 -0
  30. data/lib/cryptum/ui/order/execute.rb +629 -0
  31. data/lib/cryptum/ui/order/execute_details.rb +300 -0
  32. data/lib/cryptum/ui/order/plan.rb +518 -0
  33. data/lib/cryptum/ui/order/plan_details.rb +243 -0
  34. data/lib/cryptum/ui/order/timer.rb +140 -0
  35. data/lib/cryptum/ui/order.rb +21 -0
  36. data/lib/cryptum/ui/portfolio.rb +5 -3
  37. data/lib/cryptum/ui/signal_engine.rb +1 -3
  38. data/lib/cryptum/ui/terminal_window.rb +1 -3
  39. data/lib/cryptum/ui/ticker.rb +4 -3
  40. data/lib/cryptum/ui.rb +17 -22
  41. data/lib/cryptum/version.rb +1 -1
  42. data/lib/cryptum/web_sock/coinbase.rb +1 -6
  43. data/lib/cryptum/web_sock/event_machine.rb +3 -7
  44. data/lib/cryptum.rb +16 -33
  45. data/spec/lib/cryptum/{matrix_spec.rb → ui/exit_spec.rb} +2 -2
  46. data/spec/lib/cryptum/ui/{order_plan_spec.rb → matrix_spec.rb} +2 -2
  47. data/spec/lib/cryptum/ui/{order_execute_details_spec.rb → order/execute_details_spec.rb} +2 -2
  48. data/spec/lib/cryptum/ui/{order_execution_spec.rb → order/execute_spec.rb} +2 -2
  49. data/spec/lib/cryptum/ui/{order_plan_details_spec.rb → order/plan_details_spec.rb} +2 -2
  50. data/spec/lib/cryptum/ui/order/plan_spec.rb +10 -0
  51. data/spec/lib/cryptum/ui/order/timer_spec.rb +10 -0
  52. data/spec/lib/cryptum/ui/{order_timer_spec.rb → order_spec.rb} +2 -2
  53. metadata +20 -30
  54. data/lib/cryptum/matrix.rb +0 -181
  55. data/lib/cryptum/ui/order_execute_details.rb +0 -297
  56. data/lib/cryptum/ui/order_execution.rb +0 -624
  57. data/lib/cryptum/ui/order_plan.rb +0 -514
  58. data/lib/cryptum/ui/order_plan_details.rb +0 -240
  59. data/lib/cryptum/ui/order_timer.rb +0 -136
@@ -0,0 +1,518 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cryptum
4
+ # This plugin is used to Refresh the Cryptum console UI
5
+ module UI
6
+ module Order
7
+ module Plan
8
+ # Supported Method Parameters::
9
+ # Cryptum::UI::Order::Plan.refresh(
10
+ # order_book: 'required - Order Book Data Structure',
11
+ # event: 'required - Event from Coinbase Web Socket'
12
+ # )
13
+
14
+ public_class_method def self.refresh(opts = {})
15
+ option_choice = opts[:option_choice]
16
+ order_plan_win = opts[:order_plan_win]
17
+ event_history = opts[:event_history]
18
+ indicator_status = opts[:indicator_status]
19
+ bot_conf = opts[:bot_conf]
20
+
21
+ ticker_price = event_history.order_book[:ticker_price].to_f
22
+ return event_history unless ticker_price.positive?
23
+
24
+ market_trend_color = plan_color = :white
25
+
26
+ this_product = event_history.order_book[:this_product]
27
+ base_increment = this_product[:base_increment]
28
+ min_market_funds = this_product[:min_market_funds]
29
+
30
+ crypto_smallest_decimal = base_increment.to_s.split('.')[-1].length
31
+
32
+ autotrade_percent = bot_conf[:autotrade_portfolio_percent].to_f
33
+ autotrade_cast_as_decimal = autotrade_percent / 100
34
+
35
+ tpm = bot_conf[:target_profit_margin_percent].to_f
36
+ tpm_cast_as_decimal = tpm / 100
37
+
38
+ crypto_currency = option_choice.symbol.to_s.upcase.split('_').first.to_sym
39
+ portfolio = event_history.order_book[:portfolio]
40
+ this_account = portfolio.select do |account|
41
+ account[:currency] == crypto_currency.to_s
42
+ end
43
+ raise "ID for Crypto Currency, #{crypto_currency} Not Found" if this_account.empty?
44
+
45
+ balance = format("%0.#{crypto_smallest_decimal}f", this_account.first[:balance])
46
+ fiat_portfolio = event_history.order_book[:fiat_portfolio]
47
+ total_holdings = format('%0.2f', fiat_portfolio.first[:total_holdings])
48
+ fiat_avail_for_trade = format('%0.2f', fiat_portfolio.first[:available])
49
+
50
+ fiat_budget = fiat_avail_for_trade.to_f
51
+
52
+ order_plan = event_history.order_book[:order_plan]
53
+
54
+ current_crypto_fiat_value = format(
55
+ '%0.2f',
56
+ balance.to_f * ticker_price
57
+ )
58
+
59
+ crypto_invested_percent = format(
60
+ '%0.2f',
61
+ current_crypto_fiat_value.to_f.fdiv(total_holdings.to_f) * 100
62
+ )
63
+
64
+ event_history.red_pill = true if crypto_invested_percent.to_f > autotrade_percent
65
+
66
+ # SAUCE 2
67
+ # Generating and/or recalculating order plan
68
+ if event_history.recalculate_order_plan || (
69
+ crypto_invested_percent.to_f <= autotrade_percent &&
70
+ autotrade_percent.positive? && (
71
+ order_plan.empty? || (
72
+ indicator_status.action_signal == :buy &&
73
+ indicator_status.last_action_signal == :sell
74
+ )
75
+ )
76
+ )
77
+
78
+ event_history.red_pill = false
79
+ if indicator_status.action_signal == :buy &&
80
+ indicator_status.last_action_signal == :sell
81
+
82
+ order_plan = []
83
+ # Reset time between orders
84
+ event_history.time_between_orders = event_history.time_between_orders_reset
85
+ event_history.plan_no += 1
86
+ end
87
+
88
+ plan_no_slice = 1
89
+ plan_no = event_history.plan_no
90
+ # Sum up currently invested amount so we don't
91
+ # exceed autotrade % for next order plan generation
92
+ allocation_decimal = 0.0
93
+ risk_target = fiat_budget * autotrade_cast_as_decimal
94
+ risk_alloc = risk_target
95
+
96
+ # TODO: Order Plan <= event_history.max_open_sell_orders
97
+ previous_slice_fiat_investing = 0.00
98
+ last_slice_detected = false
99
+ loop do
100
+ # Calculate min order size
101
+ allocation_decimal += 0.0001
102
+ fiat_investing = risk_alloc * allocation_decimal
103
+
104
+ next unless fiat_investing > min_market_funds.to_f
105
+
106
+ # Increase amount invested in each slice as we increment
107
+ # plan numbers (i.e. slices) in the order plan cycle
108
+ slice_alloc = fiat_budget * allocation_decimal
109
+ fiat_investing = slice_alloc + previous_slice_fiat_investing
110
+
111
+ risk_alloc = fiat_budget * autotrade_cast_as_decimal
112
+ allocation_percent = allocation_decimal * 100
113
+
114
+ break if order_plan.map { |op| op[:invest].to_f }.sum > risk_target ||
115
+ allocation_percent > 100 ||
116
+ last_slice_detected
117
+
118
+ next unless fiat_budget >= min_market_funds.to_f
119
+
120
+ if fiat_investing > fiat_budget
121
+ last_slice_detected = true
122
+ fiat_investing = fiat_budget
123
+ end
124
+
125
+ fiat_returning = fiat_investing + (
126
+ fiat_investing * tpm_cast_as_decimal
127
+ )
128
+ profit = fiat_returning - fiat_investing
129
+
130
+ this_plan_no = "#{plan_no}.#{plan_no_slice}"
131
+ if event_history.recalculate_order_plan
132
+ order_slice = order_plan.find do |order|
133
+ order[:plan_no] == this_plan_no
134
+ end
135
+ end
136
+ order_slice ||= {}
137
+ order_slice[:plan_no] = this_plan_no
138
+ order_slice[:fiat_available] = format('%0.2f', fiat_budget)
139
+ order_slice[:risk_alloc] = format('%0.2f', risk_alloc)
140
+ order_slice[:allocation_decimal] = format(
141
+ '%0.8f',
142
+ allocation_decimal
143
+ )
144
+ order_slice[:allocation_percent] = format(
145
+ '%0.2f',
146
+ allocation_percent
147
+ )
148
+ order_slice[:slice_alloc] = format('%0.2f', slice_alloc)
149
+ order_slice[:risk_target] = format('%0.2f', risk_target)
150
+ order_slice[:previous_slice_invest] = format(
151
+ '%0.2f',
152
+ previous_slice_fiat_investing
153
+ )
154
+ order_slice[:invest] = format('%0.2f', fiat_investing)
155
+ order_slice[:return] = format('%0.2f', fiat_returning)
156
+ order_slice[:profit] = format('%0.2f', profit)
157
+ order_slice[:tpm] = tpm
158
+ order_slice[:autotrade_percent] = autotrade_percent
159
+ order_slice[:color] = plan_color
160
+ order_slice[:last_slice] = false
161
+
162
+ order_plan.push(order_slice) unless event_history.recalculate_order_plan
163
+ fiat_budget -= fiat_investing
164
+ previous_slice_fiat_investing = fiat_investing
165
+ plan_no_slice += 1
166
+ end
167
+ order_plan.last[:last_slice] = true if order_plan.any?
168
+ op_last_slice_invest = order_plan.last[:invest].to_f if order_plan.any?
169
+ order_plan.last[:invest] = fiat_budget if order_plan.any? &&
170
+ op_last_slice_invest > fiat_budget &&
171
+ fiat_budget > min_market_funds.to_f
172
+
173
+ event_history.order_book[:order_plan] = order_plan
174
+ end
175
+
176
+ red_pill_alert = '| RED PILL ALERT |'
177
+ red_pill_msg = '- Autotrade % Exhausted -'
178
+ event_history.red_pill = true if order_plan.empty?
179
+
180
+ if event_history.red_pill
181
+ order_plan_prefix = '--'
182
+ # max_order_plan_slices = '0'
183
+ order_plan_volume_out = '0.00'
184
+ order_plan_profit_sum_out = '0.00'
185
+ order_plan_exec_percent = '0.00'
186
+ else
187
+ order_plan_len = order_plan.length
188
+ order_plan_prefix = order_plan.last[:plan_no].split('.').first
189
+ max_order_plan_slices = order_plan.last[:plan_no].split('.').last.to_i
190
+ calc_order_plan_exec = (
191
+ 1 - (order_plan_len / max_order_plan_slices.to_f)
192
+ ) * 100
193
+ order_plan_volume = order_plan.map do |op|
194
+ op[:invest].to_f
195
+ end.sum
196
+ order_plan_volume_out = Cryptum.beautify_large_number(
197
+ value: format(
198
+ '%0.2f',
199
+ order_plan_volume
200
+ )
201
+ )
202
+ order_plan_profit_sum = order_plan.map do |op|
203
+ op[:profit].to_f
204
+ end.sum
205
+ order_plan_profit_sum_out = Cryptum.beautify_large_number(
206
+ value: format(
207
+ '%0.2f',
208
+ order_plan_profit_sum
209
+ )
210
+ )
211
+ order_plan_exec_percent = format('%0.2f', calc_order_plan_exec)
212
+ end
213
+
214
+ # TODO: Everything Above this Line Needs to be Indicators ^
215
+
216
+ # UI
217
+ col_just1 = (Curses.cols - Cryptum::UI.col_first) - 1
218
+ col_just4 = Curses.cols - Cryptum::UI.col_fourth
219
+
220
+ # ROW 1
221
+ out_line_no = 0
222
+ line_color = :white
223
+ header_color = :white
224
+ header_style = :bold
225
+ style = :bold
226
+ if event_history.order_plan_win_active
227
+ line_color = :blue
228
+ header_color = :blue
229
+ header_style = :reverse
230
+ end
231
+
232
+ Cryptum::UI.line(
233
+ ui_win: order_plan_win,
234
+ out_line_no: out_line_no,
235
+ color: line_color
236
+ )
237
+
238
+ # ROW 2
239
+ out_line_no += 1
240
+ order_plan_win.setpos(out_line_no, Cryptum::UI.col_first)
241
+ order_plan_win.clrtoeol
242
+ Cryptum::UI.colorize(
243
+ ui_win: order_plan_win,
244
+ color: header_color,
245
+ style: header_style,
246
+ string: ''.ljust(col_just1, ' ')
247
+ )
248
+
249
+ header_str = "- ORDER PLAN | CYCLE ##{order_plan_prefix} -"
250
+ header_str = '- ORDER PLAN | UNAVAILABLE -' if event_history.red_pill
251
+ order_plan_win.setpos(
252
+ out_line_no,
253
+ Cryptum::UI.col_center(str: header_str)
254
+ )
255
+
256
+ Cryptum::UI.colorize(
257
+ ui_win: order_plan_win,
258
+ color: header_color,
259
+ style: header_style,
260
+ string: header_str
261
+ )
262
+
263
+ order_plan_win.setpos(out_line_no, Cryptum::UI.col_fourth)
264
+ Cryptum::UI.colorize(
265
+ ui_win: order_plan_win,
266
+ color: header_color,
267
+ style: header_style,
268
+ string: ''.ljust(col_just4, ' ')
269
+ )
270
+
271
+ # ROWS 3-10
272
+ remaining_blank_rows = 0
273
+ max_rows_to_display = event_history.order_plan_max_rows_to_display
274
+ remaining_blank_rows = max_rows_to_display if order_plan.empty?
275
+ if event_history.red_pill
276
+ out_line_no += 1
277
+ order_plan_win.setpos(out_line_no, Cryptum::UI.col_first)
278
+ order_plan_win.clrtoeol
279
+
280
+ Cryptum::UI.colorize(
281
+ ui_win: order_plan_win,
282
+ color: :red,
283
+ style: :highlight,
284
+ string: ''.ljust(col_just1, ' ')
285
+ )
286
+
287
+ order_plan_win.setpos(
288
+ out_line_no,
289
+ Cryptum::UI.col_center(str: red_pill_alert)
290
+ )
291
+ Cryptum::UI.colorize(
292
+ ui_win: order_plan_win,
293
+ color: :red,
294
+ style: :highlight,
295
+ string: red_pill_alert
296
+ )
297
+
298
+ order_plan_win.setpos(out_line_no, Cryptum::UI.col_fourth)
299
+ Cryptum::UI.colorize(
300
+ ui_win: order_plan_win,
301
+ color: :red,
302
+ style: :highlight,
303
+ string: ''.ljust(col_just4, ' ')
304
+ )
305
+
306
+ out_line_no += 1
307
+ order_plan_win.setpos(out_line_no, Cryptum::UI.col_first)
308
+ order_plan_win.clrtoeol
309
+
310
+ Cryptum::UI.colorize(
311
+ ui_win: order_plan_win,
312
+ color: :yellow,
313
+ string: ''.ljust(col_just1, ' ')
314
+ )
315
+
316
+ order_plan_win.setpos(
317
+ out_line_no,
318
+ Cryptum::UI.col_center(str: red_pill_msg)
319
+ )
320
+ Cryptum::UI.colorize(
321
+ ui_win: order_plan_win,
322
+ color: :yellow,
323
+ style: :bold,
324
+ string: red_pill_msg
325
+ )
326
+
327
+ order_plan_win.setpos(out_line_no, Cryptum::UI.col_fourth)
328
+ Cryptum::UI.colorize(
329
+ ui_win: order_plan_win,
330
+ color: :yellow,
331
+ string: ''.ljust(col_just4, ' ')
332
+ )
333
+
334
+ max_rows_to_display.times.each do
335
+ out_line_no += 1
336
+ this_matrix_row = Cryptum::UI::Matrix.generate(cols: Curses.cols)
337
+ order_plan_win.setpos(out_line_no, Cryptum::UI.col_first)
338
+ order_plan_win.clrtoeol
339
+ Cryptum::UI.colorize(
340
+ ui_win: order_plan_win,
341
+ color: :red,
342
+ style: :bold,
343
+ string: this_matrix_row
344
+ )
345
+ end
346
+ else
347
+ market_trend_color = indicator_status.market_trend[:color] if indicator_status.market_trend
348
+ plan_color = :cyan if market_trend_color == :red
349
+ plan_color = :green if market_trend_color == :green
350
+
351
+ # ROWS 3-10
352
+ max_rows_to_display = event_history.order_plan_max_rows_to_display
353
+ first_row = event_history.order_plan_index_offset
354
+ last_row = first_row + max_rows_to_display
355
+ if last_row >= order_plan.length
356
+ last_row = order_plan.length - 1
357
+ event_history.order_plan_max_records_available_to_display = last_row if last_row < max_rows_to_display
358
+ first_row = last_row - event_history.order_plan_max_records_available_to_display
359
+ event_history.order_plan_index_offset = first_row
360
+ remaining_blank_rows = max_rows_to_display - last_row
361
+ end
362
+
363
+ selected_order = event_history.order_plan_selected_data
364
+ order_plan[first_row..last_row].each do |order|
365
+ out_line_no += 1
366
+ current_line = out_line_no - 2
367
+
368
+ style = :normal
369
+ if event_history.order_plan_row_to_select == current_line
370
+ style = :highlight
371
+ selected_order = order
372
+ selected_order[:color] = plan_color
373
+ end
374
+
375
+ fiat_avail_out = Cryptum.beautify_large_number(
376
+ value: order[:fiat_available]
377
+ )
378
+ risk_alloc_out = Cryptum.beautify_large_number(
379
+ value: order[:risk_alloc]
380
+ )
381
+ slice_alloc_out = Cryptum.beautify_large_number(
382
+ value: order[:slice_alloc]
383
+ )
384
+ previous_slice_invest_out = Cryptum.beautify_large_number(
385
+ value: order[:previous_slice_invest]
386
+ )
387
+ invest_out = Cryptum.beautify_large_number(
388
+ value: order[:invest]
389
+ )
390
+ profit_out = Cryptum.beautify_large_number(
391
+ value: order[:profit]
392
+ )
393
+ tpm_out = format('%0.2f', tpm)
394
+ return_out = Cryptum.beautify_large_number(
395
+ value: order[:return]
396
+ )
397
+ plan_no = "#{order[:plan_no]}|"
398
+ fiat = "#{autotrade_percent}% of $#{fiat_avail_out} = "
399
+ alloc = "$#{risk_alloc_out} @ #{order[:allocation_percent]}% = "
400
+ alloc = "$#{risk_alloc_out}" if order[:last_slice]
401
+ invest = "$#{slice_alloc_out} + $#{previous_slice_invest_out} = $#{invest_out} + #{tpm_out}% = "
402
+ invest = " + #{tpm_out}% = " if order[:last_slice]
403
+
404
+ returns = "$#{return_out}|"
405
+ profit = "Profit: $#{profit_out}"
406
+
407
+ order_plan_invest = "#{plan_no}#{fiat}#{alloc}#{invest}"
408
+ order_plan_return = "#{returns}#{profit}"
409
+
410
+ order_plan_win.setpos(out_line_no, Cryptum::UI.col_first)
411
+ order_plan_win.clrtoeol
412
+ Cryptum::UI.colorize(
413
+ ui_win: order_plan_win,
414
+ color: plan_color,
415
+ style: style,
416
+ string: "#{order_plan_invest}#{order_plan_return}".ljust(col_just1, '.')
417
+ )
418
+
419
+ Cryptum::UI.colorize(
420
+ ui_win: order_plan_win,
421
+ color: plan_color,
422
+ style: style,
423
+ string: ''.ljust(col_just4, ' ')
424
+ )
425
+ end
426
+ event_history.order_plan_selected_data = selected_order
427
+
428
+ # Clear to SUMMARY
429
+ # (Only Applicable if order_book[:order_plan] < max_rows_to_display)
430
+ # out_line_no += 1
431
+ if remaining_blank_rows.positive?
432
+ rows_to_blank = (out_line_no + remaining_blank_rows)
433
+ out_line_no += 1
434
+ (out_line_no..rows_to_blank).each do |clr_line|
435
+ out_line_no = clr_line
436
+ order_plan_win.setpos(clr_line, Cryptum::UI.col_first)
437
+ order_plan_win.clrtoeol
438
+ Cryptum::UI.colorize(
439
+ ui_win: order_plan_win,
440
+ color: :white,
441
+ style: :normal,
442
+ string: ''.ljust(col_just1, ' ')
443
+ )
444
+ end
445
+ end
446
+
447
+ # ROW 10
448
+ out_line_no += 1
449
+ order_plan_win.setpos(out_line_no, Cryptum::UI.col_first)
450
+ order_plan_win.clrtoeol
451
+ Cryptum::UI.colorize(
452
+ ui_win: order_plan_win,
453
+ color: header_color,
454
+ style: header_style,
455
+ string: ''.ljust(col_just1, ' ')
456
+ )
457
+
458
+ header_str = "RISK ALLOCATED: $#{order_plan_volume_out} | "
459
+ header_str += "PROJECTED PROFIT: $#{order_plan_profit_sum_out} | "
460
+ header_str += "#{order_plan_exec_percent}% DONE"
461
+ order_plan_win.setpos(
462
+ out_line_no,
463
+ Cryptum::UI.col_center(str: header_str)
464
+ )
465
+
466
+ Cryptum::UI.colorize(
467
+ ui_win: order_plan_win,
468
+ color: header_color,
469
+ style: header_style,
470
+ string: header_str
471
+ )
472
+
473
+ # order_plan_win.setpos(out_line_no, Cryptum::UI.col_fourth)
474
+ # order_plan_win.clrtoeol
475
+ # Cryptum::UI.colorize(
476
+ # ui_win: order_plan_win,
477
+ # color: header_color,
478
+ # style: header_style,
479
+ # string: ''.ljust(col_just4, ' ')
480
+ # )
481
+ end
482
+
483
+ # ROW 11
484
+ out_line_no += 1
485
+ Cryptum::UI.line(
486
+ ui_win: order_plan_win,
487
+ out_line_no: out_line_no,
488
+ color: line_color
489
+ )
490
+
491
+ order_plan_win.refresh
492
+
493
+ event_history
494
+ rescue Interrupt
495
+ # Exit Gracefully if CTRL+C is Pressed During Session
496
+ Cryptum::UI::Exit.gracefully(
497
+ which_self: self,
498
+ event_history: event_history,
499
+ option_choice: option_choice
500
+ )
501
+ rescue StandardError => e
502
+ raise e
503
+ end
504
+
505
+ # Display Usage for this Module
506
+
507
+ public_class_method def self.help
508
+ puts "USAGE:
509
+ #{self}.refresh(
510
+ order_book: 'required - Order Book Data Structure',
511
+ event: 'required - Event from Coinbase Web Socket'
512
+ )
513
+ "
514
+ end
515
+ end
516
+ end
517
+ end
518
+ end