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.
- checksums.yaml +4 -4
- data/README.md +101 -4
- data/buda_api.gemspec +4 -1
- data/examples/ai/README.md +314 -0
- data/examples/ai/anomaly_detection_example.rb +412 -0
- data/examples/ai/natural_language_trading.rb +369 -0
- data/examples/ai/report_generation_example.rb +605 -0
- data/examples/ai/risk_management_example.rb +300 -0
- data/examples/ai/trading_assistant_example.rb +295 -0
- data/lib/buda_api/ai/anomaly_detector.rb +787 -0
- data/lib/buda_api/ai/natural_language_trader.rb +541 -0
- data/lib/buda_api/ai/report_generator.rb +1054 -0
- data/lib/buda_api/ai/risk_manager.rb +789 -0
- data/lib/buda_api/ai/trading_assistant.rb +404 -0
- data/lib/buda_api/version.rb +1 -1
- data/lib/buda_api.rb +37 -0
- metadata +32 -1
@@ -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
|