cryptomnio 0.2.2

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9ad53c1623ebabab3c246cd9a2e4a2a60330f5d441b0664a79cb31869a8f1db7
4
+ data.tar.gz: 5d1681703eeff7980f07160f33d4fc9c9909f0b0aa7326e840f28499412a5b23
5
+ SHA512:
6
+ metadata.gz: 2d3d4de13ba6db5fa209aeacb7a28742166ca36658868b3af95817dd1f802da0f71039f5fca5e691847479188bdc7bcba12b1a853326416296964053dd099dae
7
+ data.tar.gz: 715b52c02a5c359e58ba91f64d27b9584af5c28c780f9eee8e57137320f25cd4919e57119ad5acd69a081778d2278736eab2e185d0f36c46402c605484bd0d9e
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dustin D. Trammell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,4 @@
1
+ class Cryptomnio
2
+ VERSION = "0.2.2"
3
+ DATE = "2026-10-02"
4
+ end
data/lib/cryptomnio.rb ADDED
@@ -0,0 +1,839 @@
1
+ require 'rest-client'
2
+ require 'openssl'
3
+ require 'base64'
4
+ require 'json'
5
+ require_relative "cryptomnio/version"
6
+
7
+ ##
8
+ # This is the root Cryptomnio object Class. It contains global constants.
9
+
10
+ class Cryptomnio
11
+ attr_reader :VERSION
12
+ attr_reader :API_VERSION
13
+
14
+ # Creates a new Cryptomnio object
15
+ def initialize
16
+ ##
17
+ # Class Instance Variables
18
+
19
+ # The name of the Gem
20
+ @NAME = "Cryptomnio Ruby Gem"
21
+ # The name of the Gem's Author
22
+ @AUTHOR = "Dustin D. Trammell"
23
+ # The publication date of the current Gem version (from lib/cryptomnio/version.rb)
24
+ @DATE = DATE
25
+ # The Gem version (from lib/cryptomnio/version.rb)
26
+ @VERSION = VERSION
27
+ # The Cryptomnio API Version
28
+ @API_VERSION = "0.24.0"
29
+ # API URI Path Version Slug @URI_VERSION = "/v1"
30
+ @URI_VERSION = ""
31
+
32
+ puts "Cryptomnio Initalized" if $DEBUG
33
+ end
34
+
35
+ # Returns the Gem information
36
+ def geminfo
37
+ info = @NAME + " " + @VERSION + "\n"
38
+ info << @DATE + " - " + @AUTHOR + "\n"
39
+ info << "Cryptomnio API Version: " + @API_VERSION + "\n"
40
+
41
+ return info
42
+ end
43
+ end
44
+
45
+ # HTTP Request Logging module for HTTPClient decoration
46
+ module LogHTTPRequest
47
+ # Gets the specified URL
48
+ def get(url)
49
+ puts "Sending Request for #{url}"
50
+ super
51
+ end
52
+ end
53
+
54
+ # HTTPClient Class Wrapper
55
+ class HTTPClient
56
+ def initialize(client = RestClient)
57
+ @client = client
58
+ end
59
+
60
+ def get(*args)
61
+ @client.get(*args)
62
+ end
63
+ end
64
+
65
+ ##
66
+ # This is the root Cryptomnio REST object Class.
67
+ #
68
+ # It inherets from the Cryptomnio object Class and contains REST-specific methods and variables.
69
+
70
+ class Cryptomnio::REST < Cryptomnio
71
+
72
+ ##
73
+ # Creates a new Cryptomnio::REST object.
74
+ #
75
+ # If "http_proxy" is set in the program's environment, it will be used for
76
+ # the RestClient's proxy setting.
77
+
78
+ def initialize
79
+ # Call Superclass initialize
80
+ super
81
+
82
+ # Get the HTTP proxy from the environment (if there is one)
83
+ RestClient.proxy = ENV['http_proxy'] if ENV['http_proxy']
84
+ end
85
+ end
86
+
87
+ ##
88
+ # This is the Cryptomnio REST Client object Class
89
+ #
90
+ # It inherets from the Cryptomnio::REST object Class and contains REST Client-specific methods and variables.
91
+
92
+ class Cryptomnio::REST::Client < Cryptomnio::REST
93
+ attr_accessor :config
94
+
95
+ # Switch Object's Context (Account + Venue to operate as)
96
+ def context_switch( label )
97
+ puts "Switching to context \"%s\"" % label if $DEBUG
98
+
99
+ # Validate that a context for label exists
100
+ raise "No context found for label: %s" % label if ! @config[:contexts][label]
101
+ puts @config[:contexts][label].inspect if $DEBUG
102
+
103
+ # Validate that the required parameters exist
104
+ raise "Missing Configuration Parameter: contexts[:venue]" if ! @config[:contexts][label][:venue]
105
+ raise "Missing Configuration Parameter: contexts[:accountid]" if ! @config[:contexts][label][:accountid]
106
+ raise "Missing Configuration Parameter: contexts[:venuekeyid]" if ! @config[:contexts][label][:venuekeyid]
107
+
108
+ # Switch to the context
109
+ @context = @config[:contexts][label]
110
+
111
+ # Return true on success
112
+ puts "Context Switch Successful!" if $DEBUG
113
+ return true
114
+ end
115
+
116
+ # Cryptomnio Key Authentication
117
+ def auth_cryptomnio( method, uripath )
118
+ puts "Building Authentication Credentials" if $DEBUG
119
+
120
+ # Create concatenated method and base URL path string
121
+ methodpath = method.to_s.upcase + uripath # convert RestClient's method symbol to uppercase string
122
+ puts " Method+Path = %s" % methodpath if $DEBUG
123
+
124
+ # Create HMAC SHA-512 authentication hash of methodpath using secret key
125
+ @config[:auth_string] = Base64.encode64(OpenSSL::HMAC.digest('sha512', @config[:secret_key], methodpath)).split.join # .split.join is to remove '/n' inserted into signature by HMAC
126
+
127
+ # Output
128
+ if $DEBUG && $VERBOSE
129
+ puts " Access-Key: %s" % @config[:access_key]
130
+ puts " Signature: %s" % @config[:auth_string]
131
+ puts
132
+ end
133
+
134
+ # Return the signature
135
+ return @config[:auth_string]
136
+ end
137
+
138
+ # Test method wrapper for Cryptomnio Key Authentication
139
+ def auth_cryptomnio_test
140
+ # TODO: Find better CMA test call to use
141
+ method = :get
142
+ endpoint = "/venues"
143
+ #endpoint = "/nope"
144
+ uripath = @URI_VERSION + endpoint
145
+ signature = self.auth_cryptomnio( method, uripath )
146
+
147
+ puts "Testing Cryptomnio Key Authentication" if $VERBOSE
148
+ if $DEBUG
149
+ puts " Requesting: %s %s%s" % [ method, @config[:apiurl], uripath ]
150
+ puts " Access-Key: %s" % @config[:access_key]
151
+ puts " Signature: %s" % signature
152
+ end
153
+
154
+ # Test Authentication Call
155
+ self._rest_call( method, api = :core, uripath = uripath, uriparameters = nil, error_string = "Cryptomnio Key Authentication test failed." )
156
+
157
+ # Success
158
+ return true
159
+ end
160
+
161
+ # Generic REST API call method
162
+ # Provides RestClient call with needed authentication headers and error handling
163
+ def _rest_call( method = :get, api = :core, uripath = "/", uriparameters = nil, error_string = "REST API Call Failed", body = nil )
164
+ # Get the HMAC SHA-512 hash signature value for the method+URIpath
165
+ signature = self.auth_cryptomnio( method, @URI_VERSION + uripath )
166
+
167
+ # Build the URI from the URIpath and optional parameters
168
+ case api
169
+ when :core
170
+ uri = "https://" + @config[:api_host_core] + @URI_VERSION + uripath
171
+ when :cma
172
+ uri = "https://" + @config[:api_host_cma] + @URI_VERSION + uripath
173
+ else
174
+ raise "Unknown API (%s) referenced" % api
175
+ end
176
+ uri << "?" + uriparameters if uriparameters
177
+
178
+ ##
179
+ # Build the Headers
180
+
181
+ headers = {
182
+ # Authentication headers Access-Key and Sign
183
+ "Access-Key" => @config[:access_key],
184
+ "Sign" => signature
185
+ }
186
+ # Add a Content-type header for application/json if there is a payload body to send
187
+ headers["Content-type"] = "application/json" if body
188
+
189
+ # Call RestClient based on the method against URI with authentication headers "Access-Key" and "Sign"
190
+ retries ||= 0
191
+ response = nil
192
+ begin
193
+
194
+ RestClient::Request.execute(
195
+ method: method,
196
+ url: uri,
197
+ headers: headers,
198
+ payload: body
199
+ ) do
200
+ |inner_response, request, result, &block|
201
+ response = inner_response
202
+
203
+ # Debug output
204
+ if $DEBUG
205
+ puts "Request: %s: %s" % [ request.method.upcase, request.uri ]
206
+ puts "Headers: %s" % [ request.headers ]
207
+ puts "POST payload: %s" % [ body ] if body
208
+ puts "Timestamp: %s" % [ Time.now.strftime('%Y-%m-%d %H:%M:%S %z') ]
209
+ puts "Response: %d: %s" % [ response.code, response ]
210
+ end
211
+
212
+ # Check for errors
213
+ self._check_errors( response, error_string )
214
+
215
+ # Success: Convert JSON to Ruby object and return it
216
+ responsebody = JSON.parse(response)['body']
217
+ return responsebody
218
+ end
219
+ rescue => e
220
+ # Rescue any exceptions thrown by _check_errors
221
+
222
+ # Retry 3 times with pauses if temporary error
223
+ puts "Retries: #{retries} | response.code: #{response.code}"
224
+ if response.code == 502 and retries += 1 < 3
225
+ sleep 30
226
+ retry
227
+ end
228
+
229
+ # Output
230
+ puts e.inspect if $DEBUG
231
+ puts e.message if $DEBUG
232
+
233
+ # Pass it on
234
+ raise e
235
+ end
236
+ end
237
+
238
+ ##
239
+ # CORE API Venue and Market function methods
240
+
241
+ # Return an array of supported Venues
242
+ def get_venues
243
+ return self._rest_call( :get, :core, "/venues", nil, "Retrieval of supported venues failed." )
244
+ end
245
+
246
+ # Return an array of hashes of a venue's markets
247
+ def get_markets( venue: )
248
+ uripath = "/venues/" + venue + "/markets"
249
+ return self._rest_call( :get, :core, uripath, nil, "Retrieval of supported venue's markets failed." )
250
+ end
251
+
252
+ ##
253
+ # CORE API Account function methods
254
+
255
+ # Return a hash of a venue account
256
+ def get_account(
257
+ venue: @context[:venue].to_s,
258
+ accountid: @context[:accountid] )
259
+
260
+ uripath = "/venues/" + venue + "/accounts/" + accountid
261
+ errormsg = "Retrieval of venue account information for account %s failed." % accountid
262
+ return self._rest_call( :get, :core, uripath, nil, errormsg )
263
+ end
264
+
265
+ # Return an array of hashes of a venue account's balances
266
+ def get_account_balance(
267
+ venue: @context[:venue].to_s,
268
+ accountid: @context[:accountid],
269
+ venuekeyid: @context[:venuekeyid] )
270
+
271
+ uripath = "/venues/exchanges/" + venue + "/accounts/" + accountid + "/balance"
272
+ uriparams = "venueKeyId=" + venuekeyid
273
+ errormsg = "Retrieval of venue account's balance for account %s failed." % accountid
274
+ return self._rest_call( :get, :core, uripath, uriparams, errormsg )
275
+ end
276
+
277
+ # Get account balance for symbol
278
+ def get_account_balance_symbol(
279
+ symbol:,
280
+ venue: @context[:venue].to_s,
281
+ accountid: @context[:accountid],
282
+ venuekeyid: @context[:venuekeyid] )
283
+
284
+ symbol = symbol.downcase
285
+ balance = nil
286
+ # TODO: Check cached balance's timestamp, if too old, update balances
287
+ # Retrieve all balances for account
288
+ @balances = self.get_account_balance( venue: venue, accountid: accountid, venuekeyid: venuekeyid )
289
+ # Find the balance for the symbol we want
290
+ @balances["assets"].each do |asset|
291
+ balance = asset["amount"] if asset["currency"]&.downcase == symbol
292
+ end
293
+ # Raise an exception if the requested symbol is not found
294
+ raise "No balance returned for currency symbol \"%s\"" % symbol if ! balance
295
+ # Return balance for requested symbol as a Float
296
+ return balance.to_f
297
+ end
298
+
299
+ # Get account balance for symbol
300
+ def get_account_available_balance_symbol(
301
+ symbol:,
302
+ venue: @context[:venue].to_s,
303
+ accountid: @context[:accountid],
304
+ venuekeyid: @context[:venuekeyid] )
305
+
306
+ symbol = symbol.downcase
307
+ balance = nil
308
+ # TODO: Check cached balance's timestamp, if too old, update balances
309
+ # Retrieve all balances for account
310
+ @balances = self.get_account_balance( venue: venue, accountid: accountid, venuekeyid: venuekeyid )
311
+ # Find the balance for the symbol we want
312
+ @balances["assets"].each do |asset|
313
+ balance = asset["available"] if asset["currency"]&.downcase == symbol
314
+ end
315
+ # Raise an exception if the requested symbol is not found
316
+ raise "No balance returned for currency symbol \"%s\"" % symbol if ! balance
317
+ # Return balance for requested symbol as a Float
318
+ return balance.to_f
319
+ end
320
+
321
+ # Return an array of hashes of a venue account's orders
322
+ # Accepts optional array of orders statuses for filtering results
323
+ def get_account_orders(
324
+ status_filters: [],
325
+ venue: @context[:venue].to_s,
326
+ accountid: @context[:accountid],
327
+ venuekeyid: @context[:venuekeyid] )
328
+
329
+ uripath = "/venues/exchanges/" + venue + "/accounts/" + accountid + "/orders"
330
+ uriparams = "venueKeyId=" + venuekeyid
331
+ status_filters.each do |status|
332
+ uriparams << "&status=" + status.to_s
333
+ end
334
+ errormsg = "Retrieval of venue account's orders for account %s failed." % accountid
335
+ return self._rest_call( :get, :core, uripath, uriparams, errormsg )
336
+ end
337
+
338
+ # Return an hash of a venue account's single order
339
+ def get_account_order(
340
+ orderid:,
341
+ venue: @context[:venue].to_s,
342
+ accountid: @context[:accountid],
343
+ venuekeyid: @context[:venuekeyid] )
344
+
345
+ uripath = "/venues/exchanges/" + venue + "/accounts/" + accountid + "/orders/" + orderid
346
+ uriparams = "venueKeyId=" + venuekeyid
347
+ errormsg = "Retrieval of venue account's order %s for account %s failed." % [orderid, accountid]
348
+ return self._rest_call( :get, :core, uripath, uriparams, errormsg )
349
+ end
350
+
351
+ # Create a new market order under a venue account
352
+ def put_account_order_market(
353
+ market:,
354
+ side:,
355
+ quantity:,
356
+ amount:,
357
+ venue: @context[:venue].to_s,
358
+ accountid: @context[:accountid],
359
+ venuekeyid: @context[:venuekeyid] )
360
+
361
+ uripath = '/venues/exchanges/' + venue + '/accounts/' + accountid + '/orders'
362
+ errormsg = 'Creation of new market order for account %s failed.' % accountid
363
+ body = {
364
+ 'venue' => venue,
365
+ 'venueKeyId' => venuekeyid,
366
+ 'orderType' => 'market',
367
+ 'side' => side,
368
+ 'market' => market
369
+ }
370
+ body['quantity'] = quantity if quantity
371
+ body['amount'] = amount if amount
372
+ return self._rest_call( :post, :core, uripath, nil, errormsg, body.to_json )
373
+ end
374
+
375
+ # Create a new limit order under a venue account
376
+ def put_account_order_limit(
377
+ market:,
378
+ side:,
379
+ quantity:,
380
+ price:,
381
+ venue: @context[:venue].to_s,
382
+ accountid: @context[:accountid],
383
+ venuekeyid: @context[:venuekeyid] )
384
+
385
+ uripath = "/venues/exchanges/" + venue + "/accounts/" + accountid + "/orders"
386
+ errormsg = "Creation of new limit order for account %s failed." % accountid
387
+ body = {
388
+ "venue" => venue,
389
+ "venueKeyId" => venuekeyid,
390
+ "orderType" => "limit",
391
+ "side" => side,
392
+ "quantity" => quantity,
393
+ "price" => price,
394
+ "market" => market
395
+ }
396
+ return self._rest_call( :post, :core, uripath, nil, errormsg, body.to_json )
397
+ end
398
+
399
+ # Create a new stop loss order under a venue account
400
+ def put_account_order_stop(
401
+ market:,
402
+ side:,
403
+ quantity:,
404
+ triggerprice:,
405
+ amount:,
406
+ venue: @context[:venue].to_s,
407
+ accountid: @context[:accountid],
408
+ venuekeyid: @context[:venuekeyid] )
409
+
410
+ uripath = "/venues/exchanges/" + venue + "/accounts/" + accountid + "/orders"
411
+ errormsg = "Creation of new limit order for account %s failed." % accountid
412
+ body = {
413
+ "venue" => venue,
414
+ "venueKeyId" => venuekeyid,
415
+ "orderType" => "stop",
416
+ "side" => side,
417
+ "triggerPrice" => triggerprice,
418
+ "market" => market
419
+ }
420
+ body['quantity'] = quantity if quantity
421
+ body['amount'] = amount if amount
422
+ return self._rest_call( :post, :core, uripath, nil, errormsg, body.to_json )
423
+ end
424
+
425
+ # Delete (cancel) a venue account's open order
426
+ def del_account_order(
427
+ orderid:,
428
+ venue: @context[:venue].to_s,
429
+ accountid: @context[:accountid],
430
+ venuekeyid: @context[:venuekeyid] )
431
+
432
+ uripath = "/venues/exchanges/" + venue + "/accounts/" + accountid + "/orders/" + orderid
433
+ uriparams = "venueKeyId=" + venuekeyid
434
+ errormsg = "Cancellation of venue account's order %s for account %s failed." % [orderid, accountid]
435
+ return self._rest_call( :delete, :core, uripath, uriparams, errormsg )
436
+ end
437
+
438
+ # Return an array of hashes of a venue account's trades
439
+ def get_account_trades(
440
+ venue: @context[:venue].to_s,
441
+ accountid: @context[:accountid],
442
+ venuekeyid: @context[:venuekeyid] )
443
+
444
+ uripath = "/venues/exchanges/" + venue + "/accounts/" + accountid + "/trades"
445
+ uriparams = "venueKeyId=" + venuekeyid
446
+ errormsg = "Retrieval of venue account's orders for account %s failed." % accountid
447
+ return self._rest_call( :get, :core, uripath, uriparams, errormsg )
448
+ end
449
+
450
+ # Return a hash of a venue account's trade
451
+ def get_account_trade(
452
+ tradeid:,
453
+ venue: @context[:venue].to_s,
454
+ accountid: @context[:accountid],
455
+ venuekeyid: @context[:venuekeyid] )
456
+
457
+ uripath = "/venues/exchanges/" + venue + "/accounts/" + accountid + "/trades/" + tradeid
458
+ uriparams = "venueKeyId=" + venuekeyid
459
+ errormsg = "Retrieval of venue account's trade %s for account %s failed." % [tradeid, accountid]
460
+ return self._rest_call( :get, :core, uripath, uriparams, errormsg )
461
+ end
462
+
463
+ ##
464
+ # CMA API Methods
465
+
466
+ # Return a hash of a venue market's order book
467
+ def get_market_orderbook(
468
+ market:,
469
+ side: nil,
470
+ limit: nil,
471
+ venue: @context[:venue].to_s)
472
+
473
+ uripath = "/venues/" + venue + "/markets/" + market + "/orderbook"
474
+ parts = []
475
+ parts << "side=%s" % side if side
476
+ parts << "limit=%d" % limit if limit
477
+ uriparams = parts.empty? ? nil : parts.join("&")
478
+
479
+ return self._rest_call( :get, :cma, uripath, uriparams, "Retrieval of venue market's order book failed." )
480
+ end
481
+
482
+ # Return a hash of a venue market's order book's price at a specified volume
483
+ def get_market_orderbook_depth_price(
484
+ market:,
485
+ side: nil,
486
+ volume: nil,
487
+ cumulative: nil,
488
+ venue: @context[:venue].to_s)
489
+
490
+ uripath = "/venues/" + venue + "/markets/" + market + "/orderbook/depth/price"
491
+ parts = []
492
+ parts << "side=%s" % side if side
493
+ parts << "volume=%f" % volume if volume
494
+ parts << "cumulative=%s" % cumulative.to_s if !cumulative.nil?
495
+ uriparams = parts.empty? ? nil : parts.join("&")
496
+
497
+ return self._rest_call( :get, :cma, uripath, uriparams, "Retrieval of venue market's order book price at given volume (#{volume}) failed." )
498
+ end
499
+
500
+ # Return a hash of a venue market's order book's volume at a specified price
501
+ def get_market_orderbook_depth_volume(
502
+ market:,
503
+ side: nil,
504
+ price: nil,
505
+ cumulative: nil,
506
+ venue: @context[:venue].to_s)
507
+
508
+ uripath = "/venues/" + venue + "/markets/" + market + "/orderbook/depth/volume"
509
+ parts = []
510
+ parts << "side=%s" % side if side
511
+ parts << "price=%f" % price if price
512
+ parts << "cumulative=%s" % cumulative.to_s if !cumulative.nil?
513
+ uriparams = parts.empty? ? nil : parts.join("&")
514
+
515
+ return self._rest_call( :get, :cma, uripath, uriparams, "Retrieval of venue market's order book volume at given price (#{price}) failed." )
516
+ end
517
+
518
+ # Return an array of a venue market's ticker hashes
519
+ def get_market_tickers(
520
+ market:,
521
+ from: (Time.now.to_i - 300) * 1000, # default to last 5 minutes
522
+ to: nil, # to defaults to Time.now within Cryptomnio
523
+ cursor: nil,
524
+ venue: @context[:venue].to_s)
525
+
526
+ uripath = "/venues/" + venue + "/markets/" + market + "/ticker"
527
+ uriparams = "from=%d" % from if from
528
+ uriparams << "&to=%d" % to if to
529
+ uriparams << "&cursor=%d" % cursor if cursor
530
+
531
+ retries = 0
532
+ pause = 4
533
+ begin
534
+ result = self._rest_call( :get, :cma, uripath, uriparams, "Retrieval of venue's market's market ticker failed." )
535
+ pp result if $DEBUG
536
+ raise "Error: Empty response received" if result == nil
537
+ raise "Error: No latest ticker value" if result['latest'] == nil
538
+ return result.dup
539
+ rescue => e
540
+ case
541
+ when retries <= 3
542
+ retries += 1
543
+ sleep pause
544
+ pause = pause * 2
545
+ retry
546
+ else
547
+ raise "Error: Continued error for 3 retries: %s" % e.message
548
+ return false
549
+ end
550
+ end
551
+ end
552
+
553
+ # Return a venue market's most current ticker hash
554
+ def get_market_ticker(
555
+ market:,
556
+ venue: @context[:venue].to_s)
557
+
558
+ # Call get_market_tickers to request most recent ticker
559
+ #TODO: Replace with this call once Cryptomnio accepts calls without a from parameter (don't need a set of tickers, just most recent)
560
+ #tickers = self.get_market_tickers( market: market, venue: venue )
561
+ tickers = self.get_market_tickers( market: market, from: (Time.now.to_i - 30) * 1000, venue: venue )
562
+
563
+ # Check timestamp on ticker and ensure it's not too old
564
+ too_old = (Time.now.to_i - 120) * 1000 # over 2 minutes ago
565
+ raise "ERROR: Most recent ticker is too old! (%s is over 2 minutes ago)" % [ Time.new(tickers['latest']['timestamp'] / 1000) ] if tickers['latest']['timestamp'] < too_old
566
+
567
+ # Output
568
+ puts "Ticker: %s" % tickers['latest']['price'] if $VERBOSE
569
+
570
+ return tickers['latest'].dup
571
+ end
572
+
573
+ # Return a venue market's recent trades array
574
+ def get_market_trades(
575
+ market:,
576
+ limit: nil,
577
+ venue: @context[:venue].to_s )
578
+
579
+ uripath = "/venues/" + venue + "/markets/" + market + "/trades"
580
+ uriparams = limit ? "limit=%s" % limit : nil
581
+
582
+ retries = 0
583
+ begin
584
+ result = self._rest_call( :get, :cma, uripath, uriparams, "Retrieval of venue's market's market ticker failed." )
585
+
586
+ # Raise an execption if the result is null or empty
587
+ raise "Error: Empty set of market trades received" if ! result || result.count < 1
588
+
589
+ # Output
590
+ if $DEBUG
591
+ puts "Trades:\n"
592
+ pp result
593
+ end
594
+
595
+ # Return the trades array
596
+ return result
597
+ rescue => e
598
+ case
599
+ when retries <= 3
600
+ retries += 1
601
+ sleep 1
602
+ retry
603
+ else
604
+ raise "Error: Received empty set of market trades for 3 retries: %s" % e.message
605
+ return false
606
+ end
607
+ end
608
+
609
+ end
610
+
611
+ # Return an array of period data
612
+ def get_market_periods(
613
+ market:,
614
+ periodlength:,
615
+ periodcount: nil, # periodCount defaults to 10 within Cryptomnio
616
+ to: nil, # to defaults to Time.now within Cryptomnio
617
+ limit: nil,
618
+ venue: @context[:venue].to_s)
619
+
620
+ uripath = "/venues/" + venue + "/markets/" + market + "/periods"
621
+ uriparams = "periodLength=%s" % periodlength
622
+ uriparams << "&periodCount=%d" % periodcount if periodcount
623
+ uriparams << "&to=%d" % to if to
624
+ uriparams << "&limit=%d" % limit if limit
625
+
626
+ # TODO: Determine if we have any of the requested periods cached, and instead request only what we don't already have
627
+
628
+ retries = 0
629
+ begin
630
+ result = self._rest_call( :get, :cma, uripath, uriparams, "Retrieval of venue's market's market periods failed." )
631
+
632
+ # Raise an execption if the result is null or empty
633
+ raise "Error: Empty set of market periods received" if ! result || result.count < 1
634
+
635
+ # Output
636
+ if $DEBUG
637
+ puts "Periods:\n"
638
+ pp result
639
+ end
640
+
641
+ # Return the periods array
642
+ return result
643
+ rescue => e
644
+ case
645
+ when retries <= 3
646
+ retries += 1
647
+ sleep 1
648
+ retry
649
+ else
650
+ raise "Error: Received empty set of market periods for 3 retries: %s" % e.message
651
+ return false
652
+ end
653
+ end
654
+ end
655
+
656
+ # Return a momentum value for a defined period of time
657
+ def get_market_momentum(
658
+ market:,
659
+ type:,
660
+ periodlength:,
661
+ periodcount: nil, # periodCount defaults to 10 within Cryptomnio
662
+ to: nil, # to defaults to Time.now within Cryptomnio
663
+ venue: @context[:venue].to_s)
664
+
665
+ uripath = "/venues/" + venue + "/markets/" + market + "/momentum"
666
+ uriparams = "type=%s" % type
667
+ uriparams << "&periodLength=%s" % periodlength
668
+ uriparams << "&periodCount=%d" % periodcount if periodcount
669
+ uriparams << "&to=%d" % to if to
670
+
671
+ retries = 0
672
+ begin
673
+ result = self._rest_call( :get, :cma, uripath, uriparams, "Retrieval of venue's market's market momentum failed." )
674
+
675
+ # Raise an execption if the result is null or empty
676
+ raise "Error: Empty set of market momentum received" if ! result || result.count < 1
677
+
678
+ # Output
679
+ puts "Momentum: %f\n" % result['price_change'].to_f if $DEBUG
680
+
681
+ # Return momentum value
682
+ return result['price_change'].to_f
683
+ rescue => e
684
+ case
685
+ when retries <= 3
686
+ retries += 1
687
+ sleep 1
688
+ retry
689
+ else
690
+ raise "Error: Received empty set of market momentum for 3 retries: %s" % e.message
691
+ return false
692
+ end
693
+ end
694
+ end
695
+
696
+ # Return an exponential moving average value for a defined period of time
697
+ def get_market_ema(
698
+ market:,
699
+ type:,
700
+ periodlength:,
701
+ periodcalc:,
702
+ periodcount: nil, # periodCount defaults to 10 within Cryptomnio
703
+ to: nil, # to defaults to Time.now within Cryptomnio
704
+ venue: @context[:venue].to_s)
705
+
706
+ uripath = "/venues/" + venue + "/markets/" + market + "/averages/ema"
707
+ uriparams = "type=%s" % type
708
+ uriparams << "&periodLength=%s" % periodlength
709
+ uriparams << "&periodCalc=%d" % periodcalc
710
+ uriparams << "&periodCount=%d" % periodcount if periodcount
711
+ uriparams << "&to=%d" % to if to
712
+
713
+ retries = 0
714
+ begin
715
+ result = self._rest_call( :get, :cma, uripath, uriparams, "Retrieval of venue's market's market Exponential Moving Average (EMA) failed." )
716
+
717
+ # Raise an execption if the result is null or empty
718
+ raise "Error: Empty set of market Exponential Moving Average (EMA) received" if ! result || result.count < 1
719
+
720
+ # Output
721
+ if $DEBUG
722
+ puts "EMA sets:\n"
723
+ pp result
724
+ end
725
+
726
+ # Return averages array
727
+ return result
728
+ rescue => e
729
+ case
730
+ when retries <= 3
731
+ retries += 1
732
+ sleep 1
733
+ retry
734
+ else
735
+ raise "Error: Received empty set of market Exponential Moving Average (EMA) for 3 retries: %s" % e.message
736
+ return false
737
+ end
738
+ end
739
+
740
+ end
741
+
742
+ ##
743
+ # Object Methods
744
+
745
+ # Object initialization
746
+ def initialize
747
+ yield self if block_given?
748
+ super
749
+
750
+ ##
751
+ # Configuration check
752
+
753
+ # Check for required API hostnames
754
+ raise "Missing Configuration Parameter: api_host_core" if ! @config[:api_host_core]
755
+ raise "Missing Configuration Parameter: api_host_cma" if ! @config[:api_host_cma]
756
+
757
+ # Check for required Authentication Credentials
758
+ raise "Missing Configuration Parameter: authtype" if ! @config[:authtype]
759
+ case @config[:authtype]
760
+ when 'Basic'
761
+ raise "Missing Configuration Parameter: username" if ! @config[:username]
762
+ raise "Missing Configuration Parameter: password" if ! @config[:password]
763
+ when 'Cryptomnio'
764
+ raise "Missing Configuration Parameter: access_key" if ! @config[:access_key]
765
+ raise "Missing Configuration Parameter: secret_key" if ! @config[:secret_key]
766
+ else
767
+ raise "Unrecognized Authentication Type: %s" % @config[:authtype]
768
+ end
769
+
770
+ # Check for at least one contexts (venue+account)
771
+ raise "Missing Configuration Paramters: contexts" if @config[:contexts].count < 1
772
+
773
+ # Check configured contexts and validate them by switching to them
774
+ puts "Checking all configured contexts (venue + account) for required parameters" if $DEBUG
775
+ @config[:contexts].each do | label, paramhash |
776
+ self.context_switch( label )
777
+ end
778
+
779
+ # Switch back to the first context by default
780
+ puts "Switching back to the first context by default" if $DEBUG
781
+ label = @config[:contexts].keys[0]
782
+ self.context_switch( label )
783
+
784
+ # Output
785
+ if $DEBUG
786
+ puts "Cryptomnio Startup Configuration:"
787
+ p @config
788
+ end
789
+ end
790
+
791
+ ##
792
+ # Utility Methods
793
+
794
+ # Error-handling method
795
+ def _check_errors( response, thisfailure )
796
+ case
797
+ when response.code == 200
798
+ # Success!
799
+ return true
800
+ when response.code == 400
801
+ # Bad Request
802
+ raise "Error: [%d]: %s" % [ response.code, response ]
803
+ return false
804
+ when response.code == 401
805
+ # Unauthorized
806
+ case @config[:authtype]
807
+ when "Basic"
808
+ raise "Cryptomnio Basic Authentication for user %s failed: [%d]: %s" % [ @config[:username], response.code, response ]
809
+ return false
810
+ when "Cryptomnio"
811
+ raise "Cryptomnio Key Authentication failed: [%d]: %s" % [ response.code, response ]
812
+ return false
813
+ else
814
+ raise "Authentication failed: [%d]: %s" % [ response.code, response ]
815
+ return false
816
+ end
817
+ when response.code == 404
818
+ # Not Found
819
+ raise "Error: Missing Resource"
820
+ return false
821
+ when response.code == 502
822
+ # Bad Gateway
823
+ raise "Error: %s: [%d]: %s" % [ thisfailure, response.code, response ]
824
+ when response.code == 503
825
+ # Service Unavailable
826
+ raise "Temporary Error: %s: [%d]: %s" % [ thisfailure, response.code, response ]
827
+ return false
828
+ when response.code == 504
829
+ # Gateway Timeout (temporary error)
830
+ raise "Temporary Error: %s: [%d]: %s" % [ thisfailure, response.code, response ]
831
+ return false
832
+ else
833
+ # Catch-all: Anything other than the above
834
+ raise "Error: %s: [%d]: %s" % [ thisfailure, response.code, response ]
835
+ return false
836
+ end
837
+ end
838
+
839
+ end
metadata ADDED
@@ -0,0 +1,56 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cryptomnio
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.2
5
+ platform: ruby
6
+ authors:
7
+ - Dustin D. Trammell
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2026-10-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rest-client
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.1'
26
+ description: A Ruby gem providing an interface to the Cryptomnio API
27
+ email: info@cryptomnio.com
28
+ executables: []
29
+ extensions: []
30
+ extra_rdoc_files: []
31
+ files:
32
+ - LICENSE
33
+ - lib/cryptomnio.rb
34
+ - lib/cryptomnio/version.rb
35
+ homepage: https://github.com/dtrammell/ruby-cryptomnio
36
+ licenses:
37
+ - MIT
38
+ metadata: {}
39
+ rdoc_options: []
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '3.3'
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ requirements: []
53
+ rubygems_version: 4.0.20
54
+ specification_version: 4
55
+ summary: Cryptomnio API Interface
56
+ test_files: []