buda_api 1.0.0 → 1.0.1

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.
@@ -0,0 +1,1054 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BudaApi
4
+ module AI
5
+ # AI-powered report generation for trading analysis
6
+ class ReportGenerator
7
+ REPORT_TYPES = %w[
8
+ portfolio_summary
9
+ trading_performance
10
+ market_analysis
11
+ risk_assessment
12
+ profit_loss
13
+ tax_report
14
+ custom
15
+ ].freeze
16
+
17
+ EXPORT_FORMATS = %w[text markdown html json csv].freeze
18
+
19
+ def initialize(client, llm_provider: :openai)
20
+ @client = client
21
+ @llm = RubyLLM.new(
22
+ provider: llm_provider,
23
+ system_prompt: build_report_system_prompt
24
+ )
25
+
26
+ BudaApi::Logger.info("Report Generator initialized")
27
+ end
28
+
29
+ # Generate comprehensive portfolio summary report
30
+ #
31
+ # @param options [Hash] report options
32
+ # @option options [String] :format export format (text, markdown, html, json, csv)
33
+ # @option options [Boolean] :include_charts include visual elements
34
+ # @option options [Date] :start_date analysis start date
35
+ # @option options [Date] :end_date analysis end date
36
+ # @return [Hash] generated report
37
+ def generate_portfolio_summary(options = {})
38
+ format = options[:format] || "markdown"
39
+ include_ai = options[:include_ai] != false
40
+
41
+ BudaApi::Logger.info("Generating portfolio summary report")
42
+
43
+ begin
44
+ # Gather portfolio data
45
+ balances_result = @client.balances
46
+ portfolios = extract_portfolio_data(balances_result)
47
+
48
+ return empty_portfolio_report(format) if portfolios.empty?
49
+
50
+ # Get market data
51
+ market_data = fetch_portfolio_market_data(portfolios)
52
+
53
+ # Calculate metrics
54
+ portfolio_metrics = calculate_portfolio_metrics(portfolios, market_data)
55
+
56
+ # Generate base report
57
+ report_data = {
58
+ type: :portfolio_summary,
59
+ generated_at: Time.now,
60
+ portfolio: portfolios,
61
+ market_data: market_data,
62
+ metrics: portfolio_metrics,
63
+ summary: {
64
+ total_value: portfolio_metrics[:total_value_clp],
65
+ asset_count: portfolios.length,
66
+ top_holding: portfolio_metrics[:top_holding],
67
+ total_change_24h: portfolio_metrics[:total_change_24h]
68
+ }
69
+ }
70
+
71
+ # Add AI analysis if requested
72
+ if include_ai
73
+ report_data[:ai_analysis] = generate_ai_portfolio_analysis(report_data)
74
+ end
75
+
76
+ # Format the report
77
+ formatted_report = format_report(report_data, format)
78
+
79
+ {
80
+ type: :portfolio_summary_report,
81
+ format: format,
82
+ data: report_data,
83
+ formatted_content: formatted_report,
84
+ timestamp: Time.now
85
+ }
86
+
87
+ rescue => e
88
+ error_msg = "Portfolio summary generation failed: #{e.message}"
89
+ BudaApi::Logger.error(error_msg)
90
+
91
+ {
92
+ type: :report_error,
93
+ error: error_msg,
94
+ timestamp: Time.now
95
+ }
96
+ end
97
+ end
98
+
99
+ # Generate trading performance report
100
+ #
101
+ # @param market_id [String] specific market or 'all'
102
+ # @param options [Hash] report options
103
+ # @return [Hash] trading performance report
104
+ def generate_trading_performance(market_id = 'all', options = {})
105
+ format = options[:format] || "markdown"
106
+ limit = options[:limit] || 50
107
+ include_ai = options[:include_ai] != false
108
+
109
+ BudaApi::Logger.info("Generating trading performance report for #{market_id}")
110
+
111
+ begin
112
+ # Get trading history
113
+ trading_data = fetch_trading_history(market_id, limit)
114
+
115
+ return empty_trading_report(format) if trading_data.empty?
116
+
117
+ # Calculate performance metrics
118
+ performance_metrics = calculate_performance_metrics(trading_data)
119
+
120
+ # Generate report data
121
+ report_data = {
122
+ type: :trading_performance,
123
+ generated_at: Time.now,
124
+ market_id: market_id,
125
+ period: {
126
+ trades_analyzed: trading_data.length,
127
+ date_range: get_date_range(trading_data)
128
+ },
129
+ trades: trading_data,
130
+ performance: performance_metrics
131
+ }
132
+
133
+ # Add AI insights
134
+ if include_ai
135
+ report_data[:ai_insights] = generate_ai_trading_insights(report_data)
136
+ end
137
+
138
+ # Format report
139
+ formatted_report = format_report(report_data, format)
140
+
141
+ {
142
+ type: :trading_performance_report,
143
+ format: format,
144
+ data: report_data,
145
+ formatted_content: formatted_report,
146
+ timestamp: Time.now
147
+ }
148
+
149
+ rescue => e
150
+ error_msg = "Trading performance report failed: #{e.message}"
151
+ BudaApi::Logger.error(error_msg)
152
+
153
+ {
154
+ type: :report_error,
155
+ error: error_msg,
156
+ timestamp: Time.now
157
+ }
158
+ end
159
+ end
160
+
161
+ # Generate market analysis report
162
+ #
163
+ # @param markets [Array<String>] markets to analyze
164
+ # @param options [Hash] report options
165
+ # @return [Hash] market analysis report
166
+ def generate_market_analysis(markets = nil, options = {})
167
+ markets ||= BudaApi::Constants::Market::MAJOR
168
+ format = options[:format] || "markdown"
169
+ include_ai = options[:include_ai] != false
170
+
171
+ BudaApi::Logger.info("Generating market analysis report for #{markets.length} markets")
172
+
173
+ begin
174
+ # Gather market data
175
+ market_analysis = {}
176
+
177
+ markets.each do |market_id|
178
+ market_analysis[market_id] = analyze_single_market(market_id)
179
+ end
180
+
181
+ # Calculate cross-market metrics
182
+ market_metrics = calculate_market_metrics(market_analysis)
183
+
184
+ # Generate report data
185
+ report_data = {
186
+ type: :market_analysis,
187
+ generated_at: Time.now,
188
+ markets_analyzed: markets.length,
189
+ markets: market_analysis,
190
+ metrics: market_metrics,
191
+ summary: generate_market_summary(market_analysis, market_metrics)
192
+ }
193
+
194
+ # Add AI market insights
195
+ if include_ai
196
+ report_data[:ai_insights] = generate_ai_market_insights(report_data)
197
+ end
198
+
199
+ # Format report
200
+ formatted_report = format_report(report_data, format)
201
+
202
+ {
203
+ type: :market_analysis_report,
204
+ format: format,
205
+ data: report_data,
206
+ formatted_content: formatted_report,
207
+ timestamp: Time.now
208
+ }
209
+
210
+ rescue => e
211
+ error_msg = "Market analysis report failed: #{e.message}"
212
+ BudaApi::Logger.error(error_msg)
213
+
214
+ {
215
+ type: :report_error,
216
+ error: error_msg,
217
+ timestamp: Time.now
218
+ }
219
+ end
220
+ end
221
+
222
+ # Generate custom report with AI assistance
223
+ #
224
+ # @param prompt [String] custom report requirements
225
+ # @param data_sources [Array<Symbol>] data to include (:portfolio, :trades, :market)
226
+ # @param options [Hash] report options
227
+ # @return [Hash] custom report
228
+ def generate_custom_report(prompt, data_sources = [:portfolio], options = {})
229
+ format = options[:format] || "markdown"
230
+
231
+ BudaApi::Logger.info("Generating custom report: #{prompt}")
232
+
233
+ begin
234
+ # Gather requested data
235
+ gathered_data = {}
236
+
237
+ if data_sources.include?(:portfolio)
238
+ balances_result = @client.balances
239
+ gathered_data[:portfolio] = extract_portfolio_data(balances_result)
240
+ end
241
+
242
+ if data_sources.include?(:trades)
243
+ gathered_data[:trades] = fetch_recent_trades_all_markets
244
+ end
245
+
246
+ if data_sources.include?(:market)
247
+ gathered_data[:market] = fetch_market_overview
248
+ end
249
+
250
+ # Generate AI report
251
+ ai_report = generate_ai_custom_report(prompt, gathered_data)
252
+
253
+ # Structure report data
254
+ report_data = {
255
+ type: :custom_report,
256
+ generated_at: Time.now,
257
+ prompt: prompt,
258
+ data_sources: data_sources,
259
+ data: gathered_data,
260
+ ai_analysis: ai_report
261
+ }
262
+
263
+ # Format report
264
+ formatted_report = format_report(report_data, format)
265
+
266
+ {
267
+ type: :custom_report,
268
+ format: format,
269
+ data: report_data,
270
+ formatted_content: formatted_report,
271
+ timestamp: Time.now
272
+ }
273
+
274
+ rescue => e
275
+ error_msg = "Custom report generation failed: #{e.message}"
276
+ BudaApi::Logger.error(error_msg)
277
+
278
+ {
279
+ type: :report_error,
280
+ error: error_msg,
281
+ timestamp: Time.now
282
+ }
283
+ end
284
+ end
285
+
286
+ # Export report to file
287
+ #
288
+ # @param report [Hash] generated report
289
+ # @param filename [String] output filename
290
+ # @return [Hash] export result
291
+ def export_report(report, filename = nil)
292
+ filename ||= generate_filename(report)
293
+
294
+ begin
295
+ case report[:format]
296
+ when "json"
297
+ write_json_report(report, filename)
298
+ when "csv"
299
+ write_csv_report(report, filename)
300
+ when "html"
301
+ write_html_report(report, filename)
302
+ else
303
+ write_text_report(report, filename)
304
+ end
305
+
306
+ {
307
+ type: :export_success,
308
+ filename: filename,
309
+ format: report[:format],
310
+ size: File.size(filename),
311
+ timestamp: Time.now
312
+ }
313
+
314
+ rescue => e
315
+ error_msg = "Report export failed: #{e.message}"
316
+ BudaApi::Logger.error(error_msg)
317
+
318
+ {
319
+ type: :export_error,
320
+ error: error_msg,
321
+ timestamp: Time.now
322
+ }
323
+ end
324
+ end
325
+
326
+ private
327
+
328
+ def build_report_system_prompt
329
+ """
330
+ You are an expert cryptocurrency trading analyst and report writer.
331
+
332
+ Your expertise includes:
333
+ - Portfolio performance analysis
334
+ - Market trend identification
335
+ - Risk assessment and recommendations
336
+ - Profit/loss analysis
337
+ - Trading strategy evaluation
338
+ - Chilean cryptocurrency market knowledge
339
+
340
+ When generating reports:
341
+ 1. Use clear, professional language
342
+ 2. Provide specific, data-driven insights
343
+ 3. Include actionable recommendations
344
+ 4. Highlight both opportunities and risks
345
+ 5. Consider Chilean market conditions and regulations
346
+ 6. Use appropriate financial terminology
347
+ 7. Structure information logically with headings and bullet points
348
+
349
+ Always base conclusions on the provided data and clearly state assumptions.
350
+ """
351
+ end
352
+
353
+ def extract_portfolio_data(balances_result)
354
+ balances_result.balances.select do |balance|
355
+ balance.amount.amount > 0.0001
356
+ end.map do |balance|
357
+ {
358
+ currency: balance.currency,
359
+ amount: balance.amount.amount,
360
+ available: balance.available_amount.amount,
361
+ frozen: balance.frozen_amount.amount
362
+ }
363
+ end
364
+ end
365
+
366
+ def fetch_portfolio_market_data(portfolios)
367
+ market_data = {}
368
+
369
+ portfolios.each do |holding|
370
+ currency = holding[:currency]
371
+ next if currency == "CLP"
372
+
373
+ market_id = "#{currency}-CLP"
374
+ begin
375
+ ticker = @client.ticker(market_id)
376
+ market_data[currency] = {
377
+ market_id: market_id,
378
+ price: ticker.last_price.amount,
379
+ change_24h: ticker.price_variation_24h,
380
+ volume: ticker.volume.amount,
381
+ min_24h: ticker.min_24h.amount,
382
+ max_24h: ticker.max_24h.amount
383
+ }
384
+ rescue => e
385
+ BudaApi::Logger.warn("Could not fetch market data for #{market_id}: #{e.message}")
386
+ end
387
+ end
388
+
389
+ market_data
390
+ end
391
+
392
+ def calculate_portfolio_metrics(portfolios, market_data)
393
+ total_value_clp = 0.0
394
+ total_change_24h = 0.0
395
+ top_holding = { currency: nil, value: 0.0, percentage: 0.0 }
396
+
397
+ # Calculate values and changes
398
+ portfolios.each do |holding|
399
+ currency = holding[:currency]
400
+
401
+ if currency == "CLP"
402
+ value_clp = holding[:amount]
403
+ change_24h_clp = 0.0
404
+ elsif market_data[currency]
405
+ value_clp = holding[:amount] * market_data[currency][:price]
406
+ change_24h_clp = value_clp * (market_data[currency][:change_24h] / 100.0)
407
+ else
408
+ value_clp = 0.0
409
+ change_24h_clp = 0.0
410
+ end
411
+
412
+ total_value_clp += value_clp
413
+ total_change_24h += change_24h_clp
414
+
415
+ # Track top holding
416
+ if value_clp > top_holding[:value]
417
+ top_holding = {
418
+ currency: currency,
419
+ value: value_clp,
420
+ amount: holding[:amount]
421
+ }
422
+ end
423
+ end
424
+
425
+ # Calculate percentages
426
+ if total_value_clp > 0
427
+ top_holding[:percentage] = (top_holding[:value] / total_value_clp) * 100
428
+ total_change_24h_percent = (total_change_24h / total_value_clp) * 100
429
+ else
430
+ total_change_24h_percent = 0.0
431
+ end
432
+
433
+ {
434
+ total_value_clp: total_value_clp,
435
+ total_change_24h: total_change_24h,
436
+ total_change_24h_percent: total_change_24h_percent,
437
+ top_holding: top_holding,
438
+ asset_allocation: calculate_asset_allocation(portfolios, market_data, total_value_clp)
439
+ }
440
+ end
441
+
442
+ def calculate_asset_allocation(portfolios, market_data, total_value)
443
+ return {} if total_value <= 0
444
+
445
+ allocation = {}
446
+
447
+ portfolios.each do |holding|
448
+ currency = holding[:currency]
449
+
450
+ if currency == "CLP"
451
+ value = holding[:amount]
452
+ elsif market_data[currency]
453
+ value = holding[:amount] * market_data[currency][:price]
454
+ else
455
+ value = 0.0
456
+ end
457
+
458
+ percentage = (value / total_value) * 100
459
+ allocation[currency] = {
460
+ amount: holding[:amount],
461
+ value_clp: value,
462
+ percentage: percentage
463
+ }
464
+ end
465
+
466
+ allocation.sort_by { |_, data| -data[:percentage] }.to_h
467
+ end
468
+
469
+ def fetch_trading_history(market_id, limit)
470
+ trades = []
471
+
472
+ if market_id == 'all'
473
+ # Get trades from all available markets
474
+ BudaApi::Constants::Market::MAJOR.each do |market|
475
+ begin
476
+ market_trades = @client.orders(market, per_page: [limit / 4, 10].max)
477
+ trades.concat(extract_trade_data(market_trades.orders, market))
478
+ rescue => e
479
+ BudaApi::Logger.warn("Could not fetch trades for #{market}: #{e.message}")
480
+ end
481
+ end
482
+ else
483
+ begin
484
+ orders_result = @client.orders(market_id, per_page: limit)
485
+ trades = extract_trade_data(orders_result.orders, market_id)
486
+ rescue => e
487
+ BudaApi::Logger.warn("Could not fetch trades for #{market_id}: #{e.message}")
488
+ end
489
+ end
490
+
491
+ trades.sort_by { |trade| trade[:created_at] }.reverse
492
+ end
493
+
494
+ def extract_trade_data(orders, market_id)
495
+ orders.select { |order| order.state == "traded" }.map do |order|
496
+ {
497
+ id: order.id,
498
+ market_id: market_id,
499
+ side: order.type.downcase,
500
+ amount: order.amount.amount,
501
+ price: order.limit&.amount || order.price&.amount,
502
+ total: order.total_exchanged&.amount,
503
+ fee: order.fee&.amount,
504
+ created_at: order.created_at,
505
+ state: order.state
506
+ }
507
+ end
508
+ end
509
+
510
+ def calculate_performance_metrics(trades)
511
+ return empty_performance_metrics if trades.empty?
512
+
513
+ total_trades = trades.length
514
+ buy_trades = trades.select { |t| t[:side] == "bid" }
515
+ sell_trades = trades.select { |t| t[:side] == "ask" }
516
+
517
+ total_volume = trades.sum { |t| t[:total] || 0 }
518
+ total_fees = trades.sum { |t| t[:fee] || 0 }
519
+
520
+ # Calculate average trade sizes
521
+ avg_trade_size = total_volume / total_trades if total_trades > 0
522
+
523
+ # Calculate win rate (simplified)
524
+ profitable_trades = estimate_profitable_trades(trades)
525
+ win_rate = total_trades > 0 ? (profitable_trades / total_trades.to_f) * 100 : 0
526
+
527
+ {
528
+ total_trades: total_trades,
529
+ buy_trades: buy_trades.length,
530
+ sell_trades: sell_trades.length,
531
+ total_volume: total_volume,
532
+ total_fees: total_fees,
533
+ avg_trade_size: avg_trade_size,
534
+ win_rate: win_rate,
535
+ trading_frequency: calculate_trading_frequency(trades),
536
+ most_traded_market: find_most_traded_market(trades)
537
+ }
538
+ end
539
+
540
+ def estimate_profitable_trades(trades)
541
+ # Simplified profitability estimation
542
+ # In a real implementation, this would track buy/sell pairs
543
+ profitable = 0
544
+
545
+ trades.each_with_index do |trade, index|
546
+ next if index == 0
547
+
548
+ prev_trade = trades[index - 1]
549
+ if trade[:market_id] == prev_trade[:market_id]
550
+ # Simple check: if price increased between buy and sell
551
+ if prev_trade[:side] == "bid" && trade[:side] == "ask"
552
+ profitable += 1 if trade[:price] > prev_trade[:price]
553
+ end
554
+ end
555
+ end
556
+
557
+ profitable
558
+ end
559
+
560
+ def calculate_trading_frequency(trades)
561
+ return 0 if trades.length < 2
562
+
563
+ first_trade = trades.last[:created_at]
564
+ last_trade = trades.first[:created_at]
565
+
566
+ days_span = (Time.parse(last_trade) - Time.parse(first_trade)) / (24 * 60 * 60)
567
+ return 0 if days_span <= 0
568
+
569
+ trades.length / days_span.to_f
570
+ end
571
+
572
+ def find_most_traded_market(trades)
573
+ market_counts = trades.group_by { |t| t[:market_id] }.transform_values(&:length)
574
+ market_counts.max_by { |_, count| count }&.first || "N/A"
575
+ end
576
+
577
+ def analyze_single_market(market_id)
578
+ begin
579
+ ticker = @client.ticker(market_id)
580
+ order_book = @client.order_book(market_id)
581
+
582
+ {
583
+ market_id: market_id,
584
+ price: ticker.last_price.amount,
585
+ change_24h: ticker.price_variation_24h,
586
+ volume: ticker.volume.amount,
587
+ min_24h: ticker.min_24h.amount,
588
+ max_24h: ticker.max_24h.amount,
589
+ spread: calculate_spread(order_book),
590
+ order_book_depth: analyze_order_book_depth(order_book)
591
+ }
592
+ rescue => e
593
+ BudaApi::Logger.warn("Could not analyze market #{market_id}: #{e.message}")
594
+ {
595
+ market_id: market_id,
596
+ error: e.message
597
+ }
598
+ end
599
+ end
600
+
601
+ def calculate_spread(order_book)
602
+ return 0 if order_book.asks.empty? || order_book.bids.empty?
603
+
604
+ best_ask = order_book.asks.first.price
605
+ best_bid = order_book.bids.first.price
606
+
607
+ ((best_ask - best_bid) / best_ask * 100).round(4)
608
+ end
609
+
610
+ def analyze_order_book_depth(order_book)
611
+ {
612
+ ask_levels: order_book.asks.length,
613
+ bid_levels: order_book.bids.length,
614
+ ask_volume: order_book.asks.sum(&:amount),
615
+ bid_volume: order_book.bids.sum(&:amount)
616
+ }
617
+ end
618
+
619
+ def calculate_market_metrics(market_analysis)
620
+ markets_with_data = market_analysis.select { |_, data| !data.key?(:error) }
621
+
622
+ return empty_market_metrics if markets_with_data.empty?
623
+
624
+ total_volume = markets_with_data.sum { |_, data| data[:volume] }
625
+ avg_change = markets_with_data.sum { |_, data| data[:change_24h] } / markets_with_data.length
626
+
627
+ # Find best and worst performers
628
+ sorted_by_change = markets_with_data.sort_by { |_, data| data[:change_24h] }
629
+ best_performer = sorted_by_change.last
630
+ worst_performer = sorted_by_change.first
631
+
632
+ {
633
+ total_volume: total_volume,
634
+ average_change_24h: avg_change,
635
+ markets_analyzed: markets_with_data.length,
636
+ best_performer: {
637
+ market: best_performer[0],
638
+ change: best_performer[1][:change_24h]
639
+ },
640
+ worst_performer: {
641
+ market: worst_performer[0],
642
+ change: worst_performer[1][:change_24h]
643
+ }
644
+ }
645
+ end
646
+
647
+ def generate_market_summary(market_analysis, metrics)
648
+ markets_up = market_analysis.count { |_, data| !data.key?(:error) && data[:change_24h] > 0 }
649
+ markets_down = market_analysis.count { |_, data| !data.key?(:error) && data[:change_24h] < 0 }
650
+
651
+ {
652
+ markets_up: markets_up,
653
+ markets_down: markets_down,
654
+ markets_flat: market_analysis.length - markets_up - markets_down,
655
+ market_sentiment: determine_market_sentiment(metrics[:average_change_24h])
656
+ }
657
+ end
658
+
659
+ def determine_market_sentiment(avg_change)
660
+ case avg_change
661
+ when 5.. then "Very Bullish"
662
+ when 2..5 then "Bullish"
663
+ when -2..2 then "Neutral"
664
+ when -5..-2 then "Bearish"
665
+ else "Very Bearish"
666
+ end
667
+ end
668
+
669
+ def format_report(report_data, format)
670
+ case format
671
+ when "json"
672
+ JSON.pretty_generate(report_data)
673
+ when "csv"
674
+ format_csv_report(report_data)
675
+ when "html"
676
+ format_html_report(report_data)
677
+ when "markdown"
678
+ format_markdown_report(report_data)
679
+ else
680
+ format_text_report(report_data)
681
+ end
682
+ end
683
+
684
+ def format_markdown_report(report_data)
685
+ case report_data[:type]
686
+ when :portfolio_summary
687
+ format_portfolio_markdown(report_data)
688
+ when :trading_performance
689
+ format_trading_markdown(report_data)
690
+ when :market_analysis
691
+ format_market_markdown(report_data)
692
+ when :custom_report
693
+ format_custom_markdown(report_data)
694
+ else
695
+ "# Report\n\n" + JSON.pretty_generate(report_data)
696
+ end
697
+ end
698
+
699
+ def format_portfolio_markdown(data)
700
+ """
701
+ # Portfolio Summary Report
702
+ *Generated: #{data[:generated_at]}*
703
+
704
+ ## Overview
705
+ - **Total Portfolio Value:** #{data[:metrics][:total_value_clp].round(2)} CLP
706
+ - **Number of Assets:** #{data[:summary][:asset_count]}
707
+ - **24h Change:** #{data[:metrics][:total_change_24h_percent].round(2)}% (#{data[:metrics][:total_change_24h].round(2)} CLP)
708
+
709
+ ## Top Holdings
710
+ #{format_asset_allocation_markdown(data[:metrics][:asset_allocation])}
711
+
712
+ ## Market Performance
713
+ #{format_market_performance_markdown(data[:market_data])}
714
+
715
+ #{data[:ai_analysis] ? "## AI Analysis\n#{data[:ai_analysis][:content]}" : ""}
716
+ """
717
+ end
718
+
719
+ def format_asset_allocation_markdown(allocation)
720
+ lines = ["| Asset | Amount | Value (CLP) | Allocation |"]
721
+ lines << "|-------|---------|-------------|------------|"]
722
+
723
+ allocation.each do |currency, data|
724
+ lines << "| #{currency} | #{data[:amount].round(8)} | #{data[:value_clp].round(2)} | #{data[:percentage].round(1)}% |"
725
+ end
726
+
727
+ lines.join("\n")
728
+ end
729
+
730
+ def format_market_performance_markdown(market_data)
731
+ return "No market data available" if market_data.empty?
732
+
733
+ lines = ["| Market | Price | 24h Change | Volume |"]
734
+ lines << "|--------|-------|------------|--------|"]
735
+
736
+ market_data.each do |currency, data|
737
+ change_symbol = data[:change_24h] >= 0 ? "+" : ""
738
+ lines << "| #{data[:market_id]} | #{data[:price]} | #{change_symbol}#{data[:change_24h].round(2)}% | #{data[:volume].round(2)} |"
739
+ end
740
+
741
+ lines.join("\n")
742
+ end
743
+
744
+ def empty_portfolio_report(format)
745
+ {
746
+ type: :empty_portfolio,
747
+ message: "No portfolio holdings found",
748
+ timestamp: Time.now
749
+ }
750
+ end
751
+
752
+ def empty_trading_report(format)
753
+ {
754
+ type: :empty_trading,
755
+ message: "No trading history found",
756
+ timestamp: Time.now
757
+ }
758
+ end
759
+
760
+ def empty_performance_metrics
761
+ {
762
+ total_trades: 0,
763
+ buy_trades: 0,
764
+ sell_trades: 0,
765
+ total_volume: 0.0,
766
+ total_fees: 0.0,
767
+ avg_trade_size: 0.0,
768
+ win_rate: 0.0,
769
+ trading_frequency: 0.0,
770
+ most_traded_market: "N/A"
771
+ }
772
+ end
773
+
774
+ def empty_market_metrics
775
+ {
776
+ total_volume: 0.0,
777
+ average_change_24h: 0.0,
778
+ markets_analyzed: 0,
779
+ best_performer: { market: "N/A", change: 0.0 },
780
+ worst_performer: { market: "N/A", change: 0.0 }
781
+ }
782
+ end
783
+
784
+ # AI integration methods
785
+ def generate_ai_portfolio_analysis(report_data)
786
+ return nil unless defined?(RubyLLM)
787
+
788
+ prompt = build_portfolio_analysis_prompt(report_data)
789
+
790
+ begin
791
+ response = @llm.complete(
792
+ messages: [{ role: "user", content: prompt }],
793
+ max_tokens: 400
794
+ )
795
+
796
+ {
797
+ content: response.content,
798
+ generated_at: Time.now
799
+ }
800
+ rescue => e
801
+ BudaApi::Logger.error("AI portfolio analysis failed: #{e.message}")
802
+ nil
803
+ end
804
+ end
805
+
806
+ def build_portfolio_analysis_prompt(report_data)
807
+ """
808
+ Analyze this cryptocurrency portfolio and provide insights:
809
+
810
+ Portfolio Value: #{report_data[:metrics][:total_value_clp].round(2)} CLP
811
+ 24h Change: #{report_data[:metrics][:total_change_24h_percent].round(2)}%
812
+ Assets: #{report_data[:summary][:asset_count]}
813
+ Top Holding: #{report_data[:metrics][:top_holding][:currency]} (#{report_data[:metrics][:top_holding][:percentage].round(1)}%)
814
+
815
+ Asset Allocation:
816
+ #{report_data[:metrics][:asset_allocation].map { |currency, data| "- #{currency}: #{data[:percentage].round(1)}%" }.join("\n")}
817
+
818
+ Provide a brief analysis covering:
819
+ 1. Portfolio diversification assessment
820
+ 2. Risk factors and opportunities
821
+ 3. Specific recommendations for Chilean crypto investors
822
+
823
+ Keep it concise and actionable.
824
+ """
825
+ end
826
+
827
+ def generate_filename(report)
828
+ type = report[:type].to_s.gsub('_', '-')
829
+ timestamp = Time.now.strftime("%Y%m%d-%H%M%S")
830
+ extension = get_file_extension(report[:format])
831
+
832
+ "buda-#{type}-#{timestamp}.#{extension}"
833
+ end
834
+
835
+ def get_file_extension(format)
836
+ case format
837
+ when "json" then "json"
838
+ when "csv" then "csv"
839
+ when "html" then "html"
840
+ when "markdown" then "md"
841
+ else "txt"
842
+ end
843
+ end
844
+
845
+ def write_text_report(report, filename)
846
+ File.write(filename, report[:formatted_content])
847
+ end
848
+
849
+ def write_json_report(report, filename)
850
+ File.write(filename, JSON.pretty_generate(report[:data]))
851
+ end
852
+
853
+ def write_csv_report(report, filename)
854
+ # Basic CSV export - would need enhancement for complex reports
855
+ content = "Type,Generated At,Summary\n"
856
+ content += "#{report[:type]},#{report[:data][:generated_at]},#{report[:data].inspect}"
857
+
858
+ File.write(filename, content)
859
+ end
860
+
861
+ def write_html_report(report, filename)
862
+ html = """
863
+ <!DOCTYPE html>
864
+ <html>
865
+ <head>
866
+ <title>Buda API Report</title>
867
+ <style>
868
+ body { font-family: Arial, sans-serif; margin: 20px; }
869
+ table { border-collapse: collapse; width: 100%; }
870
+ th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
871
+ th { background-color: #f2f2f2; }
872
+ .positive { color: green; }
873
+ .negative { color: red; }
874
+ </style>
875
+ </head>
876
+ <body>
877
+ <h1>Buda API Report</h1>
878
+ <pre>#{report[:formatted_content]}</pre>
879
+ </body>
880
+ </html>
881
+ """
882
+
883
+ File.write(filename, html)
884
+ end
885
+
886
+ # Additional helper methods for other report types...
887
+ def fetch_recent_trades_all_markets
888
+ # Implementation for fetching recent trades across all markets
889
+ []
890
+ end
891
+
892
+ def fetch_market_overview
893
+ # Implementation for fetching general market overview
894
+ {}
895
+ end
896
+
897
+ def generate_ai_custom_report(prompt, data)
898
+ return nil unless defined?(RubyLLM)
899
+
900
+ data_summary = summarize_data_for_ai(data)
901
+ full_prompt = "#{prompt}\n\nAvailable data:\n#{data_summary}"
902
+
903
+ begin
904
+ response = @llm.complete(
905
+ messages: [{ role: "user", content: full_prompt }],
906
+ max_tokens: 800
907
+ )
908
+
909
+ {
910
+ content: response.content,
911
+ generated_at: Time.now
912
+ }
913
+ rescue => e
914
+ BudaApi::Logger.error("AI custom report failed: #{e.message}")
915
+ { content: "AI analysis unavailable", generated_at: Time.now }
916
+ end
917
+ end
918
+
919
+ def summarize_data_for_ai(data)
920
+ summary = []
921
+
922
+ if data[:portfolio]
923
+ summary << "Portfolio: #{data[:portfolio].length} assets"
924
+ end
925
+
926
+ if data[:trades]
927
+ summary << "Trades: #{data[:trades].length} recent trades"
928
+ end
929
+
930
+ if data[:market]
931
+ summary << "Market: Current market overview available"
932
+ end
933
+
934
+ summary.join(", ")
935
+ end
936
+
937
+ # Placeholder methods for additional report formatting
938
+ def format_trading_markdown(data)
939
+ "# Trading Performance Report\n\n*Generated: #{data[:generated_at]}*\n\n" +
940
+ JSON.pretty_generate(data[:performance])
941
+ end
942
+
943
+ def format_market_markdown(data)
944
+ "# Market Analysis Report\n\n*Generated: #{data[:generated_at]}*\n\n" +
945
+ JSON.pretty_generate(data[:summary])
946
+ end
947
+
948
+ def format_custom_markdown(data)
949
+ content = "# Custom Report\n\n*Generated: #{data[:generated_at]}*\n\n"
950
+ content += "**Request:** #{data[:prompt]}\n\n"
951
+ content += data[:ai_analysis] ? data[:ai_analysis][:content] : "No analysis available"
952
+ content
953
+ end
954
+
955
+ def format_text_report(data)
956
+ "Buda API Report\nGenerated: #{data[:generated_at]}\n\n#{JSON.pretty_generate(data)}"
957
+ end
958
+
959
+ def format_csv_report(data)
960
+ "Type,Timestamp,Data\n#{data[:type]},#{data[:generated_at]},#{data.inspect}"
961
+ end
962
+
963
+ def format_html_report(data)
964
+ "<html><body><h1>Buda API Report</h1><pre>#{JSON.pretty_generate(data)}</pre></body></html>"
965
+ end
966
+
967
+ def get_date_range(trades)
968
+ return { start: nil, end: nil } if trades.empty?
969
+
970
+ dates = trades.map { |t| Time.parse(t[:created_at]) }
971
+ {
972
+ start: dates.min,
973
+ end: dates.max
974
+ }
975
+ end
976
+
977
+ def generate_ai_trading_insights(report_data)
978
+ return nil unless defined?(RubyLLM)
979
+
980
+ prompt = build_trading_insights_prompt(report_data)
981
+
982
+ begin
983
+ response = @llm.complete(
984
+ messages: [{ role: "user", content: prompt }],
985
+ max_tokens: 300
986
+ )
987
+
988
+ {
989
+ content: response.content,
990
+ generated_at: Time.now
991
+ }
992
+ rescue => e
993
+ BudaApi::Logger.error("AI trading insights failed: #{e.message}")
994
+ nil
995
+ end
996
+ end
997
+
998
+ def build_trading_insights_prompt(report_data)
999
+ """
1000
+ Analyze this trading performance data:
1001
+
1002
+ Total Trades: #{report_data[:performance][:total_trades]}
1003
+ Win Rate: #{report_data[:performance][:win_rate].round(1)}%
1004
+ Total Volume: #{report_data[:performance][:total_volume].round(2)} CLP
1005
+ Total Fees: #{report_data[:performance][:total_fees].round(2)} CLP
1006
+ Most Traded: #{report_data[:performance][:most_traded_market]}
1007
+
1008
+ Provide brief insights on:
1009
+ 1. Trading performance strengths/weaknesses
1010
+ 2. Fee optimization opportunities
1011
+ 3. Strategy improvement recommendations
1012
+ """
1013
+ end
1014
+
1015
+ def generate_ai_market_insights(report_data)
1016
+ return nil unless defined?(RubyLLM)
1017
+
1018
+ prompt = build_market_insights_prompt(report_data)
1019
+
1020
+ begin
1021
+ response = @llm.complete(
1022
+ messages: [{ role: "user", content: prompt }],
1023
+ max_tokens: 300
1024
+ )
1025
+
1026
+ {
1027
+ content: response.content,
1028
+ generated_at: Time.now
1029
+ }
1030
+ rescue => e
1031
+ BudaApi::Logger.error("AI market insights failed: #{e.message}")
1032
+ nil
1033
+ end
1034
+ end
1035
+
1036
+ def build_market_insights_prompt(report_data)
1037
+ """
1038
+ Analyze this market data:
1039
+
1040
+ Markets Analyzed: #{report_data[:markets_analyzed]}
1041
+ Average Change: #{report_data[:metrics][:average_change_24h].round(2)}%
1042
+ Best Performer: #{report_data[:metrics][:best_performer][:market]} (+#{report_data[:metrics][:best_performer][:change].round(2)}%)
1043
+ Worst Performer: #{report_data[:metrics][:worst_performer][:market]} (#{report_data[:metrics][:worst_performer][:change].round(2)}%)
1044
+ Market Sentiment: #{report_data[:summary][:market_sentiment]}
1045
+
1046
+ Provide brief market analysis covering:
1047
+ 1. Current market trends
1048
+ 2. Trading opportunities
1049
+ 3. Risk factors to consider
1050
+ """
1051
+ end
1052
+ end
1053
+ end
1054
+ end