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,789 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BudaApi
4
+ module AI
5
+ # AI-powered risk management and portfolio analysis
6
+ class RiskManager
7
+ RISK_LEVELS = {
8
+ very_low: { score: 1, color: "🟢", description: "Very low risk" },
9
+ low: { score: 2, color: "🟡", description: "Low risk" },
10
+ medium: { score: 3, color: "🟠", description: "Medium risk" },
11
+ high: { score: 4, color: "🔴", description: "High risk" },
12
+ very_high: { score: 5, color: "🚫", description: "Very high risk - avoid!" }
13
+ }.freeze
14
+
15
+ PORTFOLIO_RISK_FACTORS = [
16
+ "concentration_risk",
17
+ "volatility_risk",
18
+ "liquidity_risk",
19
+ "correlation_risk",
20
+ "size_risk"
21
+ ].freeze
22
+
23
+ def initialize(client, llm_provider: :openai)
24
+ @client = client
25
+ @llm = RubyLLM.new(
26
+ provider: llm_provider,
27
+ system_prompt: build_risk_system_prompt
28
+ )
29
+
30
+ BudaApi::Logger.info("Risk Manager initialized")
31
+ end
32
+
33
+ # Analyze portfolio risk across all holdings
34
+ #
35
+ # @param options [Hash] analysis options
36
+ # @option options [Boolean] :include_ai_insights include AI analysis
37
+ # @option options [Array<String>] :focus_factors specific risk factors to analyze
38
+ # @return [Hash] comprehensive risk analysis
39
+ def analyze_portfolio_risk(options = {})
40
+ BudaApi::Logger.info("Analyzing portfolio risk")
41
+
42
+ begin
43
+ # Get account balances
44
+ balances_result = @client.balances
45
+ portfolios = extract_non_zero_balances(balances_result)
46
+
47
+ return no_portfolio_risk if portfolios.empty?
48
+
49
+ # Calculate basic risk metrics
50
+ basic_metrics = calculate_basic_risk_metrics(portfolios)
51
+
52
+ # Get market data for risk calculations
53
+ market_data = fetch_market_data_for_portfolio(portfolios)
54
+
55
+ # Calculate advanced risk metrics
56
+ advanced_metrics = calculate_advanced_risk_metrics(portfolios, market_data)
57
+
58
+ # Generate overall risk assessment
59
+ overall_risk = calculate_overall_risk_score(basic_metrics, advanced_metrics)
60
+
61
+ result = {
62
+ type: :portfolio_risk_analysis,
63
+ timestamp: Time.now,
64
+ portfolio_value: basic_metrics[:total_value],
65
+ currency_count: portfolios.length,
66
+ overall_risk: overall_risk,
67
+ basic_metrics: basic_metrics,
68
+ advanced_metrics: advanced_metrics,
69
+ recommendations: generate_risk_recommendations(overall_risk, basic_metrics, advanced_metrics),
70
+ holdings: portfolios
71
+ }
72
+
73
+ # Add AI insights if requested
74
+ if options[:include_ai_insights]
75
+ result[:ai_insights] = generate_ai_risk_insights(result)
76
+ end
77
+
78
+ result
79
+
80
+ rescue => e
81
+ error_msg = "Portfolio risk analysis failed: #{e.message}"
82
+ BudaApi::Logger.error(error_msg)
83
+
84
+ {
85
+ type: :risk_analysis_error,
86
+ error: error_msg,
87
+ timestamp: Time.now
88
+ }
89
+ end
90
+ end
91
+
92
+ # Evaluate risk for a specific trade before execution
93
+ #
94
+ # @param market_id [String] trading pair
95
+ # @param side [String] 'buy' or 'sell'
96
+ # @param amount [Float] trade amount
97
+ # @param price [Float] trade price (optional)
98
+ # @return [Hash] trade risk assessment
99
+ def evaluate_trade_risk(market_id, side, amount, price = nil)
100
+ BudaApi::Logger.info("Evaluating trade risk for #{side} #{amount} #{market_id}")
101
+
102
+ begin
103
+ # Get current market data
104
+ ticker = @client.ticker(market_id)
105
+ order_book = @client.order_book(market_id)
106
+
107
+ # Get current portfolio
108
+ balances_result = @client.balances
109
+ current_portfolio = extract_non_zero_balances(balances_result)
110
+
111
+ # Calculate trade impact
112
+ trade_impact = calculate_trade_impact(market_id, side, amount, price, ticker, order_book)
113
+
114
+ # Calculate position sizing risk
115
+ position_risk = calculate_position_risk(market_id, amount, ticker.last_price.amount, current_portfolio)
116
+
117
+ # Calculate market impact risk
118
+ market_impact_risk = calculate_market_impact_risk(amount, price || ticker.last_price.amount, order_book)
119
+
120
+ # Generate overall trade risk score
121
+ trade_risk_score = calculate_trade_risk_score(trade_impact, position_risk, market_impact_risk)
122
+
123
+ {
124
+ type: :trade_risk_evaluation,
125
+ market_id: market_id,
126
+ side: side,
127
+ amount: amount,
128
+ price: price,
129
+ risk_level: determine_risk_level(trade_risk_score),
130
+ risk_score: trade_risk_score,
131
+ trade_impact: trade_impact,
132
+ position_risk: position_risk,
133
+ market_impact_risk: market_impact_risk,
134
+ recommendations: generate_trade_recommendations(trade_risk_score, trade_impact),
135
+ should_proceed: trade_risk_score < 3.5,
136
+ timestamp: Time.now
137
+ }
138
+
139
+ rescue => e
140
+ error_msg = "Trade risk evaluation failed: #{e.message}"
141
+ BudaApi::Logger.error(error_msg)
142
+
143
+ {
144
+ type: :trade_risk_error,
145
+ error: error_msg,
146
+ timestamp: Time.now
147
+ }
148
+ end
149
+ end
150
+
151
+ # Monitor portfolio for risk threshold breaches
152
+ #
153
+ # @param thresholds [Hash] risk thresholds to monitor
154
+ # @return [Hash] monitoring results with alerts
155
+ def monitor_risk_thresholds(thresholds = {})
156
+ default_thresholds = {
157
+ max_position_percentage: 30.0,
158
+ max_daily_loss: 5.0,
159
+ min_diversification_score: 0.6,
160
+ max_volatility_score: 4.0
161
+ }
162
+
163
+ thresholds = default_thresholds.merge(thresholds)
164
+
165
+ BudaApi::Logger.info("Monitoring risk thresholds")
166
+
167
+ begin
168
+ # Get current portfolio analysis
169
+ portfolio_analysis = analyze_portfolio_risk
170
+
171
+ alerts = []
172
+
173
+ # Check position concentration
174
+ if portfolio_analysis[:basic_metrics][:max_position_percentage] > thresholds[:max_position_percentage]
175
+ alerts << {
176
+ type: :concentration_alert,
177
+ level: :high,
178
+ message: "🚨 Position concentration too high: #{portfolio_analysis[:basic_metrics][:max_position_percentage].round(1)}% (limit: #{thresholds[:max_position_percentage]}%)",
179
+ current_value: portfolio_analysis[:basic_metrics][:max_position_percentage],
180
+ threshold: thresholds[:max_position_percentage]
181
+ }
182
+ end
183
+
184
+ # Check diversification
185
+ if portfolio_analysis[:advanced_metrics][:diversification_score] < thresholds[:min_diversification_score]
186
+ alerts << {
187
+ type: :diversification_alert,
188
+ level: :medium,
189
+ message: "⚠️ Portfolio not well diversified: #{portfolio_analysis[:advanced_metrics][:diversification_score].round(2)} (minimum: #{thresholds[:min_diversification_score]})",
190
+ current_value: portfolio_analysis[:advanced_metrics][:diversification_score],
191
+ threshold: thresholds[:min_diversification_score]
192
+ }
193
+ end
194
+
195
+ # Check overall volatility
196
+ if portfolio_analysis[:overall_risk][:score] > thresholds[:max_volatility_score]
197
+ alerts << {
198
+ type: :volatility_alert,
199
+ level: :high,
200
+ message: "🔥 Portfolio volatility too high: #{portfolio_analysis[:overall_risk][:score].round(1)} (limit: #{thresholds[:max_volatility_score]})",
201
+ current_value: portfolio_analysis[:overall_risk][:score],
202
+ threshold: thresholds[:max_volatility_score]
203
+ }
204
+ end
205
+
206
+ {
207
+ type: :risk_monitoring,
208
+ timestamp: Time.now,
209
+ alerts_count: alerts.length,
210
+ alerts: alerts,
211
+ thresholds: thresholds,
212
+ portfolio_summary: {
213
+ total_value: portfolio_analysis[:portfolio_value],
214
+ risk_level: portfolio_analysis[:overall_risk][:level],
215
+ risk_score: portfolio_analysis[:overall_risk][:score]
216
+ },
217
+ safe: alerts.empty?
218
+ }
219
+
220
+ rescue => e
221
+ error_msg = "Risk monitoring failed: #{e.message}"
222
+ BudaApi::Logger.error(error_msg)
223
+
224
+ {
225
+ type: :risk_monitoring_error,
226
+ error: error_msg,
227
+ timestamp: Time.now
228
+ }
229
+ end
230
+ end
231
+
232
+ # Generate stop-loss recommendations based on risk analysis
233
+ #
234
+ # @param market_id [String] trading pair
235
+ # @param position_size [Float] current position size
236
+ # @return [Hash] stop-loss recommendations
237
+ def recommend_stop_loss(market_id, position_size)
238
+ BudaApi::Logger.info("Generating stop-loss recommendations for #{market_id}")
239
+
240
+ begin
241
+ ticker = @client.ticker(market_id)
242
+ current_price = ticker.last_price.amount
243
+
244
+ # Calculate different stop-loss levels
245
+ conservative_stop = current_price * 0.95 # 5% stop loss
246
+ moderate_stop = current_price * 0.90 # 10% stop loss
247
+ aggressive_stop = current_price * 0.85 # 15% stop loss
248
+
249
+ # Calculate potential losses
250
+ position_value = position_size * current_price
251
+
252
+ {
253
+ type: :stop_loss_recommendations,
254
+ market_id: market_id,
255
+ current_price: current_price,
256
+ position_size: position_size,
257
+ position_value: position_value,
258
+ recommendations: {
259
+ conservative: {
260
+ price: conservative_stop,
261
+ percentage: 5.0,
262
+ max_loss: position_value * 0.05,
263
+ description: "Conservative 5% stop-loss for capital preservation"
264
+ },
265
+ moderate: {
266
+ price: moderate_stop,
267
+ percentage: 10.0,
268
+ max_loss: position_value * 0.10,
269
+ description: "Moderate 10% stop-loss balancing protection and flexibility"
270
+ },
271
+ aggressive: {
272
+ price: aggressive_stop,
273
+ percentage: 15.0,
274
+ max_loss: position_value * 0.15,
275
+ description: "Aggressive 15% stop-loss for volatile markets"
276
+ }
277
+ },
278
+ recommendation: determine_best_stop_loss(ticker, position_value),
279
+ timestamp: Time.now
280
+ }
281
+
282
+ rescue => e
283
+ error_msg = "Stop-loss recommendation failed: #{e.message}"
284
+ BudaApi::Logger.error(error_msg)
285
+
286
+ {
287
+ type: :stop_loss_error,
288
+ error: error_msg,
289
+ timestamp: Time.now
290
+ }
291
+ end
292
+ end
293
+
294
+ private
295
+
296
+ def build_risk_system_prompt
297
+ """
298
+ You are an expert risk management analyst for cryptocurrency trading.
299
+
300
+ Your expertise includes:
301
+ - Portfolio diversification analysis
302
+ - Position sizing recommendations
303
+ - Volatility assessment
304
+ - Correlation analysis between crypto assets
305
+ - Market risk evaluation
306
+ - Risk-adjusted return optimization
307
+
308
+ When analyzing risks:
309
+ 1. Consider both technical and fundamental factors
310
+ 2. Account for crypto market volatility and correlations
311
+ 3. Provide specific, actionable recommendations
312
+ 4. Explain risk levels in simple terms
313
+ 5. Consider Chilean market conditions and regulations
314
+
315
+ Always prioritize capital preservation while identifying opportunities.
316
+ Be conservative with risk assessments - it's better to be cautious.
317
+ """
318
+ end
319
+
320
+ def extract_non_zero_balances(balances_result)
321
+ balances_result.balances.select do |balance|
322
+ balance.amount.amount > 0.0001 # Filter out dust
323
+ end.map do |balance|
324
+ {
325
+ currency: balance.currency,
326
+ amount: balance.amount.amount,
327
+ available: balance.available_amount.amount,
328
+ frozen: balance.frozen_amount.amount
329
+ }
330
+ end
331
+ end
332
+
333
+ def no_portfolio_risk
334
+ {
335
+ type: :no_portfolio,
336
+ message: "No significant portfolio holdings to analyze",
337
+ timestamp: Time.now
338
+ }
339
+ end
340
+
341
+ def calculate_basic_risk_metrics(portfolios)
342
+ # Calculate total portfolio value in CLP
343
+ total_value_clp = calculate_total_portfolio_value_clp(portfolios)
344
+
345
+ # Calculate position percentages
346
+ position_percentages = calculate_position_percentages(portfolios, total_value_clp)
347
+
348
+ # Find largest position
349
+ max_position_percentage = position_percentages.values.max || 0
350
+
351
+ {
352
+ total_value: total_value_clp,
353
+ currency_count: portfolios.length,
354
+ max_position_percentage: max_position_percentage,
355
+ position_percentages: position_percentages,
356
+ is_concentrated: max_position_percentage > 50.0
357
+ }
358
+ end
359
+
360
+ def calculate_total_portfolio_value_clp(portfolios)
361
+ total_value = 0.0
362
+
363
+ portfolios.each do |holding|
364
+ if holding[:currency] == "CLP"
365
+ total_value += holding[:amount]
366
+ else
367
+ # Get current market price for conversion to CLP
368
+ market_id = "#{holding[:currency]}-CLP"
369
+ begin
370
+ ticker = @client.ticker(market_id)
371
+ total_value += holding[:amount] * ticker.last_price.amount
372
+ rescue
373
+ # Skip if market doesn't exist or API fails
374
+ BudaApi::Logger.warn("Could not get price for #{market_id}")
375
+ end
376
+ end
377
+ end
378
+
379
+ total_value
380
+ end
381
+
382
+ def calculate_position_percentages(portfolios, total_value_clp)
383
+ percentages = {}
384
+
385
+ portfolios.each do |holding|
386
+ currency = holding[:currency]
387
+
388
+ if currency == "CLP"
389
+ value_clp = holding[:amount]
390
+ else
391
+ market_id = "#{currency}-CLP"
392
+ begin
393
+ ticker = @client.ticker(market_id)
394
+ value_clp = holding[:amount] * ticker.last_price.amount
395
+ rescue
396
+ value_clp = 0.0
397
+ end
398
+ end
399
+
400
+ percentage = total_value_clp > 0 ? (value_clp / total_value_clp) * 100 : 0
401
+ percentages[currency] = percentage
402
+ end
403
+
404
+ percentages
405
+ end
406
+
407
+ def fetch_market_data_for_portfolio(portfolios)
408
+ market_data = {}
409
+
410
+ portfolios.each do |holding|
411
+ currency = holding[:currency]
412
+ next if currency == "CLP"
413
+
414
+ market_id = "#{currency}-CLP"
415
+ begin
416
+ ticker = @client.ticker(market_id)
417
+ market_data[currency] = {
418
+ price: ticker.last_price.amount,
419
+ volume: ticker.volume.amount,
420
+ change_24h: ticker.price_variation_24h
421
+ }
422
+ rescue => e
423
+ BudaApi::Logger.warn("Could not fetch market data for #{market_id}: #{e.message}")
424
+ end
425
+ end
426
+
427
+ market_data
428
+ end
429
+
430
+ def calculate_advanced_risk_metrics(portfolios, market_data)
431
+ # Calculate diversification score (Simpson's index)
432
+ diversification_score = calculate_diversification_score(portfolios)
433
+
434
+ # Calculate volatility score based on 24h changes
435
+ volatility_score = calculate_volatility_score(market_data)
436
+
437
+ # Calculate correlation risk (simplified)
438
+ correlation_risk = calculate_correlation_risk(portfolios)
439
+
440
+ {
441
+ diversification_score: diversification_score,
442
+ volatility_score: volatility_score,
443
+ correlation_risk: correlation_risk,
444
+ risk_factors: analyze_risk_factors(portfolios, market_data)
445
+ }
446
+ end
447
+
448
+ def calculate_diversification_score(portfolios)
449
+ return 0.0 if portfolios.empty?
450
+
451
+ total_amount = portfolios.sum { |p| p[:amount] }
452
+ return 0.0 if total_amount == 0
453
+
454
+ # Calculate Simpson's diversity index
455
+ sum_of_squares = portfolios.sum do |holding|
456
+ proportion = holding[:amount] / total_amount
457
+ proportion ** 2
458
+ end
459
+
460
+ # Convert to 0-1 scale where 1 is perfectly diversified
461
+ 1.0 - sum_of_squares
462
+ end
463
+
464
+ def calculate_volatility_score(market_data)
465
+ return 1.0 if market_data.empty?
466
+
467
+ # Calculate average absolute change across holdings
468
+ changes = market_data.values.map { |data| data[:change_24h].abs }
469
+ avg_volatility = changes.sum / changes.length
470
+
471
+ # Convert to 1-5 scale
472
+ case avg_volatility
473
+ when 0..2 then 1.0
474
+ when 2..5 then 2.0
475
+ when 5..10 then 3.0
476
+ when 10..20 then 4.0
477
+ else 5.0
478
+ end
479
+ end
480
+
481
+ def calculate_correlation_risk(portfolios)
482
+ # Simplified correlation analysis
483
+ # In a real implementation, this would use historical price correlations
484
+
485
+ crypto_count = portfolios.count { |p| p[:currency] != "CLP" }
486
+
487
+ case crypto_count
488
+ when 0..1 then 1.0 # Low correlation risk with few assets
489
+ when 2..3 then 2.0 # Medium risk
490
+ when 4..6 then 3.0 # Higher risk - many cryptos tend to correlate
491
+ else 4.0 # High correlation risk
492
+ end
493
+ end
494
+
495
+ def analyze_risk_factors(portfolios, market_data)
496
+ factors = {}
497
+
498
+ # Concentration risk
499
+ max_position = portfolios.max_by { |p| p[:amount] }
500
+ factors[:concentration] = max_position ? calculate_concentration_risk(max_position, portfolios) : 1.0
501
+
502
+ # Liquidity risk
503
+ factors[:liquidity] = calculate_liquidity_risk(market_data)
504
+
505
+ # Size risk
506
+ factors[:size] = calculate_size_risk(portfolios)
507
+
508
+ factors
509
+ end
510
+
511
+ def calculate_concentration_risk(max_position, portfolios)
512
+ total_value = portfolios.sum { |p| p[:amount] }
513
+ concentration = max_position[:amount] / total_value
514
+
515
+ case concentration
516
+ when 0..0.3 then 1.0
517
+ when 0.3..0.5 then 2.0
518
+ when 0.5..0.7 then 3.0
519
+ when 0.7..0.9 then 4.0
520
+ else 5.0
521
+ end
522
+ end
523
+
524
+ def calculate_liquidity_risk(market_data)
525
+ return 1.0 if market_data.empty?
526
+
527
+ # Use volume as proxy for liquidity
528
+ volumes = market_data.values.map { |data| data[:volume] }
529
+ avg_volume = volumes.sum / volumes.length
530
+
531
+ # Rough categorization based on volume
532
+ case avg_volume
533
+ when 1000000.. then 1.0 # High liquidity
534
+ when 100000..1000000 then 2.0
535
+ when 10000..100000 then 3.0
536
+ when 1000..10000 then 4.0
537
+ else 5.0 # Low liquidity
538
+ end
539
+ end
540
+
541
+ def calculate_size_risk(portfolios)
542
+ total_currencies = portfolios.length
543
+
544
+ case total_currencies
545
+ when 5.. then 1.0 # Well diversified
546
+ when 3..4 then 2.0 # Moderately diversified
547
+ when 2 then 3.0 # Limited diversification
548
+ when 1 then 5.0 # No diversification
549
+ else 1.0
550
+ end
551
+ end
552
+
553
+ def calculate_overall_risk_score(basic_metrics, advanced_metrics)
554
+ # Weighted average of different risk components
555
+ concentration_weight = 0.3
556
+ volatility_weight = 0.25
557
+ diversification_weight = 0.25
558
+ correlation_weight = 0.2
559
+
560
+ concentration_risk = basic_metrics[:is_concentrated] ? 4.0 : 2.0
561
+ volatility_risk = advanced_metrics[:volatility_score]
562
+ diversification_risk = (1.0 - advanced_metrics[:diversification_score]) * 5.0
563
+ correlation_risk = advanced_metrics[:correlation_risk]
564
+
565
+ weighted_score = (
566
+ concentration_risk * concentration_weight +
567
+ volatility_risk * volatility_weight +
568
+ diversification_risk * diversification_weight +
569
+ correlation_risk * correlation_weight
570
+ )
571
+
572
+ risk_level = determine_risk_level(weighted_score)
573
+
574
+ {
575
+ score: weighted_score,
576
+ level: risk_level[:description],
577
+ color: risk_level[:color],
578
+ components: {
579
+ concentration: concentration_risk,
580
+ volatility: volatility_risk,
581
+ diversification: diversification_risk,
582
+ correlation: correlation_risk
583
+ }
584
+ }
585
+ end
586
+
587
+ def determine_risk_level(score)
588
+ case score
589
+ when 0..1.5 then RISK_LEVELS[:very_low]
590
+ when 1.5..2.5 then RISK_LEVELS[:low]
591
+ when 2.5..3.5 then RISK_LEVELS[:medium]
592
+ when 3.5..4.5 then RISK_LEVELS[:high]
593
+ else RISK_LEVELS[:very_high]
594
+ end
595
+ end
596
+
597
+ def generate_risk_recommendations(overall_risk, basic_metrics, advanced_metrics)
598
+ recommendations = []
599
+
600
+ # Concentration recommendations
601
+ if basic_metrics[:max_position_percentage] > 50
602
+ recommendations << {
603
+ type: :diversification,
604
+ priority: :high,
605
+ message: "🎯 Reduce position concentration - largest holding is #{basic_metrics[:max_position_percentage].round(1)}%"
606
+ }
607
+ end
608
+
609
+ # Diversification recommendations
610
+ if advanced_metrics[:diversification_score] < 0.6
611
+ recommendations << {
612
+ type: :diversification,
613
+ priority: :medium,
614
+ message: "📊 Improve diversification across more assets"
615
+ }
616
+ end
617
+
618
+ # Volatility recommendations
619
+ if advanced_metrics[:volatility_score] > 3.5
620
+ recommendations << {
621
+ type: :volatility,
622
+ priority: :high,
623
+ message: "🌊 Consider reducing exposure to high-volatility assets"
624
+ }
625
+ end
626
+
627
+ recommendations
628
+ end
629
+
630
+ def calculate_trade_impact(market_id, side, amount, price, ticker, order_book)
631
+ current_price = ticker.last_price.amount
632
+ trade_price = price || current_price
633
+
634
+ # Calculate price impact
635
+ price_impact_percent = ((trade_price - current_price) / current_price * 100).abs
636
+
637
+ # Calculate size impact relative to order book
638
+ relevant_side = side == "buy" ? order_book.asks : order_book.bids
639
+ total_volume = relevant_side.sum(&:amount)
640
+ size_impact_percent = total_volume > 0 ? (amount / total_volume * 100) : 0
641
+
642
+ {
643
+ price_impact_percent: price_impact_percent,
644
+ size_impact_percent: size_impact_percent,
645
+ estimated_cost: amount * trade_price,
646
+ current_market_price: current_price,
647
+ price_deviation: price_impact_percent
648
+ }
649
+ end
650
+
651
+ def calculate_position_risk(market_id, amount, price, current_portfolio)
652
+ trade_value = amount * price
653
+
654
+ # Get current portfolio value
655
+ total_portfolio_value = calculate_total_portfolio_value_clp(current_portfolio)
656
+
657
+ position_percentage = total_portfolio_value > 0 ? (trade_value / total_portfolio_value * 100) : 0
658
+
659
+ {
660
+ trade_value: trade_value,
661
+ portfolio_percentage: position_percentage,
662
+ is_significant: position_percentage > 10.0,
663
+ risk_level: case position_percentage
664
+ when 0..5 then :low
665
+ when 5..15 then :medium
666
+ when 15..30 then :high
667
+ else :very_high
668
+ end
669
+ }
670
+ end
671
+
672
+ def calculate_market_impact_risk(amount, price, order_book)
673
+ # Analyze order book depth
674
+ trade_side = order_book.asks.first(10) # Look at top 10 levels
675
+ cumulative_volume = 0
676
+ levels_needed = 0
677
+
678
+ trade_side.each do |level|
679
+ cumulative_volume += level.amount
680
+ levels_needed += 1
681
+ break if cumulative_volume >= amount
682
+ end
683
+
684
+ {
685
+ levels_needed: levels_needed,
686
+ available_volume: cumulative_volume,
687
+ impact_score: case levels_needed
688
+ when 1 then 1.0 # Low impact - fits in top level
689
+ when 2..3 then 2.0 # Medium impact
690
+ when 4..6 then 3.0 # High impact
691
+ else 4.0 # Very high impact
692
+ end
693
+ }
694
+ end
695
+
696
+ def calculate_trade_risk_score(trade_impact, position_risk, market_impact_risk)
697
+ # Combine different risk factors
698
+ price_risk = trade_impact[:price_impact_percent] / 5.0 # Normalize to 0-4 scale
699
+ position_risk_score = case position_risk[:risk_level]
700
+ when :low then 1.0
701
+ when :medium then 2.5
702
+ when :high then 4.0
703
+ when :very_high then 5.0
704
+ end
705
+ market_risk_score = market_impact_risk[:impact_score]
706
+
707
+ # Weighted average
708
+ (price_risk * 0.3 + position_risk_score * 0.4 + market_risk_score * 0.3)
709
+ end
710
+
711
+ def generate_trade_recommendations(risk_score, trade_impact)
712
+ recommendations = []
713
+
714
+ if risk_score > 3.5
715
+ recommendations << "🚨 High risk trade - consider reducing size"
716
+ end
717
+
718
+ if trade_impact[:size_impact_percent] > 20
719
+ recommendations << "📊 Large market impact - consider splitting into smaller orders"
720
+ end
721
+
722
+ if trade_impact[:price_deviation] > 5
723
+ recommendations << "💰 Significant price deviation - verify price is intentional"
724
+ end
725
+
726
+ recommendations << "✅ Trade looks reasonable" if recommendations.empty?
727
+
728
+ recommendations
729
+ end
730
+
731
+ def determine_best_stop_loss(ticker, position_value)
732
+ volatility = ticker.price_variation_24h.abs
733
+
734
+ case volatility
735
+ when 0..3
736
+ :conservative
737
+ when 3..8
738
+ :moderate
739
+ else
740
+ :aggressive
741
+ end
742
+ end
743
+
744
+ def generate_ai_risk_insights(risk_data)
745
+ return nil unless defined?(RubyLLM)
746
+
747
+ prompt = build_ai_risk_analysis_prompt(risk_data)
748
+
749
+ begin
750
+ response = @llm.complete(
751
+ messages: [{ role: "user", content: prompt }],
752
+ max_tokens: 300
753
+ )
754
+
755
+ {
756
+ analysis: response.content,
757
+ generated_at: Time.now
758
+ }
759
+ rescue => e
760
+ BudaApi::Logger.error("AI risk insights failed: #{e.message}")
761
+ nil
762
+ end
763
+ end
764
+
765
+ def build_ai_risk_analysis_prompt(risk_data)
766
+ """
767
+ Analyze this cryptocurrency portfolio risk assessment:
768
+
769
+ Portfolio Value: #{risk_data[:portfolio_value]} CLP
770
+ Holdings: #{risk_data[:currency_count]} currencies
771
+ Risk Level: #{risk_data[:overall_risk][:level]} (#{risk_data[:overall_risk][:score]}/5)
772
+
773
+ Risk Breakdown:
774
+ - Max Position: #{risk_data[:basic_metrics][:max_position_percentage].round(1)}%
775
+ - Diversification Score: #{risk_data[:advanced_metrics][:diversification_score].round(2)}
776
+ - Volatility Score: #{risk_data[:advanced_metrics][:volatility_score]}
777
+ - Correlation Risk: #{risk_data[:advanced_metrics][:correlation_risk]}
778
+
779
+ Provide a concise risk analysis with:
780
+ 1. Key concerns and strengths
781
+ 2. Specific improvement recommendations
782
+ 3. Market outlook considerations
783
+
784
+ Focus on actionable insights for Chilean crypto investors.
785
+ """
786
+ end
787
+ end
788
+ end
789
+ end