From 601ac2a7d4c6973804707c41bc28b2b26d22b228 Mon Sep 17 00:00:00 2001 From: "Tj (bougyman) Vanderpoel" Date: Sun, 4 Jan 2026 18:21:31 -0600 Subject: [PATCH 1/2] doc: updated Readme with a more real-world filter use case --- Readme.adoc | 8 ++++---- lib/kalshi/client.rb | 4 ++++ lib/kalshi/market/client.rb | 24 ++++++++++++++++++++++++ test/kalshi/client_test.rb | 8 ++++++++ test/kalshi/market/client_test.rb | 28 ++++++++++++++++++++++++++++ 5 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 lib/kalshi/market/client.rb create mode 100644 test/kalshi/market/client_test.rb diff --git a/Readme.adoc b/Readme.adoc index e537f80..59210ca 100644 --- a/Readme.adoc +++ b/Readme.adoc @@ -44,7 +44,7 @@ client = Rubyists::Kalshi.client === Market -Access Market endpoints via the `market` namespace (to be implemented in client wrapper, currently direct usage). +Access Market endpoints via the `market` namespace. ==== Series List @@ -55,10 +55,10 @@ List series with optional filtering: [source,ruby] ---- # List series with default filter -series_list = Rubyists::Kalshi::Market::SeriesList.list +series_list = client.market.series_list.list # List series with custom filter -series_list = Rubyists::Kalshi::Market::SeriesList.list(category: 'Politics', status: 'active') +series_list = client.market.series_list.list(category: 'Sports', tags: 'Football') ---- ==== Series @@ -69,7 +69,7 @@ Get a specific series by ticker: [source,ruby] ---- -series = Rubyists::Kalshi::Market::Series.new.fetch('KX-SERIES') +series = client.market.series.fetch('KX-SERIES') ---- === WebSocket Client diff --git a/lib/kalshi/client.rb b/lib/kalshi/client.rb index 2402e9a..0c54f02 100644 --- a/lib/kalshi/client.rb +++ b/lib/kalshi/client.rb @@ -24,6 +24,10 @@ def get(path, params: {}) handle_response(response) end + def market + @market ||= Market::Client.new(clone) + end + private def full_url(path) diff --git a/lib/kalshi/market/client.rb b/lib/kalshi/market/client.rb new file mode 100644 index 0000000..edb986e --- /dev/null +++ b/lib/kalshi/market/client.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Rubyists + module Kalshi + module Market + # Market API Client + class Client + attr_reader :client + + def initialize(client) + @client = client + end + + def series_list + @series_list ||= SeriesList.new(client) + end + + def series + @series ||= Series.new(client) + end + end + end + end +end diff --git a/test/kalshi/client_test.rb b/test/kalshi/client_test.rb index 759e2c2..669e8aa 100644 --- a/test/kalshi/client_test.rb +++ b/test/kalshi/client_test.rb @@ -17,6 +17,14 @@ assert_match(/API Error: 400 - Bad Request/, error.message) end end + + describe '#market' do + it 'returns a Market::Client instance' do + client = Rubyists::Kalshi::Client.new + + assert_instance_of Rubyists::Kalshi::Market::Client, client.market + end + end end describe Rubyists::Kalshi do diff --git a/test/kalshi/market/client_test.rb b/test/kalshi/market/client_test.rb new file mode 100644 index 0000000..e9fe269 --- /dev/null +++ b/test/kalshi/market/client_test.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require_relative '../../helper' + +describe Rubyists::Kalshi::Market::Client do + let(:client) { Rubyists::Kalshi::Client.new } + let(:market_client) { client.market } + + describe '#series_list' do + it 'returns a SeriesList instance' do + assert_instance_of Rubyists::Kalshi::Market::SeriesList, market_client.series_list + end + + it 'memoizes the instance' do + assert_same market_client.series_list, market_client.series_list + end + end + + describe '#series' do + it 'returns a Series instance' do + assert_instance_of Rubyists::Kalshi::Market::Series, market_client.series + end + + it 'memoizes the instance' do + assert_same market_client.series, market_client.series + end + end +end From 7e8ceb543a5f8dd71d058dac99fc6b97e707d03a Mon Sep 17 00:00:00 2001 From: "Tj (bougyman) Vanderpoel" Date: Sun, 4 Jan 2026 18:33:28 -0600 Subject: [PATCH 2/2] feat: adds all market/ endpoints --- Readme.adoc | 84 +++++++++++++++++++++++ lib/kalshi/market/candlesticks.rb | 28 ++++++++ lib/kalshi/market/client.rb | 16 +++++ lib/kalshi/market/markets.rb | 36 ++++++++++ lib/kalshi/market/orderbook.rb | 16 +++++ lib/kalshi/market/trades.rb | 29 ++++++++ test/kalshi/market/candlesticks_test.rb | 91 +++++++++++++++++++++++++ test/kalshi/market/client_test.rb | 40 +++++++++++ test/kalshi/market/markets_test.rb | 42 ++++++++++++ test/kalshi/market/orderbook_test.rb | 32 +++++++++ test/kalshi/market/trades_test.rb | 30 ++++++++ 11 files changed, 444 insertions(+) create mode 100644 lib/kalshi/market/candlesticks.rb create mode 100644 lib/kalshi/market/markets.rb create mode 100644 lib/kalshi/market/orderbook.rb create mode 100644 lib/kalshi/market/trades.rb create mode 100644 test/kalshi/market/candlesticks_test.rb create mode 100644 test/kalshi/market/markets_test.rb create mode 100644 test/kalshi/market/orderbook_test.rb create mode 100644 test/kalshi/market/trades_test.rb diff --git a/Readme.adoc b/Readme.adoc index 59210ca..44f83e8 100644 --- a/Readme.adoc +++ b/Readme.adoc @@ -72,6 +72,90 @@ Get a specific series by ticker: series = client.market.series.fetch('KX-SERIES') ---- +==== Markets + +https://docs.kalshi.com/api-reference/market/get-markets[API Reference] + +List markets with optional filtering: + +[source,ruby] +---- +# List markets with default filter +markets = client.market.markets.list + +# List markets with custom filter +markets = client.market.markets.list(status: 'open', limit: 10) +---- + +Get a specific market by ticker: + +[source,ruby] +---- +market = client.market.markets.fetch('KX-MARKET') +---- + +==== Orderbook + +https://docs.kalshi.com/api-reference/market/get-market-orderbook[API Reference] + +Get the orderbook for a market: + +[source,ruby] +---- +# Get full orderbook +orderbook = client.market.orderbook.fetch('KX-MARKET') + +# Get orderbook with specific depth +orderbook = client.market.orderbook.fetch('KX-MARKET', depth: 10) +---- + +==== Trades + +https://docs.kalshi.com/api-reference/market/get-trades[API Reference] + +List trades with optional filtering: + +[source,ruby] +---- +# List trades with default filter +trades = client.market.trades.list + +# List trades with custom filter +trades = client.market.trades.list(ticker: 'KX-MARKET', limit: 100) +---- + +==== Candlesticks + +https://docs.kalshi.com/api-reference/market/get-market-candlesticks[API Reference] + +Get candlesticks for a market: + +[source,ruby] +---- +candlesticks = client.market.candlesticks.fetch( + series_ticker: 'KX-SERIES', + ticker: 'KX-MARKET', + start_ts: 1600000000, + end_ts: 1600000060, + period_interval: 1 +) +---- + +https://docs.kalshi.com/api-reference/market/batch-get-market-candlesticks[API Reference] + +Get candlesticks for multiple markets: + +[source,ruby] +---- +candlesticks = client.market.candlesticks.batch( + tickers: ['KX-MARKET-1', 'KX-MARKET-2'], + series_ticker: 'KX-SERIES', + start_ts: 1600000000, + end_ts: 1600000060, + period_interval: 1 +) +---- + === WebSocket Client A raw WebSocket client is included in `bin/wss-raw` for testing connection and subscription. diff --git a/lib/kalshi/market/candlesticks.rb b/lib/kalshi/market/candlesticks.rb new file mode 100644 index 0000000..f0a1214 --- /dev/null +++ b/lib/kalshi/market/candlesticks.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Rubyists + module Kalshi + module Market + # Candlesticks API endpoint + class Candlesticks < 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: } + client.get(path, params:) + end + + def batch(tickers:, series_ticker:, start_ts:, end_ts:, period_interval:) + path = 'markets/candlesticks' + params = { + market_tickers: tickers.is_a?(Array) ? tickers.join(',') : tickers, + series_ticker:, + start_ts:, + end_ts:, + period_interval: + } + client.get(path, params:) + end + end + end + end +end diff --git a/lib/kalshi/market/client.rb b/lib/kalshi/market/client.rb index edb986e..1c41fe3 100644 --- a/lib/kalshi/market/client.rb +++ b/lib/kalshi/market/client.rb @@ -18,6 +18,22 @@ def series_list def series @series ||= Series.new(client) end + + def markets + @markets ||= Markets.new(client) + end + + def orderbook + @orderbook ||= Orderbook.new(client) + end + + def trades + @trades ||= Trades.new(client) + end + + def candlesticks + @candlesticks ||= Candlesticks.new(client) + end end end end diff --git a/lib/kalshi/market/markets.rb b/lib/kalshi/market/markets.rb new file mode 100644 index 0000000..d496223 --- /dev/null +++ b/lib/kalshi/market/markets.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Rubyists + module Kalshi + module Market + # Markets API endpoint + class Markets < Kalshi::Endpoint + include Kalshi::Listable + + kalshi_path 'markets' + + # Filter for Kalshi markets list + class Filter < Kalshi::Contract + propertize(%i[limit cursor event_ticker series_ticker max_close_ts min_close_ts status tickers]) + + validation do + params do + optional(:limit).maybe(:integer) + optional(:cursor).maybe(:string) + optional(:event_ticker).maybe(:string) + optional(:series_ticker).maybe(:string) + optional(:max_close_ts).maybe(:integer) + optional(:min_close_ts).maybe(:integer) + optional(:status).maybe(:string) + optional(:tickers).maybe(:string) + end + end + end + + def fetch(ticker) + client.get("markets/#{ticker}") + end + end + end + end +end diff --git a/lib/kalshi/market/orderbook.rb b/lib/kalshi/market/orderbook.rb new file mode 100644 index 0000000..9bd2c79 --- /dev/null +++ b/lib/kalshi/market/orderbook.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Rubyists + module Kalshi + module Market + # Orderbook API endpoint + class Orderbook < Kalshi::Endpoint + def fetch(ticker, depth: nil) + params = {} + params[:depth] = depth if depth + client.get("markets/#{ticker}/orderbook", params: params) + end + end + end + end +end diff --git a/lib/kalshi/market/trades.rb b/lib/kalshi/market/trades.rb new file mode 100644 index 0000000..edadde7 --- /dev/null +++ b/lib/kalshi/market/trades.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Rubyists + module Kalshi + module Market + # Trades API endpoint + class Trades < Kalshi::Endpoint + include Kalshi::Listable + + kalshi_path 'markets/trades' + + # Filter for Kalshi trades list + class Filter < Kalshi::Contract + propertize(%i[limit cursor ticker min_ts max_ts]) + + validation do + params do + optional(:limit).maybe(:integer) + optional(:cursor).maybe(:string) + optional(:ticker).maybe(:string) + optional(:min_ts).maybe(:integer) + optional(:max_ts).maybe(:integer) + end + end + end + end + end + end +end diff --git a/test/kalshi/market/candlesticks_test.rb b/test/kalshi/market/candlesticks_test.rb new file mode 100644 index 0000000..f849bcb --- /dev/null +++ b/test/kalshi/market/candlesticks_test.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require_relative '../../helper' + +describe Rubyists::Kalshi::Market::Candlesticks do + let(:client) { Rubyists::Kalshi::Client.new } + let(:candlesticks) { Rubyists::Kalshi::Market::Candlesticks.new(client) } + let(:base_url) { Rubyists::Kalshi.config.base_url } + + describe '#fetch' do + it 'fetches candlesticks for a market' do + series_ticker = 'KX-SERIES' + ticker = 'KX-MARKET' + start_ts = 1_600_000_000 + end_ts = 1_600_000_060 + period_interval = 1 + + stub_request(:get, "#{base_url}/series/#{series_ticker}/markets/#{ticker}/candlesticks") + .with(query: { start_ts: start_ts, end_ts: end_ts, period_interval: period_interval }) + .to_return(status: 200, body: '{"candlesticks": []}', headers: { 'Content-Type' => 'application/json' }) + + response = candlesticks.fetch( + series_ticker: series_ticker, + ticker: ticker, + start_ts: start_ts, + end_ts: end_ts, + period_interval: period_interval + ) + + assert_equal({ candlesticks: [] }, response) + end + end + + describe '#batch' do + it 'fetches candlesticks for multiple markets' do + tickers = %w[KX-MARKET-1 KX-MARKET-2] + series_ticker = 'KX-SERIES' + start_ts = 1_600_000_000 + end_ts = 1_600_000_060 + period_interval = 1 + + stub_request(:get, "#{base_url}/markets/candlesticks") + .with(query: { + market_tickers: tickers.join(','), + series_ticker: series_ticker, + start_ts: start_ts, + end_ts: end_ts, + period_interval: period_interval + }) + .to_return(status: 200, body: '{"markets": []}', headers: { 'Content-Type' => 'application/json' }) + + response = candlesticks.batch( + tickers: tickers, + series_ticker: series_ticker, + start_ts: start_ts, + end_ts: end_ts, + period_interval: period_interval + ) + + assert_equal({ markets: [] }, response) + end + + it 'handles single ticker string in batch' do + tickers = 'KX-MARKET-1' + series_ticker = 'KX-SERIES' + start_ts = 1_600_000_000 + end_ts = 1_600_000_060 + period_interval = 1 + + stub_request(:get, "#{base_url}/markets/candlesticks") + .with(query: { + market_tickers: tickers, + series_ticker: series_ticker, + start_ts: start_ts, + end_ts: end_ts, + period_interval: period_interval + }) + .to_return(status: 200, body: '{"markets": []}', headers: { 'Content-Type' => 'application/json' }) + + response = candlesticks.batch( + tickers: tickers, + series_ticker: series_ticker, + start_ts: start_ts, + end_ts: end_ts, + period_interval: period_interval + ) + + assert_equal({ markets: [] }, response) + end + end +end diff --git a/test/kalshi/market/client_test.rb b/test/kalshi/market/client_test.rb index e9fe269..cfc2115 100644 --- a/test/kalshi/market/client_test.rb +++ b/test/kalshi/market/client_test.rb @@ -25,4 +25,44 @@ assert_same market_client.series, market_client.series end end + + describe '#markets' do + it 'returns a Markets instance' do + assert_instance_of Rubyists::Kalshi::Market::Markets, market_client.markets + end + + it 'memoizes the instance' do + assert_same market_client.markets, market_client.markets + end + end + + describe '#orderbook' do + it 'returns an Orderbook instance' do + assert_instance_of Rubyists::Kalshi::Market::Orderbook, market_client.orderbook + end + + it 'memoizes the instance' do + assert_same market_client.orderbook, market_client.orderbook + end + end + + describe '#trades' do + it 'returns a Trades instance' do + assert_instance_of Rubyists::Kalshi::Market::Trades, market_client.trades + end + + it 'memoizes the instance' do + assert_same market_client.trades, market_client.trades + end + end + + describe '#candlesticks' do + it 'returns a Candlesticks instance' do + assert_instance_of Rubyists::Kalshi::Market::Candlesticks, market_client.candlesticks + end + + it 'memoizes the instance' do + assert_same market_client.candlesticks, market_client.candlesticks + end + end end diff --git a/test/kalshi/market/markets_test.rb b/test/kalshi/market/markets_test.rb new file mode 100644 index 0000000..d177e10 --- /dev/null +++ b/test/kalshi/market/markets_test.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require_relative '../../helper' + +describe Rubyists::Kalshi::Market::Markets do + let(:client) { Rubyists::Kalshi::Client.new } + let(:markets) { Rubyists::Kalshi::Market::Markets.new(client) } + let(:base_url) { Rubyists::Kalshi.config.base_url } + + describe '#list' do + it 'fetches the markets list' do + stub_request(:get, "#{base_url}/markets") + .to_return(status: 200, body: '{"markets": []}', headers: { 'Content-Type' => 'application/json' }) + + response = markets.list + + assert_equal({ markets: [] }, response) + end + + it 'fetches the markets list with filters' do + stub_request(:get, "#{base_url}/markets") + .with(query: { status: 'open' }) + .to_return(status: 200, body: '{"markets": []}', headers: { 'Content-Type' => 'application/json' }) + + response = markets.list(status: 'open') + + assert_equal({ markets: [] }, response) + end + end + + describe '#fetch' do + it 'fetches a specific market by ticker' do + ticker = 'KX-MARKET' + stub_request(:get, "#{base_url}/markets/#{ticker}") + .to_return(status: 200, body: '{"market": {}}', headers: { 'Content-Type' => 'application/json' }) + + response = markets.fetch(ticker) + + assert_equal({ market: {} }, response) + end + end +end diff --git a/test/kalshi/market/orderbook_test.rb b/test/kalshi/market/orderbook_test.rb new file mode 100644 index 0000000..e030973 --- /dev/null +++ b/test/kalshi/market/orderbook_test.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require_relative '../../helper' + +describe Rubyists::Kalshi::Market::Orderbook do + let(:client) { Rubyists::Kalshi::Client.new } + let(:orderbook) { Rubyists::Kalshi::Market::Orderbook.new(client) } + let(:base_url) { Rubyists::Kalshi.config.base_url } + + describe '#fetch' do + it 'fetches the orderbook for a market' do + ticker = 'KX-MARKET' + stub_request(:get, "#{base_url}/markets/#{ticker}/orderbook") + .to_return(status: 200, body: '{"orderbook": {}}', headers: { 'Content-Type' => 'application/json' }) + + response = orderbook.fetch(ticker) + + assert_equal({ orderbook: {} }, response) + end + + it 'fetches the orderbook with depth' do + ticker = 'KX-MARKET' + stub_request(:get, "#{base_url}/markets/#{ticker}/orderbook") + .with(query: { depth: 10 }) + .to_return(status: 200, body: '{"orderbook": {}}', headers: { 'Content-Type' => 'application/json' }) + + response = orderbook.fetch(ticker, depth: 10) + + assert_equal({ orderbook: {} }, response) + end + end +end diff --git a/test/kalshi/market/trades_test.rb b/test/kalshi/market/trades_test.rb new file mode 100644 index 0000000..cc503c2 --- /dev/null +++ b/test/kalshi/market/trades_test.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require_relative '../../helper' + +describe Rubyists::Kalshi::Market::Trades do + let(:client) { Rubyists::Kalshi::Client.new } + let(:trades) { Rubyists::Kalshi::Market::Trades.new(client) } + let(:base_url) { Rubyists::Kalshi.config.base_url } + + describe '#list' do + it 'fetches the trades list' do + stub_request(:get, "#{base_url}/markets/trades") + .to_return(status: 200, body: '{"trades": []}', headers: { 'Content-Type' => 'application/json' }) + + response = trades.list + + assert_equal({ trades: [] }, response) + end + + it 'fetches the trades list with filters' do + stub_request(:get, "#{base_url}/markets/trades") + .with(query: { ticker: 'KX-MARKET' }) + .to_return(status: 200, body: '{"trades": []}', headers: { 'Content-Type' => 'application/json' }) + + response = trades.list(ticker: 'KX-MARKET') + + assert_equal({ trades: [] }, response) + end + end +end