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,541 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BudaApi
|
4
|
+
module AI
|
5
|
+
# Natural language interface for trading operations
|
6
|
+
class NaturalLanguageTrader
|
7
|
+
TRADING_FUNCTIONS = [
|
8
|
+
{
|
9
|
+
name: "place_order",
|
10
|
+
description: "Place a buy or sell order on the exchange",
|
11
|
+
parameters: {
|
12
|
+
type: "object",
|
13
|
+
properties: {
|
14
|
+
market_id: {
|
15
|
+
type: "string",
|
16
|
+
description: "Trading pair (e.g., BTC-CLP)",
|
17
|
+
enum: BudaApi::Constants::Market::ALL
|
18
|
+
},
|
19
|
+
side: {
|
20
|
+
type: "string",
|
21
|
+
enum: ["buy", "sell"],
|
22
|
+
description: "Whether to buy or sell"
|
23
|
+
},
|
24
|
+
amount: {
|
25
|
+
type: "number",
|
26
|
+
description: "Amount to trade (in base currency)"
|
27
|
+
},
|
28
|
+
price: {
|
29
|
+
type: "number",
|
30
|
+
description: "Price per unit (optional for market orders)"
|
31
|
+
},
|
32
|
+
order_type: {
|
33
|
+
type: "string",
|
34
|
+
enum: ["market", "limit"],
|
35
|
+
description: "Order type - market executes immediately, limit waits for price"
|
36
|
+
}
|
37
|
+
},
|
38
|
+
required: ["market_id", "side", "amount"]
|
39
|
+
}
|
40
|
+
},
|
41
|
+
{
|
42
|
+
name: "check_balance",
|
43
|
+
description: "Check account balance for a specific currency",
|
44
|
+
parameters: {
|
45
|
+
type: "object",
|
46
|
+
properties: {
|
47
|
+
currency: {
|
48
|
+
type: "string",
|
49
|
+
description: "Currency code (e.g., BTC, CLP)",
|
50
|
+
enum: BudaApi::Constants::Currency::ALL
|
51
|
+
}
|
52
|
+
},
|
53
|
+
required: ["currency"]
|
54
|
+
}
|
55
|
+
},
|
56
|
+
{
|
57
|
+
name: "get_market_data",
|
58
|
+
description: "Get current market data including price and order book",
|
59
|
+
parameters: {
|
60
|
+
type: "object",
|
61
|
+
properties: {
|
62
|
+
market_id: {
|
63
|
+
type: "string",
|
64
|
+
description: "Trading pair (e.g., BTC-CLP)",
|
65
|
+
enum: BudaApi::Constants::Market::ALL
|
66
|
+
}
|
67
|
+
},
|
68
|
+
required: ["market_id"]
|
69
|
+
}
|
70
|
+
},
|
71
|
+
{
|
72
|
+
name: "cancel_order",
|
73
|
+
description: "Cancel an existing order",
|
74
|
+
parameters: {
|
75
|
+
type: "object",
|
76
|
+
properties: {
|
77
|
+
order_id: {
|
78
|
+
type: "integer",
|
79
|
+
description: "ID of the order to cancel"
|
80
|
+
}
|
81
|
+
},
|
82
|
+
required: ["order_id"]
|
83
|
+
}
|
84
|
+
},
|
85
|
+
{
|
86
|
+
name: "get_order_history",
|
87
|
+
description: "Get recent order history for a market",
|
88
|
+
parameters: {
|
89
|
+
type: "object",
|
90
|
+
properties: {
|
91
|
+
market_id: {
|
92
|
+
type: "string",
|
93
|
+
description: "Trading pair (e.g., BTC-CLP)",
|
94
|
+
enum: BudaApi::Constants::Market::ALL
|
95
|
+
},
|
96
|
+
limit: {
|
97
|
+
type: "integer",
|
98
|
+
description: "Number of orders to retrieve (max 100)",
|
99
|
+
maximum: 100
|
100
|
+
}
|
101
|
+
},
|
102
|
+
required: ["market_id"]
|
103
|
+
}
|
104
|
+
},
|
105
|
+
{
|
106
|
+
name: "get_quotation",
|
107
|
+
description: "Get price quotation for a potential trade",
|
108
|
+
parameters: {
|
109
|
+
type: "object",
|
110
|
+
properties: {
|
111
|
+
market_id: {
|
112
|
+
type: "string",
|
113
|
+
description: "Trading pair (e.g., BTC-CLP)",
|
114
|
+
enum: BudaApi::Constants::Market::ALL
|
115
|
+
},
|
116
|
+
side: {
|
117
|
+
type: "string",
|
118
|
+
enum: ["buy", "sell"],
|
119
|
+
description: "Whether you want to buy or sell"
|
120
|
+
},
|
121
|
+
amount: {
|
122
|
+
type: "number",
|
123
|
+
description: "Amount you want to trade"
|
124
|
+
}
|
125
|
+
},
|
126
|
+
required: ["market_id", "side", "amount"]
|
127
|
+
}
|
128
|
+
}
|
129
|
+
].freeze
|
130
|
+
|
131
|
+
def initialize(client, llm_provider: :openai)
|
132
|
+
@client = client
|
133
|
+
@llm = RubyLLM.new(
|
134
|
+
provider: llm_provider,
|
135
|
+
functions: TRADING_FUNCTIONS
|
136
|
+
)
|
137
|
+
@conversation_history = []
|
138
|
+
|
139
|
+
BudaApi::Logger.info("Natural Language Trader initialized")
|
140
|
+
end
|
141
|
+
|
142
|
+
# Execute a natural language trading command
|
143
|
+
#
|
144
|
+
# @param input [String] natural language command
|
145
|
+
# @param confirm_trades [Boolean] whether to confirm before placing orders
|
146
|
+
# @return [Hash] execution result
|
147
|
+
# @example
|
148
|
+
# trader = BudaApi.natural_language_trader(client)
|
149
|
+
# result = trader.execute_command("Check my BTC balance")
|
150
|
+
# result = trader.execute_command("Buy 0.001 BTC at market price")
|
151
|
+
def execute_command(input, confirm_trades: true)
|
152
|
+
BudaApi::Logger.info("Processing natural language command: #{input}")
|
153
|
+
|
154
|
+
# Add to conversation history
|
155
|
+
@conversation_history << { role: "user", content: input }
|
156
|
+
|
157
|
+
begin
|
158
|
+
response = @llm.complete(
|
159
|
+
messages: build_conversation_messages,
|
160
|
+
system_prompt: build_system_prompt,
|
161
|
+
max_tokens: 500
|
162
|
+
)
|
163
|
+
|
164
|
+
# Add assistant response to history
|
165
|
+
@conversation_history << { role: "assistant", content: response.content }
|
166
|
+
|
167
|
+
if response.function_call
|
168
|
+
result = execute_function_with_confirmation(response.function_call, confirm_trades)
|
169
|
+
|
170
|
+
# Add function result to conversation
|
171
|
+
@conversation_history << {
|
172
|
+
role: "function",
|
173
|
+
name: response.function_call.name,
|
174
|
+
content: result.to_json
|
175
|
+
}
|
176
|
+
|
177
|
+
result
|
178
|
+
else
|
179
|
+
{
|
180
|
+
type: :text_response,
|
181
|
+
content: response.content,
|
182
|
+
timestamp: Time.now
|
183
|
+
}
|
184
|
+
end
|
185
|
+
|
186
|
+
rescue => e
|
187
|
+
error_msg = "Failed to process command: #{e.message}"
|
188
|
+
BudaApi::Logger.error(error_msg)
|
189
|
+
|
190
|
+
{
|
191
|
+
type: :error,
|
192
|
+
error: error_msg,
|
193
|
+
timestamp: Time.now
|
194
|
+
}
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# Clear conversation history
|
199
|
+
def clear_history
|
200
|
+
@conversation_history.clear
|
201
|
+
BudaApi::Logger.info("Conversation history cleared")
|
202
|
+
end
|
203
|
+
|
204
|
+
# Get conversation history
|
205
|
+
# @return [Array<Hash>] conversation messages
|
206
|
+
def conversation_history
|
207
|
+
@conversation_history.dup
|
208
|
+
end
|
209
|
+
|
210
|
+
# Process batch commands
|
211
|
+
# @param commands [Array<String>] list of natural language commands
|
212
|
+
# @param confirm_trades [Boolean] whether to confirm trades
|
213
|
+
# @return [Array<Hash>] results for each command
|
214
|
+
def execute_batch(commands, confirm_trades: true)
|
215
|
+
results = []
|
216
|
+
|
217
|
+
commands.each_with_index do |command, index|
|
218
|
+
BudaApi::Logger.info("Processing batch command #{index + 1}/#{commands.length}")
|
219
|
+
result = execute_command(command, confirm_trades: confirm_trades)
|
220
|
+
results << result
|
221
|
+
|
222
|
+
# Add small delay between commands to avoid rate limiting
|
223
|
+
sleep(0.5) unless index == commands.length - 1
|
224
|
+
end
|
225
|
+
|
226
|
+
results
|
227
|
+
end
|
228
|
+
|
229
|
+
private
|
230
|
+
|
231
|
+
def build_system_prompt
|
232
|
+
"""
|
233
|
+
You are a cryptocurrency trading assistant for the Buda exchange in Chile.
|
234
|
+
|
235
|
+
CAPABILITIES:
|
236
|
+
- Check account balances for any supported currency
|
237
|
+
- Get real-time market data and prices
|
238
|
+
- Place buy and sell orders (market and limit orders)
|
239
|
+
- Cancel existing orders
|
240
|
+
- Get order history and trading records
|
241
|
+
- Provide price quotations for potential trades
|
242
|
+
|
243
|
+
SUPPORTED MARKETS: #{BudaApi::Constants::Market::ALL.join(', ')}
|
244
|
+
SUPPORTED CURRENCIES: #{BudaApi::Constants::Currency::ALL.join(', ')}
|
245
|
+
|
246
|
+
GUIDELINES:
|
247
|
+
1. Always confirm risky operations like placing orders
|
248
|
+
2. Be precise with numbers and avoid rounding errors
|
249
|
+
3. Explain what each function does before executing
|
250
|
+
4. Provide helpful context about market conditions
|
251
|
+
5. Use Chilean Peso (CLP) as the default quote currency
|
252
|
+
6. Warn about risks and fees when appropriate
|
253
|
+
|
254
|
+
IMPORTANT SAFETY RULES:
|
255
|
+
- Always double-check order parameters
|
256
|
+
- Warn about large orders that could impact the market
|
257
|
+
- Suggest limit orders for better price control
|
258
|
+
- Remind users about trading fees
|
259
|
+
|
260
|
+
When users ask about prices, trading, or market data, use the appropriate functions.
|
261
|
+
Always be helpful, accurate, and prioritize user safety.
|
262
|
+
"""
|
263
|
+
end
|
264
|
+
|
265
|
+
def build_conversation_messages
|
266
|
+
# Keep last 10 messages to maintain context while avoiding token limits
|
267
|
+
recent_history = @conversation_history.last(10)
|
268
|
+
|
269
|
+
messages = []
|
270
|
+
recent_history.each do |msg|
|
271
|
+
case msg[:role]
|
272
|
+
when "user", "assistant"
|
273
|
+
messages << { role: msg[:role], content: msg[:content] }
|
274
|
+
when "function"
|
275
|
+
messages << {
|
276
|
+
role: "function",
|
277
|
+
name: msg[:name],
|
278
|
+
content: msg[:content]
|
279
|
+
}
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
messages
|
284
|
+
end
|
285
|
+
|
286
|
+
def execute_function_with_confirmation(function_call, confirm_trades)
|
287
|
+
function_name = function_call.name
|
288
|
+
arguments = function_call.arguments
|
289
|
+
|
290
|
+
BudaApi::Logger.info("Executing function: #{function_name} with args: #{arguments}")
|
291
|
+
|
292
|
+
# Check if this is a trading operation that needs confirmation
|
293
|
+
if trading_function?(function_name) && confirm_trades
|
294
|
+
confirmation = request_confirmation(function_name, arguments)
|
295
|
+
return confirmation unless confirmation[:confirmed]
|
296
|
+
end
|
297
|
+
|
298
|
+
case function_name
|
299
|
+
when "place_order"
|
300
|
+
place_order_from_params(arguments)
|
301
|
+
when "check_balance"
|
302
|
+
check_balance_from_params(arguments)
|
303
|
+
when "get_market_data"
|
304
|
+
get_market_data_from_params(arguments)
|
305
|
+
when "cancel_order"
|
306
|
+
cancel_order_from_params(arguments)
|
307
|
+
when "get_order_history"
|
308
|
+
get_order_history_from_params(arguments)
|
309
|
+
when "get_quotation"
|
310
|
+
get_quotation_from_params(arguments)
|
311
|
+
else
|
312
|
+
{
|
313
|
+
type: :error,
|
314
|
+
error: "Unknown function: #{function_name}",
|
315
|
+
timestamp: Time.now
|
316
|
+
}
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
def trading_function?(function_name)
|
321
|
+
%w[place_order cancel_order].include?(function_name)
|
322
|
+
end
|
323
|
+
|
324
|
+
def request_confirmation(function_name, arguments)
|
325
|
+
case function_name
|
326
|
+
when "place_order"
|
327
|
+
{
|
328
|
+
type: :confirmation_required,
|
329
|
+
message: "⚠️ About to place #{arguments['side']} order: #{arguments['amount']} #{arguments['market_id']} at #{arguments['price'] || 'market price'}. Confirm?",
|
330
|
+
function: function_name,
|
331
|
+
arguments: arguments,
|
332
|
+
confirmed: false,
|
333
|
+
timestamp: Time.now
|
334
|
+
}
|
335
|
+
when "cancel_order"
|
336
|
+
{
|
337
|
+
type: :confirmation_required,
|
338
|
+
message: "⚠️ About to cancel order ##{arguments['order_id']}. Confirm?",
|
339
|
+
function: function_name,
|
340
|
+
arguments: arguments,
|
341
|
+
confirmed: false,
|
342
|
+
timestamp: Time.now
|
343
|
+
}
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
def place_order_from_params(params)
|
348
|
+
market_id = params["market_id"]
|
349
|
+
side = params["side"]
|
350
|
+
amount = params["amount"].to_f
|
351
|
+
price = params["price"]&.to_f
|
352
|
+
order_type = params["order_type"] || (price ? "limit" : "market")
|
353
|
+
|
354
|
+
# Convert side to Buda API format
|
355
|
+
buda_order_type = side == "buy" ? "Bid" : "Ask"
|
356
|
+
buda_price_type = order_type == "market" ? "market" : "limit"
|
357
|
+
|
358
|
+
begin
|
359
|
+
order = @client.place_order(market_id, buda_order_type, buda_price_type, amount, price)
|
360
|
+
|
361
|
+
{
|
362
|
+
type: :order_placed,
|
363
|
+
order_id: order.id,
|
364
|
+
market_id: market_id,
|
365
|
+
side: side,
|
366
|
+
amount: amount,
|
367
|
+
price: price,
|
368
|
+
order_type: order_type,
|
369
|
+
status: order.state,
|
370
|
+
message: "✅ Order placed successfully! Order ID: #{order.id}",
|
371
|
+
data: order,
|
372
|
+
timestamp: Time.now
|
373
|
+
}
|
374
|
+
rescue BudaApi::ApiError => e
|
375
|
+
{
|
376
|
+
type: :order_failed,
|
377
|
+
error: e.message,
|
378
|
+
market_id: market_id,
|
379
|
+
side: side,
|
380
|
+
amount: amount,
|
381
|
+
message: "❌ Failed to place order: #{e.message}",
|
382
|
+
timestamp: Time.now
|
383
|
+
}
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
def check_balance_from_params(params)
|
388
|
+
currency = params["currency"]
|
389
|
+
|
390
|
+
begin
|
391
|
+
balance = @client.balance(currency)
|
392
|
+
|
393
|
+
{
|
394
|
+
type: :balance_info,
|
395
|
+
currency: currency,
|
396
|
+
total: balance.amount.amount,
|
397
|
+
available: balance.available_amount.amount,
|
398
|
+
frozen: balance.frozen_amount.amount,
|
399
|
+
pending_withdrawals: balance.pending_withdraw_amount.amount,
|
400
|
+
message: "💰 #{currency} Balance: #{balance.available_amount} available, #{balance.frozen_amount} frozen",
|
401
|
+
data: balance,
|
402
|
+
timestamp: Time.now
|
403
|
+
}
|
404
|
+
rescue BudaApi::ApiError => e
|
405
|
+
{
|
406
|
+
type: :balance_error,
|
407
|
+
error: e.message,
|
408
|
+
currency: currency,
|
409
|
+
message: "❌ Failed to get balance: #{e.message}",
|
410
|
+
timestamp: Time.now
|
411
|
+
}
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
def get_market_data_from_params(params)
|
416
|
+
market_id = params["market_id"]
|
417
|
+
|
418
|
+
begin
|
419
|
+
ticker = @client.ticker(market_id)
|
420
|
+
order_book = @client.order_book(market_id)
|
421
|
+
|
422
|
+
{
|
423
|
+
type: :market_data,
|
424
|
+
market_id: market_id,
|
425
|
+
price: ticker.last_price.amount,
|
426
|
+
change_24h: ticker.price_variation_24h,
|
427
|
+
volume: ticker.volume.amount,
|
428
|
+
best_ask: order_book.best_ask.price,
|
429
|
+
best_bid: order_book.best_bid.price,
|
430
|
+
spread: order_book.spread_percentage,
|
431
|
+
message: "📊 #{market_id}: #{ticker.last_price} (#{ticker.price_variation_24h > 0 ? '+' : ''}#{ticker.price_variation_24h}%)",
|
432
|
+
data: { ticker: ticker, order_book: order_book },
|
433
|
+
timestamp: Time.now
|
434
|
+
}
|
435
|
+
rescue BudaApi::ApiError => e
|
436
|
+
{
|
437
|
+
type: :market_data_error,
|
438
|
+
error: e.message,
|
439
|
+
market_id: market_id,
|
440
|
+
message: "❌ Failed to get market data: #{e.message}",
|
441
|
+
timestamp: Time.now
|
442
|
+
}
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
def cancel_order_from_params(params)
|
447
|
+
order_id = params["order_id"]
|
448
|
+
|
449
|
+
begin
|
450
|
+
cancelled_order = @client.cancel_order(order_id)
|
451
|
+
|
452
|
+
{
|
453
|
+
type: :order_cancelled,
|
454
|
+
order_id: order_id,
|
455
|
+
status: cancelled_order.state,
|
456
|
+
message: "✅ Order ##{order_id} cancelled successfully",
|
457
|
+
data: cancelled_order,
|
458
|
+
timestamp: Time.now
|
459
|
+
}
|
460
|
+
rescue BudaApi::ApiError => e
|
461
|
+
{
|
462
|
+
type: :cancel_failed,
|
463
|
+
error: e.message,
|
464
|
+
order_id: order_id,
|
465
|
+
message: "❌ Failed to cancel order: #{e.message}",
|
466
|
+
timestamp: Time.now
|
467
|
+
}
|
468
|
+
end
|
469
|
+
end
|
470
|
+
|
471
|
+
def get_order_history_from_params(params)
|
472
|
+
market_id = params["market_id"]
|
473
|
+
limit = [params["limit"]&.to_i || 10, 100].min
|
474
|
+
|
475
|
+
begin
|
476
|
+
orders_result = @client.orders(market_id, per_page: limit)
|
477
|
+
|
478
|
+
{
|
479
|
+
type: :order_history,
|
480
|
+
market_id: market_id,
|
481
|
+
orders_count: orders_result.count,
|
482
|
+
orders: orders_result.orders.map do |order|
|
483
|
+
{
|
484
|
+
id: order.id,
|
485
|
+
type: order.type,
|
486
|
+
amount: order.amount.amount,
|
487
|
+
price: order.limit&.amount,
|
488
|
+
state: order.state,
|
489
|
+
created_at: order.created_at
|
490
|
+
}
|
491
|
+
end,
|
492
|
+
message: "📋 Found #{orders_result.count} orders for #{market_id}",
|
493
|
+
data: orders_result,
|
494
|
+
timestamp: Time.now
|
495
|
+
}
|
496
|
+
rescue BudaApi::ApiError => e
|
497
|
+
{
|
498
|
+
type: :history_error,
|
499
|
+
error: e.message,
|
500
|
+
market_id: market_id,
|
501
|
+
message: "❌ Failed to get order history: #{e.message}",
|
502
|
+
timestamp: Time.now
|
503
|
+
}
|
504
|
+
end
|
505
|
+
end
|
506
|
+
|
507
|
+
def get_quotation_from_params(params)
|
508
|
+
market_id = params["market_id"]
|
509
|
+
side = params["side"]
|
510
|
+
amount = params["amount"].to_f
|
511
|
+
|
512
|
+
# Convert side to quotation type
|
513
|
+
quotation_type = side == "buy" ? "bid_given_size" : "ask_given_size"
|
514
|
+
|
515
|
+
begin
|
516
|
+
quotation = @client.quotation(market_id, quotation_type, amount)
|
517
|
+
|
518
|
+
{
|
519
|
+
type: :quotation,
|
520
|
+
market_id: market_id,
|
521
|
+
side: side,
|
522
|
+
amount: amount,
|
523
|
+
estimated_cost: quotation.quote_balance_change.amount,
|
524
|
+
fee: quotation.fee.amount,
|
525
|
+
message: "💱 To #{side} #{amount} #{market_id.split('-').first}: ~#{quotation.quote_balance_change} (fee: #{quotation.fee})",
|
526
|
+
data: quotation,
|
527
|
+
timestamp: Time.now
|
528
|
+
}
|
529
|
+
rescue BudaApi::ApiError => e
|
530
|
+
{
|
531
|
+
type: :quotation_error,
|
532
|
+
error: e.message,
|
533
|
+
market_id: market_id,
|
534
|
+
message: "❌ Failed to get quotation: #{e.message}",
|
535
|
+
timestamp: Time.now
|
536
|
+
}
|
537
|
+
end
|
538
|
+
end
|
539
|
+
end
|
540
|
+
end
|
541
|
+
end
|