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
92 changes: 88 additions & 4 deletions Readme.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -69,7 +69,91 @@ Get a specific series by ticker:

[source,ruby]
----
series = Rubyists::Kalshi::Market::Series.new.fetch('KX-SERIES')
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
Expand Down
4 changes: 4 additions & 0 deletions lib/kalshi/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
28 changes: 28 additions & 0 deletions lib/kalshi/market/candlesticks.rb
Original file line number Diff line number Diff line change
@@ -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
40 changes: 40 additions & 0 deletions lib/kalshi/market/client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# 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

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
end
36 changes: 36 additions & 0 deletions lib/kalshi/market/markets.rb
Original file line number Diff line number Diff line change
@@ -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
16 changes: 16 additions & 0 deletions lib/kalshi/market/orderbook.rb
Original file line number Diff line number Diff line change
@@ -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
29 changes: 29 additions & 0 deletions lib/kalshi/market/trades.rb
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions test/kalshi/client_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +21 to +27
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.

Consider adding a test to verify that the market method is memoized, similar to the memoization tests for series_list, series, markets, orderbook, trades, and candlesticks in the Market::Client tests. This ensures consistent caching behavior.

Copilot uses AI. Check for mistakes.
end

describe Rubyists::Kalshi do
Expand Down
91 changes: 91 additions & 0 deletions test/kalshi/market/candlesticks_test.rb
Original file line number Diff line number Diff line change
@@ -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
Loading