buda_api 1.0.0

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,365 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ostruct"
4
+ require "time"
5
+
6
+ module BudaApi
7
+ # Data models for API responses
8
+ module Models
9
+ # Base model class with common functionality
10
+ class BaseModel
11
+ def initialize(data = {})
12
+ @data = data.is_a?(Hash) ? data : {}
13
+ @raw_data = @data.dup
14
+ end
15
+
16
+ # Access to raw API response data
17
+ def raw
18
+ @raw_data
19
+ end
20
+
21
+ # Convert to hash
22
+ def to_h
23
+ @data
24
+ end
25
+
26
+ # Convert to JSON
27
+ def to_json(*args)
28
+ @data.to_json(*args)
29
+ end
30
+
31
+ private
32
+
33
+ def parse_datetime(datetime_string)
34
+ return nil if datetime_string.nil? || datetime_string.empty?
35
+
36
+ Time.parse(datetime_string)
37
+ rescue ArgumentError
38
+ nil
39
+ end
40
+
41
+ def parse_amount(amount_data)
42
+ return nil unless amount_data.is_a?(Hash)
43
+
44
+ Amount.new(amount_data)
45
+ end
46
+ end
47
+
48
+ # Amount model for currency amounts
49
+ class Amount < BaseModel
50
+ def initialize(data)
51
+ super(data)
52
+ @amount = data["amount"]&.to_f || 0.0
53
+ @currency = data["currency"]
54
+ end
55
+
56
+ attr_reader :amount, :currency
57
+
58
+ def to_s
59
+ "#{@amount} #{@currency}"
60
+ end
61
+
62
+ def ==(other)
63
+ other.is_a?(Amount) &&
64
+ @amount == other.amount &&
65
+ @currency == other.currency
66
+ end
67
+ end
68
+
69
+ # Market model
70
+ class Market < BaseModel
71
+ def initialize(data)
72
+ super(data)
73
+ @id = data["id"]
74
+ @name = data["name"]
75
+ @base_currency = data["base_currency"]
76
+ @quote_currency = data["quote_currency"]
77
+ @minimum_order_amount = parse_amount(data["minimum_order_amount"])
78
+ end
79
+
80
+ attr_reader :id, :name, :base_currency, :quote_currency, :minimum_order_amount
81
+ end
82
+
83
+ # Ticker model
84
+ class Ticker < BaseModel
85
+ def initialize(data)
86
+ super(data)
87
+ @last_price = parse_amount(data["last_price"])
88
+ @min_ask = parse_amount(data["min_ask"])
89
+ @max_bid = parse_amount(data["max_bid"])
90
+ @volume = parse_amount(data["volume"])
91
+ @price_variation_24h = data["price_variation_24h"]&.to_f
92
+ @price_variation_7d = data["price_variation_7d"]&.to_f
93
+ end
94
+
95
+ attr_reader :last_price, :min_ask, :max_bid, :volume,
96
+ :price_variation_24h, :price_variation_7d
97
+ end
98
+
99
+ # Order book entry model
100
+ class OrderBookEntry < BaseModel
101
+ def initialize(data)
102
+ super(data)
103
+ if data.is_a?(Array) && data.length >= 2
104
+ @price = data[0].to_f
105
+ @amount = data[1].to_f
106
+ else
107
+ @price = data["price"]&.to_f || 0.0
108
+ @amount = data["amount"]&.to_f || 0.0
109
+ end
110
+ end
111
+
112
+ attr_reader :price, :amount
113
+
114
+ def total
115
+ @price * @amount
116
+ end
117
+ end
118
+
119
+ # Order book model
120
+ class OrderBook < BaseModel
121
+ def initialize(data)
122
+ super(data)
123
+ @asks = (data["asks"] || []).map { |entry| OrderBookEntry.new(entry) }
124
+ @bids = (data["bids"] || []).map { |entry| OrderBookEntry.new(entry) }
125
+ end
126
+
127
+ attr_reader :asks, :bids
128
+
129
+ def best_ask
130
+ @asks.first
131
+ end
132
+
133
+ def best_bid
134
+ @bids.first
135
+ end
136
+
137
+ def spread
138
+ return nil unless best_ask && best_bid
139
+
140
+ best_ask.price - best_bid.price
141
+ end
142
+
143
+ def spread_percentage
144
+ return nil unless best_ask && best_bid && best_bid.price > 0
145
+
146
+ ((best_ask.price - best_bid.price) / best_bid.price * 100).round(4)
147
+ end
148
+ end
149
+
150
+ # Trade model
151
+ class Trade < BaseModel
152
+ def initialize(data)
153
+ super(data)
154
+ @timestamp = parse_datetime(data["timestamp"]) || Time.at(data["timestamp"].to_i) if data["timestamp"]
155
+ @direction = data["direction"]
156
+ @price = parse_amount(data["price"])
157
+ @amount = parse_amount(data["amount"])
158
+ @market_id = data["market_id"]
159
+ end
160
+
161
+ attr_reader :timestamp, :direction, :price, :amount, :market_id
162
+ end
163
+
164
+ # Trades collection model
165
+ class Trades < BaseModel
166
+ def initialize(data)
167
+ super(data)
168
+ @trades = (data["trades"] || []).map { |trade_data| Trade.new(trade_data) }
169
+ @last_timestamp = data["last_timestamp"]
170
+ end
171
+
172
+ attr_reader :trades, :last_timestamp
173
+
174
+ def count
175
+ @trades.length
176
+ end
177
+
178
+ def each(&block)
179
+ @trades.each(&block)
180
+ end
181
+ end
182
+
183
+ # Balance model
184
+ class Balance < BaseModel
185
+ def initialize(data)
186
+ super(data)
187
+ @id = data["id"]
188
+ @account_id = data["account_id"]
189
+ @amount = parse_amount(data["amount"])
190
+ @available_amount = parse_amount(data["available_amount"])
191
+ @frozen_amount = parse_amount(data["frozen_amount"])
192
+ @pending_withdraw_amount = parse_amount(data["pending_withdraw_amount"])
193
+ end
194
+
195
+ attr_reader :id, :account_id, :amount, :available_amount,
196
+ :frozen_amount, :pending_withdraw_amount
197
+
198
+ def currency
199
+ @amount&.currency
200
+ end
201
+ end
202
+
203
+ # Order model
204
+ class Order < BaseModel
205
+ def initialize(data)
206
+ super(data)
207
+ @id = data["id"]
208
+ @account_id = data["account_id"]
209
+ @amount = parse_amount(data["amount"])
210
+ @created_at = parse_datetime(data["created_at"])
211
+ @fee_currency = data["fee_currency"]
212
+ @limit = parse_amount(data["limit"])
213
+ @market_id = data["market_id"]
214
+ @original_amount = parse_amount(data["original_amount"])
215
+ @paid_fee = parse_amount(data["paid_fee"])
216
+ @price_type = data["price_type"]
217
+ @state = data["state"]
218
+ @total_exchanged = parse_amount(data["total_exchanged"])
219
+ @traded_amount = parse_amount(data["traded_amount"])
220
+ @type = data["type"]
221
+ end
222
+
223
+ attr_reader :id, :account_id, :amount, :created_at, :fee_currency,
224
+ :limit, :market_id, :original_amount, :paid_fee, :price_type,
225
+ :state, :total_exchanged, :traded_amount, :type
226
+
227
+ def filled_percentage
228
+ return 0 unless @original_amount&.amount && @original_amount.amount > 0
229
+
230
+ ((@traded_amount&.amount || 0) / @original_amount.amount * 100).round(2)
231
+ end
232
+
233
+ def is_filled?
234
+ @state == "traded"
235
+ end
236
+
237
+ def is_active?
238
+ %w[received pending].include?(@state)
239
+ end
240
+
241
+ def is_cancelled?
242
+ %w[canceled canceling].include?(@state)
243
+ end
244
+ end
245
+
246
+ # Quotation model
247
+ class Quotation < BaseModel
248
+ def initialize(data)
249
+ super(data)
250
+ @type = data["type"]
251
+ @reverse_amount = parse_amount(data["reverse_amount"])
252
+ @amount = parse_amount(data["amount"])
253
+ @base_balance_change = parse_amount(data["base_balance_change"])
254
+ @quote_balance_change = parse_amount(data["quote_balance_change"])
255
+ @fee = parse_amount(data["fee"])
256
+ end
257
+
258
+ attr_reader :type, :reverse_amount, :amount, :base_balance_change,
259
+ :quote_balance_change, :fee
260
+ end
261
+
262
+ # Withdrawal model
263
+ class Withdrawal < BaseModel
264
+ def initialize(data)
265
+ super(data)
266
+ @id = data["id"]
267
+ @created_at = parse_datetime(data["created_at"])
268
+ @amount = parse_amount(data["amount"])
269
+ @fee = parse_amount(data["fee"])
270
+ @currency = data["currency"]
271
+ @state = data["state"]
272
+ @withdrawal_data = data["withdrawal_data"]
273
+ end
274
+
275
+ attr_reader :id, :created_at, :amount, :fee, :currency, :state, :withdrawal_data
276
+
277
+ def target_address
278
+ @withdrawal_data&.dig("target_address")
279
+ end
280
+ end
281
+
282
+ # Deposit model
283
+ class Deposit < BaseModel
284
+ def initialize(data)
285
+ super(data)
286
+ @id = data["id"]
287
+ @created_at = parse_datetime(data["created_at"])
288
+ @amount = parse_amount(data["amount"])
289
+ @currency = data["currency"]
290
+ @state = data["state"]
291
+ @deposit_data = data["deposit_data"]
292
+ end
293
+
294
+ attr_reader :id, :created_at, :amount, :currency, :state, :deposit_data
295
+
296
+ def address
297
+ @deposit_data&.dig("address")
298
+ end
299
+ end
300
+
301
+ # Pagination metadata
302
+ class PaginationMeta < BaseModel
303
+ def initialize(data)
304
+ super(data)
305
+ @current_page = data["current_page"]&.to_i
306
+ @total_count = data["total_count"]&.to_i
307
+ @total_pages = data["total_pages"]&.to_i
308
+ end
309
+
310
+ attr_reader :current_page, :total_count, :total_pages
311
+
312
+ def has_next_page?
313
+ @current_page && @total_pages && @current_page < @total_pages
314
+ end
315
+
316
+ def has_previous_page?
317
+ @current_page && @current_page > 1
318
+ end
319
+ end
320
+
321
+ # Paginated collection of orders
322
+ class OrderPages < BaseModel
323
+ def initialize(orders_data, meta_data)
324
+ @orders = orders_data.map { |order| Order.new(order) }
325
+ @meta = PaginationMeta.new(meta_data || {})
326
+ end
327
+
328
+ attr_reader :orders, :meta
329
+
330
+ def count
331
+ @orders.length
332
+ end
333
+
334
+ def each(&block)
335
+ @orders.each(&block)
336
+ end
337
+ end
338
+
339
+ # Average price report entry
340
+ class AveragePrice < BaseModel
341
+ def initialize(data)
342
+ super(data)
343
+ @timestamp = Time.at(data["timestamp"].to_i) if data["timestamp"]
344
+ @average = data["average"]&.to_f
345
+ end
346
+
347
+ attr_reader :timestamp, :average
348
+ end
349
+
350
+ # Candlestick report entry
351
+ class Candlestick < BaseModel
352
+ def initialize(data)
353
+ super(data)
354
+ @timestamp = Time.at(data["timestamp"].to_i) if data["timestamp"]
355
+ @open = data["open"]&.to_f
356
+ @close = data["close"]&.to_f
357
+ @high = data["high"]&.to_f
358
+ @low = data["low"]&.to_f
359
+ @volume = data["volume"]&.to_f
360
+ end
361
+
362
+ attr_reader :timestamp, :open, :close, :high, :low, :volume
363
+ end
364
+ end
365
+ end
@@ -0,0 +1,231 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BudaApi
4
+ # Public API client for endpoints that don't require authentication
5
+ class PublicClient < Client
6
+ include Models
7
+
8
+ # Get all available markets
9
+ #
10
+ # @return [Array<Market>] list of available markets
11
+ # @example
12
+ # client = BudaApi::PublicClient.new
13
+ # markets = client.markets
14
+ # markets.each { |market| puts "#{market.id}: #{market.name}" }
15
+ def markets
16
+ BudaApi::Logger.info("Fetching all markets")
17
+
18
+ response = get("markets")
19
+ markets_data = response["markets"] || []
20
+
21
+ markets = markets_data.map { |market_data| Market.new(market_data) }
22
+ BudaApi::Logger.info("Retrieved #{markets.length} markets")
23
+
24
+ markets
25
+ end
26
+
27
+ # Get details for a specific market
28
+ #
29
+ # @param market_id [String] market identifier (e.g., "BTC-CLP")
30
+ # @return [Market] market details
31
+ # @example
32
+ # client = BudaApi::PublicClient.new
33
+ # market = client.market_details("BTC-CLP")
34
+ # puts "Minimum order: #{market.minimum_order_amount}"
35
+ def market_details(market_id)
36
+ validate_required_params({ market_id: market_id }, [:market_id])
37
+ validate_param_values({ market_id: market_id }, { market_id: Market::ALL })
38
+
39
+ BudaApi::Logger.info("Fetching market details for #{market_id}")
40
+
41
+ response = get("markets/#{market_id}")
42
+ Market.new(response["market"])
43
+ end
44
+
45
+ # Get ticker information for a market
46
+ #
47
+ # @param market_id [String] market identifier
48
+ # @return [Ticker] current ticker information
49
+ # @example
50
+ # client = BudaApi::PublicClient.new
51
+ # ticker = client.ticker("BTC-CLP")
52
+ # puts "Last price: #{ticker.last_price}"
53
+ # puts "24h change: #{ticker.price_variation_24h}%"
54
+ def ticker(market_id)
55
+ validate_required_params({ market_id: market_id }, [:market_id])
56
+ validate_param_values({ market_id: market_id }, { market_id: Market::ALL })
57
+
58
+ BudaApi::Logger.info("Fetching ticker for #{market_id}")
59
+
60
+ response = get("markets/#{market_id}/ticker")
61
+ Ticker.new(response["ticker"])
62
+ end
63
+
64
+ # Get order book for a market
65
+ #
66
+ # @param market_id [String] market identifier
67
+ # @return [OrderBook] current order book
68
+ # @example
69
+ # client = BudaApi::PublicClient.new
70
+ # order_book = client.order_book("BTC-CLP")
71
+ # puts "Best ask: #{order_book.best_ask.price}"
72
+ # puts "Best bid: #{order_book.best_bid.price}"
73
+ # puts "Spread: #{order_book.spread_percentage}%"
74
+ def order_book(market_id)
75
+ validate_required_params({ market_id: market_id }, [:market_id])
76
+ validate_param_values({ market_id: market_id }, { market_id: Market::ALL })
77
+
78
+ BudaApi::Logger.info("Fetching order book for #{market_id}")
79
+
80
+ response = get("markets/#{market_id}/order_book")
81
+ OrderBook.new(response["order_book"])
82
+ end
83
+
84
+ # Get recent trades for a market
85
+ #
86
+ # @param market_id [String] market identifier
87
+ # @param timestamp [Integer, nil] trades after this timestamp
88
+ # @param limit [Integer, nil] maximum number of trades to return
89
+ # @return [Trades] collection of recent trades
90
+ # @example
91
+ # client = BudaApi::PublicClient.new
92
+ # trades = client.trades("BTC-CLP", limit: 10)
93
+ # trades.each { |trade| puts "#{trade.amount} at #{trade.price}" }
94
+ def trades(market_id, timestamp: nil, limit: nil)
95
+ validate_required_params({ market_id: market_id }, [:market_id])
96
+ validate_param_values({ market_id: market_id }, { market_id: Market::ALL })
97
+
98
+ params = normalize_params({
99
+ timestamp: timestamp,
100
+ limit: limit
101
+ })
102
+
103
+ BudaApi::Logger.info("Fetching trades for #{market_id} with params: #{params}")
104
+
105
+ response = get("markets/#{market_id}/trades", params)
106
+ Trades.new(response["trades"])
107
+ end
108
+
109
+ # Get a quotation for a potential trade
110
+ #
111
+ # @param market_id [String] market identifier
112
+ # @param quotation_type [String] type of quotation
113
+ # @param amount [Float] amount for quotation
114
+ # @param limit [Float, nil] limit price for limit quotations
115
+ # @return [Quotation] quotation details
116
+ # @example
117
+ # client = BudaApi::PublicClient.new
118
+ # # Get quotation for buying 0.1 BTC at market price
119
+ # quote = client.quotation("BTC-CLP", "bid_given_size", 0.1)
120
+ # puts "You would pay: #{quote.quote_balance_change}"
121
+ def quotation(market_id, quotation_type, amount, limit: nil)
122
+ validate_required_params({
123
+ market_id: market_id,
124
+ quotation_type: quotation_type,
125
+ amount: amount
126
+ }, [:market_id, :quotation_type, :amount])
127
+
128
+ validate_param_values({
129
+ market_id: market_id,
130
+ quotation_type: quotation_type
131
+ }, {
132
+ market_id: Market::ALL,
133
+ quotation_type: QuotationType::ALL
134
+ })
135
+
136
+ quotation_payload = {
137
+ type: quotation_type,
138
+ amount: amount.to_s
139
+ }
140
+ quotation_payload[:limit] = limit.to_s if limit
141
+
142
+ BudaApi::Logger.info("Getting quotation for #{market_id}: #{quotation_type} #{amount}")
143
+
144
+ response = post("markets/#{market_id}/quotations", body: { quotation: quotation_payload })
145
+ Quotation.new(response["quotation"])
146
+ end
147
+
148
+ # Get a market quotation (no limit price)
149
+ #
150
+ # @param market_id [String] market identifier
151
+ # @param quotation_type [String] type of quotation
152
+ # @param amount [Float] amount for quotation
153
+ # @return [Quotation] market quotation details
154
+ def quotation_market(market_id, quotation_type, amount)
155
+ quotation(market_id, quotation_type, amount)
156
+ end
157
+
158
+ # Get a limit quotation (with limit price)
159
+ #
160
+ # @param market_id [String] market identifier
161
+ # @param quotation_type [String] type of quotation
162
+ # @param amount [Float] amount for quotation
163
+ # @param limit [Float] limit price
164
+ # @return [Quotation] limit quotation details
165
+ def quotation_limit(market_id, quotation_type, amount, limit)
166
+ quotation(market_id, quotation_type, amount, limit: limit)
167
+ end
168
+
169
+ # Get average prices report
170
+ #
171
+ # @param market_id [String] market identifier
172
+ # @param start_at [Time, nil] start time for report
173
+ # @param end_at [Time, nil] end time for report
174
+ # @return [Array<AveragePrice>] average price data points
175
+ # @example
176
+ # client = BudaApi::PublicClient.new
177
+ # start_time = Time.now - 86400 # 24 hours ago
178
+ # prices = client.average_prices_report("BTC-CLP", start_at: start_time)
179
+ def average_prices_report(market_id, start_at: nil, end_at: nil)
180
+ get_report(market_id, ReportType::AVERAGE_PRICES, start_at, end_at) do |report_data|
181
+ AveragePrice.new(report_data)
182
+ end
183
+ end
184
+
185
+ # Get candlestick report
186
+ #
187
+ # @param market_id [String] market identifier
188
+ # @param start_at [Time, nil] start time for report
189
+ # @param end_at [Time, nil] end time for report
190
+ # @return [Array<Candlestick>] candlestick data points
191
+ # @example
192
+ # client = BudaApi::PublicClient.new
193
+ # start_time = Time.now - 86400 # 24 hours ago
194
+ # candles = client.candlestick_report("BTC-CLP", start_at: start_time)
195
+ def candlestick_report(market_id, start_at: nil, end_at: nil)
196
+ get_report(market_id, ReportType::CANDLESTICK, start_at, end_at) do |report_data|
197
+ Candlestick.new(report_data)
198
+ end
199
+ end
200
+
201
+ private
202
+
203
+ def get_report(market_id, report_type, start_at, end_at)
204
+ validate_required_params({
205
+ market_id: market_id,
206
+ report_type: report_type
207
+ }, [:market_id, :report_type])
208
+
209
+ validate_param_values({
210
+ market_id: market_id,
211
+ report_type: report_type
212
+ }, {
213
+ market_id: Market::ALL,
214
+ report_type: ReportType::ALL
215
+ })
216
+
217
+ params = normalize_params({
218
+ report_type: report_type,
219
+ from: start_at&.to_i,
220
+ to: end_at&.to_i
221
+ })
222
+
223
+ BudaApi::Logger.info("Fetching #{report_type} report for #{market_id}")
224
+
225
+ response = get("markets/#{market_id}/reports", params)
226
+ reports_data = response["reports"] || []
227
+
228
+ reports_data.map { |report_data| yield(report_data) }
229
+ end
230
+ end
231
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BudaApi
4
+ VERSION = "1.0.0"
5
+ end
data/lib/buda_api.rb ADDED
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "buda_api/version"
4
+ require_relative "buda_api/client"
5
+ require_relative "buda_api/public_client"
6
+ require_relative "buda_api/authenticated_client"
7
+ require_relative "buda_api/models"
8
+ require_relative "buda_api/constants"
9
+ require_relative "buda_api/errors"
10
+ require_relative "buda_api/logger"
11
+
12
+ # BudaApi is the main module for the Buda.com API Ruby SDK
13
+ #
14
+ # This SDK provides comprehensive access to the Buda.com cryptocurrency exchange API
15
+ # with built-in error handling, debugging capabilities, and extensive examples.
16
+ #
17
+ # @example Basic usage with public API
18
+ # client = BudaApi::PublicClient.new
19
+ # markets = client.markets
20
+ # ticker = client.ticker("BTC-CLP")
21
+ #
22
+ # @example Authenticated API usage
23
+ # client = BudaApi::AuthenticatedClient.new(api_key: "your_key", api_secret: "your_secret")
24
+ # balance = client.balance("BTC")
25
+ # order = client.place_order("BTC-CLP", "ask", "limit", 1000000, 0.001)
26
+ #
27
+ module BudaApi
28
+ class Error < StandardError; end
29
+
30
+ # Configure the SDK with global settings
31
+ class Configuration
32
+ attr_accessor :base_url, :timeout, :retries, :debug_mode, :logger_level
33
+
34
+ def initialize
35
+ @base_url = "https://www.buda.com/api/v2/"
36
+ @timeout = 30
37
+ @retries = 3
38
+ @debug_mode = false
39
+ @logger_level = :info
40
+ end
41
+ end
42
+
43
+ class << self
44
+ attr_accessor :configuration
45
+ end
46
+
47
+ # Configure the SDK
48
+ #
49
+ # @yield [Configuration] configuration object
50
+ # @example
51
+ # BudaApi.configure do |config|
52
+ # config.debug_mode = true
53
+ # config.timeout = 60
54
+ # end
55
+ def self.configure
56
+ self.configuration ||= Configuration.new
57
+ yield(configuration)
58
+ end
59
+
60
+ # Get current configuration
61
+ # @return [Configuration] current configuration
62
+ def self.configuration
63
+ @configuration ||= Configuration.new
64
+ end
65
+
66
+ # Convenience method to create a public client
67
+ # @param options [Hash] client options
68
+ # @return [PublicClient] new public client instance
69
+ def self.public_client(options = {})
70
+ PublicClient.new(options)
71
+ end
72
+
73
+ # Convenience method to create an authenticated client
74
+ # @param api_key [String] API key
75
+ # @param api_secret [String] API secret
76
+ # @param options [Hash] additional client options
77
+ # @return [AuthenticatedClient] new authenticated client instance
78
+ def self.authenticated_client(api_key:, api_secret:, **options)
79
+ AuthenticatedClient.new(api_key: api_key, api_secret: api_secret, **options)
80
+ end
81
+ end