kalshi 0.1.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,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyists
4
+ module Kalshi
5
+ # Base class for API client wrappers that automatically configure URL prefixes
6
+ #
7
+ # @example Using ApiClient with a Search namespace
8
+ # # For Rubyists::Kalshi::Search::Client, the prefix will be "search"
9
+ # class Search::Client < ApiClient
10
+ # # API calls will automatically use /search/ prefix
11
+ # end
12
+ #
13
+ # @example Overriding the automatic prefix
14
+ # class CustomClient < ApiClient
15
+ # self.prefix = 'custom_api'
16
+ # end
17
+ class ApiClient
18
+ attr_reader :client
19
+
20
+ # Automatically extract the URL prefix from the module hierarchy
21
+ #
22
+ # The prefix is derived by taking the second-to-last component of the
23
+ # fully qualified class name and converting it to lowercase. For example:
24
+ # - Rubyists::Kalshi::Search::Client => "search"
25
+ # - Rubyists::Kalshi::Market::Client => "market"
26
+ #
27
+ # @return [String] the URL prefix for API calls
28
+ def self.prefix
29
+ @prefix ||= to_s.split('::')[-2].downcase
30
+ end
31
+
32
+ # Set a custom URL prefix for API calls, for overriding the default behavior
33
+ def self.prefix=(value) # rubocop:disable Style/TrivialAccessors
34
+ @prefix = value
35
+ end
36
+
37
+ # Initialize the ApiClient with a given client instance
38
+ #
39
+ # @param client [Rubyists::Kalshi::Client] the client instance to wrap
40
+ #
41
+ # @return [void]
42
+ def initialize(client)
43
+ @client = client
44
+ client.prefix = self.class.prefix
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'httpx'
4
+ require 'json'
5
+ require 'uri'
6
+
7
+ module Rubyists
8
+ module Kalshi
9
+ # HTTP Client for Kalshi API
10
+ class Client
11
+ include SemanticLogger::Loggable
12
+
13
+ attr_reader :http, :base_url
14
+ attr_accessor :prefix
15
+
16
+ def initialize(base_url: Kalshi.config.base_url)
17
+ @base_url = base_url
18
+ @http = HTTPX.plugin(:persistent)
19
+ .plugin(:rate_limiter)
20
+ .with(origin: base_url)
21
+ end
22
+
23
+ # Get response from a path, adding the base_url,
24
+ # and a prefix, if set on the client.
25
+ #
26
+ # see #full_path for details
27
+ #
28
+ # @param path [String] The URL path
29
+ #
30
+ # @return [Hash] The parsed JSON response
31
+ def get(path, params: {})
32
+ get_url(full_url(path), params:)
33
+ end
34
+
35
+ # Get response from a URL
36
+ # Must pass a full URL, including scheme (http/https), host, etc.
37
+ #
38
+ # @param path [String] The full URL path
39
+ #
40
+ # @return [Hash] The parsed JSON response
41
+ def get_url(url, params: {})
42
+ uri = URI.parse(url)
43
+ raise ArgumentError, 'URL must be http or https' unless %w[http https].include?(uri.scheme)
44
+
45
+ response = @http.get(url, params:)
46
+ handle_response(response)
47
+ rescue ArgumentError => e
48
+ logger.error('Invalid URL', url:, exception: e)
49
+ raise Error, "Invalid URL: #{e.message}"
50
+ end
51
+
52
+ def market
53
+ @market ||= Market::Client.new(clone)
54
+ end
55
+
56
+ def search
57
+ @search ||= Search::Client.new(clone)
58
+ end
59
+
60
+ def events
61
+ @events ||= Events::Client.new(clone)
62
+ end
63
+
64
+ def series
65
+ @series ||= Series::Client.new(clone)
66
+ end
67
+
68
+ private
69
+
70
+ def full_url(path)
71
+ parts = [base_url, prefix, path].compact
72
+ parts.reject!(&:empty?)
73
+ File.join(*parts)
74
+ end
75
+
76
+ def handle_response(response)
77
+ response.raise_for_status
78
+ JSON.parse(response.body.to_s, symbolize_names: true)
79
+ rescue HTTPX::HTTPError => e
80
+ logger.error('API Error', e)
81
+ raise Error, "API Error: #{e.response.status} - #{e.response.body}"
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'reform'
4
+ require 'reform/form/dry'
5
+
6
+ module Rubyists
7
+ module Kalshi
8
+ # Base Contract class for Trailblazer/Reform forms
9
+ class Contract < Reform::Form
10
+ feature Reform::Form::Dry
11
+
12
+ # Custom class methods can be defined here
13
+ module ClassMethods
14
+ def propertize(*names)
15
+ # names is always an array because of the splat
16
+ names = names.flatten
17
+ const_set(:Properties, Struct.new(*custom_definitions, *names, keyword_init: true))
18
+ names.each { |name| property name, populator: ->(value:, **) { value || skip! } }
19
+ end
20
+ end
21
+
22
+ def self.inherited(subclass)
23
+ subclass.extend ClassMethods
24
+ super
25
+ end
26
+
27
+ def self.custom_definitions = instance_variable_get(:@definitions)&.keys&.map(&:to_sym) || []
28
+
29
+ def to_h
30
+ to_nested_hash.compact
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyists
4
+ module Kalshi
5
+ # Base class for Kalshi API endpoints
6
+ class Endpoint
7
+ attr_reader :client
8
+
9
+ class << self
10
+ attr_accessor :endpoint_path
11
+
12
+ # Set the endpoint path for the resource
13
+ #
14
+ # @param path [String] API endpoint path
15
+ #
16
+ # @return [String] endpoint path
17
+ def kalshi_path(path)
18
+ self.endpoint_path = path
19
+ end
20
+ end
21
+
22
+ # Initialize the Endpoint
23
+ #
24
+ # @param client [Client] The Kalshi client
25
+ #
26
+ # @return [void]
27
+ def initialize(client = nil)
28
+ @client = client || Client.new
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyists
4
+ module Kalshi
5
+ module Events
6
+ # Events API Client
7
+ class Client < ApiClient
8
+ def list(...)
9
+ List.new(client).list(...)
10
+ end
11
+
12
+ def fetch(...)
13
+ List.new(client).fetch(...)
14
+ end
15
+
16
+ def metadata(...)
17
+ List.new(client).metadata(...)
18
+ end
19
+
20
+ def multivariate
21
+ Multivariate.new(client)
22
+ end
23
+
24
+ def candlesticks
25
+ Rubyists::Kalshi::Series::EventCandlesticks.new(client)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyists
4
+ module Kalshi
5
+ module Events
6
+ # Events API endpoint
7
+ class List < Kalshi::Endpoint
8
+ include Kalshi::Listable
9
+
10
+ kalshi_path ''
11
+
12
+ # Filter for Kalshi events list
13
+ class Filter < Kalshi::Contract
14
+ propertize(%i[limit cursor status series_ticker with_nested_markets])
15
+
16
+ validation do
17
+ params do
18
+ optional(:limit).maybe(:integer)
19
+ optional(:cursor).maybe(:string)
20
+ optional(:status).maybe(:string)
21
+ optional(:series_ticker).maybe(:string)
22
+ optional(:with_nested_markets).maybe(:bool)
23
+ end
24
+ end
25
+ end
26
+
27
+ def fetch(event_ticker)
28
+ client.get(event_ticker)
29
+ end
30
+
31
+ def metadata(event_ticker)
32
+ client.get("#{event_ticker}/metadata")
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyists
4
+ module Kalshi
5
+ module Events
6
+ # Multivariate Events API endpoint
7
+ class Multivariate < Kalshi::Endpoint
8
+ include Kalshi::Listable
9
+
10
+ kalshi_path 'multivariate'
11
+
12
+ # Filter for Kalshi multivariate events list
13
+ class Filter < Kalshi::Contract
14
+ propertize(%i[limit cursor series_ticker collection_ticker])
15
+
16
+ validation do
17
+ params do
18
+ optional(:limit).maybe(:integer)
19
+ optional(:cursor).maybe(:string)
20
+ optional(:series_ticker).maybe(:string)
21
+ optional(:collection_ticker).maybe(:string)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyists
4
+ module Kalshi
5
+ # Mixin for listable resources
6
+ module Listable
7
+ def self.included(base)
8
+ base.extend(ClassMethods)
9
+ end
10
+
11
+ # Class methods for Listable
12
+ module ClassMethods
13
+ def list(...)
14
+ new.list(...)
15
+ end
16
+ end
17
+
18
+ # List resources
19
+ #
20
+ # @param filter [Filter|Hash] Filter options
21
+ #
22
+ # @return [Hash|Array] resource data
23
+ def list(filter = self.class::Filter.new(self.class::Filter::Properties.new))
24
+ filter = self.class::Filter.new(self.class::Filter::Properties.new(**filter)) if filter.is_a?(Hash)
25
+ raise ArgumentError, filter.errors.full_messages.join(', ') unless filter.validate({})
26
+
27
+ client.get(self.class.endpoint_path, params: filter.to_h)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyists
4
+ module Kalshi
5
+ module Market
6
+ # Market API Client
7
+ class Client
8
+ attr_reader :client
9
+
10
+ def initialize(client)
11
+ @client = client
12
+ end
13
+
14
+ def series_list
15
+ @series_list ||= SeriesList.new(client)
16
+ end
17
+
18
+ def series
19
+ @series ||= Series.new(client)
20
+ end
21
+
22
+ def markets
23
+ @markets ||= Markets.new(client)
24
+ end
25
+
26
+ def orderbook
27
+ @orderbook ||= Orderbook.new(client)
28
+ end
29
+
30
+ def trades
31
+ @trades ||= Trades.new(client)
32
+ end
33
+
34
+ def candlesticks
35
+ @candlesticks ||= Rubyists::Kalshi::Series::MarketCandlesticks.new(client)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyists
4
+ module Kalshi
5
+ module Market
6
+ # Markets API endpoint
7
+ class Markets < Kalshi::Endpoint
8
+ include Kalshi::Listable
9
+
10
+ kalshi_path 'markets'
11
+
12
+ # Filter for Kalshi markets list
13
+ class Filter < Kalshi::Contract
14
+ propertize(%i[limit cursor event_ticker series_ticker max_close_ts min_close_ts status tickers])
15
+
16
+ validation do
17
+ params do
18
+ optional(:limit).maybe(:integer)
19
+ optional(:cursor).maybe(:string)
20
+ optional(:event_ticker).maybe(:string)
21
+ optional(:series_ticker).maybe(:string)
22
+ optional(:max_close_ts).maybe(:integer)
23
+ optional(:min_close_ts).maybe(:integer)
24
+ optional(:status).maybe(:string)
25
+ optional(:tickers).maybe(:string)
26
+ end
27
+ end
28
+ end
29
+
30
+ def fetch(ticker)
31
+ client.get("markets/#{ticker}")
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyists
4
+ module Kalshi
5
+ module Market
6
+ # Orderbook API endpoint
7
+ class Orderbook < Kalshi::Endpoint
8
+ def fetch(ticker, depth: nil)
9
+ params = {}
10
+ params[:depth] = depth if depth
11
+ client.get("markets/#{ticker}/orderbook", params: params)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyists
4
+ module Kalshi
5
+ module Market
6
+ # Series API endpoint
7
+ class Series < Kalshi::Endpoint
8
+ def fetch(ticker)
9
+ client.get("series/#{ticker}")
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyists
4
+ module Kalshi
5
+ module Market
6
+ # Series List API endpoint
7
+ class SeriesList < Kalshi::Endpoint
8
+ include Kalshi::Listable
9
+
10
+ kalshi_path 'series'
11
+
12
+ # Filter for Kalshi series list
13
+ class Filter < Kalshi::Contract
14
+ propertize(%i[category tags include_product_metadata isInitialized status])
15
+
16
+ validation do
17
+ params do
18
+ optional(:category).maybe(:string)
19
+ optional(:tags).maybe(:string)
20
+ optional(:include_product_metadata).maybe(:bool)
21
+ optional(:isInitialized).maybe(:bool)
22
+ optional(:status).maybe(:string)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyists
4
+ module Kalshi
5
+ module Market
6
+ # Trades API endpoint
7
+ class Trades < Kalshi::Endpoint
8
+ include Kalshi::Listable
9
+
10
+ kalshi_path 'markets/trades'
11
+
12
+ # Filter for Kalshi trades list
13
+ class Filter < Kalshi::Contract
14
+ propertize(%i[limit cursor ticker min_ts max_ts])
15
+
16
+ validation do
17
+ params do
18
+ optional(:limit).maybe(:integer)
19
+ optional(:cursor).maybe(:string)
20
+ optional(:ticker).maybe(:string)
21
+ optional(:min_ts).maybe(:integer)
22
+ optional(:max_ts).maybe(:integer)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
File without changes
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyists
4
+ module Kalshi
5
+ module Search
6
+ # Search API Client
7
+ class Client < ApiClient
8
+ def tags_by_categories
9
+ SeriesTags.new(client).all
10
+ end
11
+
12
+ def filters_by_sport
13
+ SportsFilters.new(client).all
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyists
4
+ module Kalshi
5
+ module Search
6
+ # Series Tags API endpoint
7
+ class SeriesTags < Kalshi::Endpoint
8
+ def all
9
+ client.get('tags_by_categories')
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyists
4
+ module Kalshi
5
+ module Search
6
+ # Sports Filters API endpoint
7
+ class SportsFilters < Kalshi::Endpoint
8
+ def all
9
+ client.get('filters_by_sport')
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyists
4
+ module Kalshi
5
+ module Series
6
+ # Series API Client
7
+ class Client < ApiClient
8
+ def event_candlesticks
9
+ EventCandlesticks.new(client)
10
+ end
11
+
12
+ def forecast_percentile_history
13
+ ForecastPercentileHistory.new(client)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyists
4
+ module Kalshi
5
+ module Series
6
+ # Event Candlesticks API endpoint
7
+ class EventCandlesticks < Kalshi::Endpoint
8
+ # Filter for Event Candlesticks
9
+ class EventFilter < Kalshi::Contract
10
+ propertize(%i[series_ticker ticker start_ts end_ts period_interval])
11
+
12
+ validation do
13
+ params do
14
+ required(:series_ticker).filled(:string)
15
+ required(:ticker).filled(:string)
16
+ required(:start_ts).filled(:integer)
17
+ required(:end_ts).filled(:integer)
18
+ required(:period_interval).filled(:integer)
19
+ end
20
+ end
21
+ end
22
+
23
+ def fetch(params)
24
+ filter = EventFilter.new(EventFilter::Properties.new(**params))
25
+ raise ArgumentError, filter.errors.full_messages.join(', ') unless filter.validate({})
26
+
27
+ path = "#{filter.series_ticker}/events/#{filter.ticker}/candlesticks"
28
+ query_params = filter.to_h.slice('start_ts', 'end_ts', 'period_interval')
29
+ client.get(path, params: query_params)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyists
4
+ module Kalshi
5
+ module Series
6
+ # Event Forecast Percentile History API endpoint
7
+ class ForecastPercentileHistory < Kalshi::Endpoint
8
+ # Filter for Event Forecast Percentile History
9
+ class EventFilter < Kalshi::Contract
10
+ propertize(%i[series_ticker ticker percentiles start_ts end_ts period_interval])
11
+
12
+ validation do
13
+ params do
14
+ required(:series_ticker).filled(:string)
15
+ required(:ticker).filled(:string)
16
+ required(:percentiles).filled(:array)
17
+ required(:start_ts).filled(:integer)
18
+ required(:end_ts).filled(:integer)
19
+ required(:period_interval).filled(:integer)
20
+ end
21
+ end
22
+ end
23
+
24
+ def fetch(params)
25
+ filter = EventFilter.new(EventFilter::Properties.new(**params))
26
+ raise ArgumentError, filter.errors.full_messages.join(', ') unless filter.validate({})
27
+
28
+ path = "#{filter.series_ticker}/events/#{filter.ticker}/forecast_percentile_history"
29
+ query_params = filter.to_h.slice('start_ts', 'end_ts', 'period_interval')
30
+ query_params[:percentiles] = filter.percentiles.join(',')
31
+ client.get(path, params: query_params)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyists
4
+ module Kalshi
5
+ module Series
6
+ # Candlesticks API endpoint
7
+ class MarketCandlesticks < Kalshi::Endpoint
8
+ def fetch(series_ticker:, ticker:, start_ts:, end_ts:, period_interval:)
9
+ path = "series/#{series_ticker}/markets/#{ticker}/candlesticks"
10
+ params = { start_ts:, end_ts:, period_interval: }
11
+ client.get(path, params:)
12
+ end
13
+
14
+ def batch(tickers:, series_ticker:, start_ts:, end_ts:, period_interval:)
15
+ path = 'markets/candlesticks'
16
+ params = {
17
+ market_tickers: tickers.is_a?(Array) ? tickers.join(',') : tickers,
18
+ series_ticker:,
19
+ start_ts:,
20
+ end_ts:,
21
+ period_interval:
22
+ }
23
+ client.get(path, params:)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyists
4
+ # Kalshi module version
5
+ module Kalshi
6
+ # x-release-please-start-version
7
+ VERSION = '0.1.0'
8
+ # x-release-please-end
9
+ end
10
+ end