Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Websockets #14

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
215 changes: 189 additions & 26 deletions Cargo.lock

Large diffs are not rendered by default.

116 changes: 74 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,60 +2,92 @@

## Rust Wrapper for the Kalshi trading API

This is a wrapper for the [Kalshi](https://kalshi.com/) trading API written by and for those using Rust.
This wrapper is asynchronous and typically more performant than the official Python API provided by the developers, presented here: [*KalshiDevAPI*](https://github.com/Kalshi/kalshi-python).
This is a wrapper for the [Kalshi](https://kalshi.com/) trading API written by
and for those using Rust. This wrapper is asynchronous and typically more
performant than the official Python API provided by the developers, presented
here: [_KalshiDevAPI_](https://github.com/Kalshi/kalshi-python).

You can authenticate using email/password or api key (even for websockets).

## Documentation
Read the fully-featured docs [here](https://docs.rs/kalshi/0.9.0/kalshi/) and check the project out on [crates.io](https://crates.io/crates/kalshi/0.9.0).

Read the fully-featured docs [here](https://docs.rs/kalshi/0.9.0/kalshi/) and
check the project out on [crates.io](https://crates.io/crates/kalshi/0.9.0).

## Sample Bot

The Sample Bot directory is an example script that completes all the tasks required to obtain advanced API access from the developers.
The Sample Bot directory is an example script that completes all the tasks
required to obtain advanced API access from the developers.

## Featurelist + Roadmap

### HTTP Requests: ✅
### HTTP Requests: ✅

As of now the project supports interacting with Kalshi's RESTful API **fully**.

However any user of this library can build a rust trading bot using this library
However any user of this library can build a rust trading bot using this library
if they wish!

Every function present in the library wraps around the [Kalshi Trading API](https://trading-api.readme.io/reference/getting-started).
Every function present in the library wraps around the
[Kalshi Trading API](https://trading-api.readme.io/reference/getting-started).

#### HTTP Feature Table

| Feature | Description | Status |
|------------------------|---------------------------------------|-------------|
| **Auth/Login** | Retreiving your user token | ✅ |
| **Auth/Logout** | Deleting your user token | ✅ |
| **Exchange/GetSchedule** | Retrieve Exchange Schedule | ✅ |
| **Exchange/GetExchangeStatus** | Retreive Exchange Status | ✅ |
| **Portfolio/GetBalance** | Get User Balance | ✅ |
| **Portfolio/GetFills** | Get User's Fills that fit certain criteria| ✅ |
| **Portfolio/GetOrders** | Get User's orders that fit certain criteria | ✅ |
| **Portfolio/CreateOrder** | Submit an Order |✅ |
| **Portfolio/BatchCreateOrders** | Submit multiple Orders |❌ |
| **Portfolio/BatchCancelOrders** | Cancel Multiple Orders (Advanced Users Only) | ✅ |
| **Portfolio/GetOrder** | Get a single Order | ✅ |
| **Portfolio/CancelOrder** | Cancel an order |✅ |
| **Portfolio/DecreaseOrder** | Decrease Order amount |✅ |
| **Portfolio/GetPositions** | Get Positions (Get all the positions of logged in user) |✅ |
| **Portfolio/GetPortfolioSettlements** | Get Portfolio Settlements (Get settlement history) |✅ |
| **Market/GetEvents** | Get data about all events |✅ |
| **Market/GetEvent** | Get data about a single event |✅ |
| **Market/GetMarkets** | Get data about all markets |✅ |
| **Market/GetTrades** | Get data about trades fitting certain criteria |✅ |
| **Market/GetMarket** | Get data about a single market |✅ |
| **Market/GetMarketHistory** | Get data about a single market's historical data |✅ |
| **Market/GetMarketOrderBook** | Get a market's order book |✅ |
| **Market/GetSeries** | Get data about a series |✅ |










| Feature | Description | Status |
| ------------------------------------- | ------------------------------------------------------- | ------ |
| **Auth/Login** | Retreiving your user token | ✅ |
| **Auth/Logout** | Deleting your user token | ✅ |
| **Exchange/GetSchedule** | Retrieve Exchange Schedule | ✅ |
| **Exchange/GetExchangeStatus** | Retreive Exchange Status | ✅ |
| **Portfolio/GetBalance** | Get User Balance | ✅ |
| **Portfolio/GetFills** | Get User's Fills that fit certain criteria | ✅ |
| **Portfolio/GetOrders** | Get User's orders that fit certain criteria | ✅ |
| **Portfolio/CreateOrder** | Submit an Order | ✅ |
| **Portfolio/BatchCreateOrders** | Submit multiple Orders | ❌ |
| **Portfolio/BatchCancelOrders** | Cancel Multiple Orders (Advanced Users Only) | ✅ |
| **Portfolio/GetOrder** | Get a single Order | ✅ |
| **Portfolio/CancelOrder** | Cancel an order | ✅ |
| **Portfolio/DecreaseOrder** | Decrease Order amount | ✅ |
| **Portfolio/GetPositions** | Get Positions (Get all the positions of logged in user) | ✅ |
| **Portfolio/GetPortfolioSettlements** | Get Portfolio Settlements (Get settlement history) | ✅ |
| **Market/GetEvents** | Get data about all events | ✅ |
| **Market/GetEvent** | Get data about a single event | ✅ |
| **Market/GetMarkets** | Get data about all markets | ✅ |
| **Market/GetTrades** | Get data about trades fitting certain criteria | ✅ |
| **Market/GetMarket** | Get data about a single market | ✅ |
| **Market/GetMarketHistory** | Get data about a single market's historical data | ✅ |
| **Market/GetMarketOrderBook** | Get a market's order book | ✅ |
| **Market/GetSeries** | Get data about a series | ✅ |

### Websocket Requests: ⌛

Websocket's are a work in progress

See documentation here
[Kalshi Trading API](https://trading-api.readme.io/reference/introduction).

#### Websocket Feature Table

| Command | Description | Status |
| ---------------------- | --------------------------------- | ------ |
| **Subscribe** | Subscribe to one or many channels | ✅ |
| **Unsubscribe** | Cancel one or more subscriptions | ✅ |
| **UpdateSubscription** | Updates an existing subscription | ✅ |

| Channels | Description | Status |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ |
| **Orderbook** | A complete view of the order book's aggregated price levels on a given market and all further updates to it. | ✅ |
| **Ticker** | The list price ticker for a given market. | ✅ |
| **Trade** | Update the client with the most recent trades that occur in the markets that the client is interested on. | ✅ |
| **Fill** | Update the client with the most recent fills, that means trades in that occur in the markets that the client is interested on. | ✅ |
| **Market Lifecycle** | Update the client with new market lifecycle events of the following types: open, pause, close, determination and settlement with corresponding details for each event. | ✅ |

## Troubleshooting & Undefined behavior

The Kalshi API is under-documented and rapidly changing

If you are using 2FA you may need to use an API key versus email/password

If you signed up with SSO (Apple or Google) on Kalshi and you need
email/password login, you can use the reset password flow with your SSO email to
get valid credentials for the API.
2 changes: 1 addition & 1 deletion cargo.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[workspace]
members = ["kalshi", "sample_bot"]
members = ["kalshi", "sample_bot"]
21 changes: 19 additions & 2 deletions kalshi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ repository = "https://github.com/dpeachpeach/kalshi-rust"
keywords = ["finance", "trading", "kalshi", "bots"]
readme = "README.md"

[features]
default = ["websockets"]
websockets = [
"dep:serde_json",
"dep:tokio-tungstenite",
"dep:futures-util",

]
tokio-stream = []

[lib]
# We would like to eventually turn this on, but the doctests require some clean-up.
# See https://github.com/dpeachpeach/kalshi-rust/issues/7
Expand All @@ -22,8 +32,15 @@ doctest = false
[dependencies]
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"]}
uuid = { version = "1.5.0", features = ["v4", "fast-rng"]}
serde = { version = "1.0", features = ["derive"] }
uuid = { version = "1.5.0", features = ["v4", "fast-rng"] }
serde_json = { version = "1.0.111", optional = true }
tokio-tungstenite = { version = "0.24.0", optional = true, features = [
"native-tls",
] }
futures-util = { version = "0.3.31", optional = true }
openssl = "0.10.68"
base64 = "0.22.1"

[dev-dependencies]
serde_json = "1.0.111"
125 changes: 122 additions & 3 deletions kalshi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,20 +115,32 @@
//! ```
//!

use std::{fmt::Debug, sync::Arc};

#[macro_use]
mod utils;
mod auth;
mod exchange;
mod kalshi_error;
mod market;
mod portfolio;
#[cfg(feature = "websockets")]
mod websockets;

pub use auth::*;
pub use exchange::*;
pub use kalshi_error::*;
pub use market::*;
use openssl::{
hash::MessageDigest,
pkey::{PKey, Private},
rsa::Padding,
sign::{RsaPssSaltlen, Signer},
};
pub use portfolio::*;

#[cfg(feature = "websockets")]
pub use websockets::*;

// imports
use reqwest;

Expand All @@ -146,16 +158,69 @@ use reqwest;
/// ```
///
///
#[derive(Debug, Clone)]
#[derive(Clone)]
pub struct Kalshi {
/// - `base_url`: The base URL for the API, determined by the trading environment.
base_url: String,
#[cfg(feature = "websockets")]
ws_url: String,
/// - `curr_token`: A field for storing the current authentication token.
curr_token: Option<String>,
/// - `member_id`: A field for storing the member ID.
member_id: Option<String>,
/// - `client`: The HTTP client used for making requests to the marketplace.
client: reqwest::Client,
/// - `auth`: Stores the method of authentication to use and any required inputs (key for example)
auth: KalshiAuth,
}

pub enum KalshiAuth {
#[deprecated(
note = "Kalshi seems to be moving to ApiKey, you might also have problems if you are using 2FA"
)]
EmailPassword,
ApiKey {
/// - `key_id`: UUID of the key, get from profile page
key_id: String,
/// - `key`: PEM formatted RSA private key, generate this on profile page
key: String,
/// - `p_key`: The private key loaded
p_key: Arc<PKey<Private>>,
/// - `signer`: If using apiKey auth, stores the RSA signer for the passed key
signer: Signer<'static>,
},
}

impl Clone for KalshiAuth {
fn clone(&self) -> Self {
match &self {
KalshiAuth::ApiKey { key_id, key, .. } => {
KalshiAuth::build_api_key(key_id.clone(), key.clone())
}
KalshiAuth::EmailPassword => KalshiAuth::EmailPassword,
}
}
}

impl KalshiAuth {
fn build_api_key(key_id: String, key: String) -> Self {
let p_key = PKey::private_key_from_pem(key.as_bytes())
.expect("Unable to load private key from pem string provided");
let mut signer = Signer::new(MessageDigest::sha256(), &p_key)
.expect("Unable to load signer from private key");
signer
.set_rsa_padding(Padding::PKCS1_PSS)
.expect("Unable to set rsa padding on signer");
signer
.set_rsa_pss_saltlen(RsaPssSaltlen::DIGEST_LENGTH)
.expect("Unable to set rsa pss salt length for signer");
KalshiAuth::ApiKey {
key_id,
key,
p_key: Arc::new(p_key),
signer,
}
}
}

impl Kalshi {
Expand All @@ -180,12 +245,52 @@ impl Kalshi {
/// let kalshi = Kalshi::new(TradingEnvironment::LiveMarketMode);
/// ```
///
pub fn new(trading_env: TradingEnvironment) -> Kalshi {
pub fn new(trading_env: TradingEnvironment) -> Self {
return Kalshi {
base_url: utils::build_base_url(trading_env).to_string(),
#[cfg(feature = "websockets")]
ws_url: utils::build_ws_url(trading_env).to_string(),
curr_token: None,
member_id: None,
client: reqwest::Client::new(),
auth: KalshiAuth::EmailPassword,
};
}

/// Creates a new instance of Kalshi with the specified trading environment.
/// Use the passed api key for authenticating rest calls (TODO) and websockets
/// This environment determines the base URL used for API requests.
///
/// # Arguments
///
/// * `trading_env` - The trading environment to be used (LiveMarketMode: Trading with real money. DemoMode: Paper Trading).
/// * `key_id` - ID of the api key you get from account profile page
/// * `key` - PEM formatted RSA private key you get from account profile page
///
/// # Example
///
/// ## Creating a Demo instance.
/// ```
/// use kalshi::{Kalshi, TradingEnvironment};
/// let kalshi = Kalshi::new_with_api_key(TradingEnvironment::DemoMode, KalshiAuth::EmailPassword);
/// ```
///
/// ## Creating a Live Trading instance (Warning, you're using real money!)
/// ```
/// use kalshi::{Kalshi, TradingEnvironment};
/// let kalshi = Kalshi::new_with_api_key(TradingEnvironment::LiveMarketMode, key_id: "f2f80-...".to_string() key: "-----BEGIN RSA PRIVATE KEY----- ...".to_string());
/// ```
///
pub fn new_with_api_key(trading_env: TradingEnvironment, key_id: String, key: String) -> Self {
// Initialize signer if api key is passed
return Kalshi {
base_url: utils::build_base_url(trading_env).to_string(),
#[cfg(feature = "websockets")]
ws_url: utils::build_ws_url(trading_env).to_string(),
curr_token: None,
member_id: None,
client: reqwest::Client::new(),
auth: KalshiAuth::build_api_key(key_id, key),
};
}

Expand Down Expand Up @@ -215,6 +320,16 @@ impl Kalshi {
_ => return None,
}
}

/// Retrieves the currently set base url
///
/// # Returns
///
/// Returns a &str of the current base url
///
pub fn get_base_url(&self) -> &str {
&self.base_url
}
}

// GENERAL ENUMS
Expand All @@ -225,6 +340,7 @@ impl Kalshi {
/// This enum is used to specify whether the interaction with the Kalshi API should be in a demo (simulated) environment
/// or in the live market with real financial transactions.
///
#[derive(Clone, Copy, Debug)]
pub enum TradingEnvironment {
/// The demo mode represents a simulated environment where trades do not involve real money.
/// This mode is typically used for testing and practice purposes.
Expand All @@ -233,4 +349,7 @@ pub enum TradingEnvironment {
/// The live market mode is the real trading environment where all transactions involve actual financial stakes.
/// Use this mode for actual trading activities with real money.
LiveMarketMode,

// Legacy only markets
LegacyLiveMarketMode,
}
4 changes: 2 additions & 2 deletions kalshi/src/portfolio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use uuid::Uuid;

use serde::{Deserialize, Deserializer, Serialize};

impl<'a> Kalshi {
impl Kalshi {
/// Retrieves the current balance of the authenticated user from the Kalshi exchange.
///
/// This method fetches the user's balance, requiring a valid authentication token.
Expand Down Expand Up @@ -555,7 +555,7 @@ impl<'a> Kalshi {
/// ).await.unwrap();
/// ```
///

// todo: rewrite using generics
pub async fn create_order(
&self,
Expand Down
Loading