Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion lib/kalshi/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,20 @@ def search
@search ||= Search::Client.new(clone)
end

def events
@events ||= Events::Client.new(clone)
end

def series
@series ||= Series::Client.new(clone)
end

private

def full_url(path)
File.join(*[base_url, prefix, path].compact)
parts = [base_url, prefix, path].compact
parts.reject!(&:empty?)
File.join(*parts)
end

def handle_response(response)
Expand Down
13 changes: 13 additions & 0 deletions lib/kalshi/endpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ module Kalshi
class Endpoint
attr_reader :client

class << self
attr_accessor :endpoint_path

# Set the endpoint path for the resource
#
# @param path [String] API endpoint path
#
# @return [String] endpoint path
def kalshi_path(path)
self.endpoint_path = path
end
end

# Initialize the Endpoint
#
# @param client [Client] The Kalshi client
Expand Down
30 changes: 30 additions & 0 deletions lib/kalshi/events/client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# frozen_string_literal: true

module Rubyists
module Kalshi
module Events
# Events API Client
class Client < ApiClient
def list(...)
List.new(client).list(...)
end

def fetch(...)
List.new(client).fetch(...)
end

def metadata(...)
List.new(client).metadata(...)
end

def multivariate
Multivariate.new(client)
Comment on lines +20 to +21
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Events::Client methods should use memoization for consistency with the established pattern in Market::Client (lib/kalshi/market/client.rb:14-35). All methods in Market::Client use the ||= operator to cache instances. Consider adding memoization like @multivariate ||= Multivariate.new(client) for the multivariate method.

Copilot uses AI. Check for mistakes.
end

def candlesticks
Rubyists::Kalshi::Series::EventCandlesticks.new(client)
Comment on lines +24 to +25
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The candlesticks method is not tested. Following the established testing pattern in this file (tests for list, fetch, metadata, and multivariate), consider adding a test case for the candlesticks method similar to the multivariate test.

Copilot uses AI. Check for mistakes.
end
end
end
end
end
37 changes: 37 additions & 0 deletions lib/kalshi/events/list.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# frozen_string_literal: true

module Rubyists
module Kalshi
module Events
# Events API endpoint
class List < Kalshi::Endpoint
include Kalshi::Listable

kalshi_path ''

# Filter for Kalshi events list
class Filter < Kalshi::Contract
propertize(%i[limit cursor status series_ticker with_nested_markets])

validation do
params do
optional(:limit).maybe(:integer)
optional(:cursor).maybe(:string)
optional(:status).maybe(:string)
optional(:series_ticker).maybe(:string)
optional(:with_nested_markets).maybe(:bool)
end
end
end

def fetch(event_ticker)
client.get(event_ticker)
end

def metadata(event_ticker)
client.get("#{event_ticker}/metadata")
end
end
end
end
end
28 changes: 28 additions & 0 deletions lib/kalshi/events/multivariate.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

module Rubyists
module Kalshi
module Events
# Multivariate Events API endpoint
class Multivariate < Kalshi::Endpoint
include Kalshi::Listable

kalshi_path 'multivariate'

# Filter for Kalshi multivariate events list
class Filter < Kalshi::Contract
propertize(%i[limit cursor series_ticker collection_ticker])

validation do
params do
optional(:limit).maybe(:integer)
optional(:cursor).maybe(:string)
optional(:series_ticker).maybe(:string)
optional(:collection_ticker).maybe(:string)
end
end
end
end
end
end
end
11 changes: 0 additions & 11 deletions lib/kalshi/listable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,6 @@ def self.included(base)

# Class methods for Listable
module ClassMethods
attr_accessor :endpoint_path

# Set the endpoint path for the resource
#
# @param path [String] API endpoint path
#
# @return [String] endpoint path
def kalshi_path(path)
self.endpoint_path = path
end

def list(...)
new.list(...)
end
Expand Down
2 changes: 1 addition & 1 deletion lib/kalshi/market/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def trades
end

def candlesticks
@candlesticks ||= Candlesticks.new(client)
@candlesticks ||= Rubyists::Kalshi::Series::MarketCandlesticks.new(client)
end
end
end
Expand Down
18 changes: 18 additions & 0 deletions lib/kalshi/series/client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

module Rubyists
module Kalshi
module Series
# Series API Client
class Client < ApiClient
def event_candlesticks
EventCandlesticks.new(client)
end

def forecast_percentile_history
ForecastPercentileHistory.new(client)
end
Comment on lines +8 to +14
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Series::Client methods should use memoization, consistent with the established pattern in Market::Client (lib/kalshi/market/client.rb:14-35). All methods in Market::Client use the ||= operator to cache instances and have corresponding tests verifying this behavior (test/kalshi/market/client_test.rb:14-66). Consider adding memoization like @event_candlesticks ||= EventCandlesticks.new(client) and @forecast_percentile_history ||= ForecastPercentileHistory.new(client).

Copilot uses AI. Check for mistakes.
end
end
end
end
34 changes: 34 additions & 0 deletions lib/kalshi/series/event_candlesticks.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: true

module Rubyists
module Kalshi
module Series
# Event Candlesticks API endpoint
class EventCandlesticks < Kalshi::Endpoint
# Filter for Event Candlesticks
class EventFilter < Kalshi::Contract
propertize(%i[series_ticker ticker start_ts end_ts period_interval])

validation do
params do
required(:series_ticker).filled(:string)
required(:ticker).filled(:string)
required(:start_ts).filled(:integer)
required(:end_ts).filled(:integer)
required(:period_interval).filled(:integer)
end
end
end

def fetch(params)
filter = EventFilter.new(EventFilter::Properties.new(**params))
raise ArgumentError, filter.errors.full_messages.join(', ') unless filter.validate({})

path = "#{filter.series_ticker}/events/#{filter.ticker}/candlesticks"
query_params = filter.to_h.slice('start_ts', 'end_ts', 'period_interval')
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The slice method is called with string keys ('start_ts', 'end_ts', 'period_interval'), but filter.to_h likely returns a hash with symbol keys based on the Reform library's behavior. Hash#slice with string keys won't match symbol keys, which could result in an empty query_params hash. Consider using symbol keys in the slice call: filter.to_h.slice(:start_ts, :end_ts, :period_interval).

Suggested change
query_params = filter.to_h.slice('start_ts', 'end_ts', 'period_interval')
query_params = filter.to_h.slice(:start_ts, :end_ts, :period_interval)

Copilot uses AI. Check for mistakes.
client.get(path, params: query_params)
end
end
end
end
end
36 changes: 36 additions & 0 deletions lib/kalshi/series/forecast_percentile_history.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

module Rubyists
module Kalshi
module Series
# Event Forecast Percentile History API endpoint
class ForecastPercentileHistory < Kalshi::Endpoint
# Filter for Event Forecast Percentile History
class EventFilter < Kalshi::Contract
propertize(%i[series_ticker ticker percentiles start_ts end_ts period_interval])

validation do
params do
required(:series_ticker).filled(:string)
required(:ticker).filled(:string)
required(:percentiles).filled(:array)
required(:start_ts).filled(:integer)
required(:end_ts).filled(:integer)
required(:period_interval).filled(:integer)
end
end
end

def fetch(params)
filter = EventFilter.new(EventFilter::Properties.new(**params))
raise ArgumentError, filter.errors.full_messages.join(', ') unless filter.validate({})

path = "#{filter.series_ticker}/events/#{filter.ticker}/forecast_percentile_history"
query_params = filter.to_h.slice('start_ts', 'end_ts', 'period_interval')
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The slice method is called with string keys ('start_ts', 'end_ts', 'period_interval'), but filter.to_h likely returns a hash with symbol keys based on the Reform library's behavior. Hash#slice with string keys won't match symbol keys, which could result in an empty query_params hash. Consider using symbol keys in the slice call: filter.to_h.slice(:start_ts, :end_ts, :period_interval).

Suggested change
query_params = filter.to_h.slice('start_ts', 'end_ts', 'period_interval')
query_params = filter.to_h.slice(:start_ts, :end_ts, :period_interval)

Copilot uses AI. Check for mistakes.
query_params[:percentiles] = filter.percentiles.join(',')
client.get(path, params: query_params)
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

module Rubyists
module Kalshi
module Market
module Series
# Candlesticks API endpoint
class Candlesticks < Kalshi::Endpoint
class MarketCandlesticks < Kalshi::Endpoint
def fetch(series_ticker:, ticker:, start_ts:, end_ts:, period_interval:)
path = "series/#{series_ticker}/markets/#{ticker}/candlesticks"
params = { start_ts:, end_ts:, period_interval: }
Expand Down
16 changes: 16 additions & 0 deletions test/kalshi/client_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,22 @@
assert_instance_of Rubyists::Kalshi::Search::Client, client.search
end
end

describe '#events' do
it 'returns an Events::Client instance' do
client = Rubyists::Kalshi::Client.new

assert_instance_of Rubyists::Kalshi::Events::Client, client.events
end
end

describe '#series' do
it 'returns a Series::Client instance' do
client = Rubyists::Kalshi::Client.new

assert_instance_of Rubyists::Kalshi::Series::Client, client.series
end
end
end

describe Rubyists::Kalshi do
Expand Down
50 changes: 50 additions & 0 deletions test/kalshi/events/client_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# frozen_string_literal: true

require_relative '../../helper'

describe Rubyists::Kalshi::Events::Client do
let(:client) { Rubyists::Kalshi::Client.new }
let(:events_client) { client.events }
let(:base_url) { Rubyists::Kalshi.config.base_url }

describe '#list' do
it 'fetches the events list' do
stub_request(:get, "#{base_url}/events")
.to_return(status: 200, body: '{"events": []}', headers: { 'Content-Type' => 'application/json' })

response = events_client.list

assert_equal({ events: [] }, response)
end
end

describe '#fetch' do
it 'fetches a specific event by ticker' do
ticker = 'KX-EVENT'
stub_request(:get, "#{base_url}/events/#{ticker}")
.to_return(status: 200, body: '{"event": {}}', headers: { 'Content-Type' => 'application/json' })

response = events_client.fetch(ticker)

assert_equal({ event: {} }, response)
end
end

describe '#metadata' do
it 'fetches event metadata' do
ticker = 'KX-EVENT'
stub_request(:get, "#{base_url}/events/#{ticker}/metadata")
.to_return(status: 200, body: '{"metadata": {}}', headers: { 'Content-Type' => 'application/json' })

response = events_client.metadata(ticker)

assert_equal({ metadata: {} }, response)
end
end

describe '#multivariate' do
it 'returns a Multivariate instance' do
assert_instance_of Rubyists::Kalshi::Events::Multivariate, events_client.multivariate
end
end
end
30 changes: 30 additions & 0 deletions test/kalshi/events/multivariate_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# frozen_string_literal: true

require_relative '../../helper'

describe Rubyists::Kalshi::Events::Multivariate do
let(:client) { Rubyists::Kalshi::Client.new }
let(:multivariate) { client.events.multivariate }
let(:base_url) { Rubyists::Kalshi.config.base_url }

describe '#list' do
it 'fetches the multivariate events list' do
stub_request(:get, "#{base_url}/events/multivariate")
.to_return(status: 200, body: '{"events": []}', headers: { 'Content-Type' => 'application/json' })

response = multivariate.list

assert_equal({ events: [] }, response)
end

it 'fetches the multivariate events list with filters' do
stub_request(:get, "#{base_url}/events/multivariate")
.with(query: { series_ticker: 'KX-SERIES' })
.to_return(status: 200, body: '{"events": []}', headers: { 'Content-Type' => 'application/json' })

response = multivariate.list(series_ticker: 'KX-SERIES')

assert_equal({ events: [] }, response)
end
end
end
4 changes: 2 additions & 2 deletions test/kalshi/market/candlesticks_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

require_relative '../../helper'

describe Rubyists::Kalshi::Market::Candlesticks do
describe Rubyists::Kalshi::Series::MarketCandlesticks do
let(:client) { Rubyists::Kalshi::Client.new }
let(:candlesticks) { Rubyists::Kalshi::Market::Candlesticks.new(client) }
let(:candlesticks) { client.market.candlesticks }
let(:base_url) { Rubyists::Kalshi.config.base_url }

describe '#fetch' do
Expand Down
2 changes: 1 addition & 1 deletion test/kalshi/market/client_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@

describe '#candlesticks' do
it 'returns a Candlesticks instance' do
assert_instance_of Rubyists::Kalshi::Market::Candlesticks, market_client.candlesticks
assert_instance_of Rubyists::Kalshi::Series::MarketCandlesticks, market_client.candlesticks
end

it 'memoizes the instance' do
Expand Down
Loading
Loading