wss_user/
wss_user.rs

1use polysqueeze::{
2    OrderArgs,
3    client::ClobClient,
4    errors::{PolyError, Result},
5    types::{GammaListParams, OrderType, Side},
6    wss::{WssUserClient, WssUserEvent},
7};
8use rust_decimal::{Decimal, prelude::FromPrimitive};
9use std::{env, str::FromStr};
10
11/// Fail fast when an expected environment variable is missing.
12fn env_var(key: &str) -> String {
13    env::var(key).expect(&format!("{} must be set for the user-channel example", key))
14}
15
16#[tokio::main]
17async fn main() -> Result<()> {
18    let base_url =
19        env::var("POLY_API_URL").unwrap_or_else(|_| "https://clob.polymarket.com".into());
20    let private_key = env_var("POLY_PRIVATE_KEY");
21    let chain_id = env::var("POLY_CHAIN_ID")
22        .ok()
23        .and_then(|value| value.parse::<u64>().ok())
24        .unwrap_or(137);
25
26    let l1_client = ClobClient::with_l1_headers(&base_url, &private_key, chain_id);
27    let creds = l1_client.create_or_derive_api_key(None).await?;
28    let mut l2_client =
29        ClobClient::with_l2_headers(&base_url, &private_key, chain_id, creds.clone());
30
31    if let Ok(funder) = env::var("POLY_FUNDER") {
32        l2_client.set_funder(&funder)?;
33    }
34
35    let min_liquidity = env::var("POLY_WSS_MIN_LIQUIDITY")
36        .ok()
37        .and_then(|value| Decimal::from_str(&value).ok())
38        .unwrap_or_else(|| Decimal::from(1_000_000));
39
40    let gamma_params = GammaListParams {
41        limit: Some(5),
42        liquidity_num_min: Some(min_liquidity),
43        ..Default::default()
44    };
45    let markets_response = l2_client.get_markets(None, Some(&gamma_params)).await?;
46
47    let market_ids: Vec<String> = markets_response
48        .data
49        .iter()
50        .filter(|market| market.liquidity_num.unwrap_or(Decimal::ZERO) >= min_liquidity)
51        .map(|market| market.condition_id.clone())
52        .filter(|id| !id.is_empty())
53        .take(2)
54        .collect();
55
56    if market_ids.is_empty() {
57        return Err(PolyError::validation(
58            "Gamma did not return any markets with condition_ids",
59        ));
60    }
61
62    let primary_market = markets_response
63        .data
64        .iter()
65        .find(|market| {
66            !market.clob_token_ids.is_empty()
67                && market.liquidity_num.unwrap_or(Decimal::ZERO) >= min_liquidity
68        })
69        .ok_or_else(|| {
70            PolyError::validation("No Gamma markets returned a CLOB token id for trading")
71        })?;
72    let token_id = primary_market.clob_token_ids.first().unwrap().clone();
73
74    let book = l2_client.get_order_book(&token_id).await?;
75    let _best_ask = book.asks.first().ok_or_else(|| {
76        PolyError::validation("Order book has no asks; cannot derive a safe price")
77    })?;
78    let tick_size = primary_market.minimum_tick_size;
79    let max_bid_level = book
80        .bids
81        .iter()
82        .max_by(|a, b| {
83            a.size
84                .partial_cmp(&b.size)
85                .unwrap_or(std::cmp::Ordering::Equal)
86        })
87        .ok_or_else(|| PolyError::validation("Order book has no depth data"))?;
88    let far_price =
89        max_bid_level.price - tick_size * Decimal::from_u32(10).unwrap_or(Decimal::ZERO);
90    let order_price = if far_price > Decimal::ZERO {
91        far_price
92    } else {
93        max_bid_level.price - tick_size
94    };
95    let order_size = Decimal::new(5, 0); // 0.1 size
96    let order_args = OrderArgs::new(&token_id, order_price, order_size, Side::BUY);
97
98    let mut user_client = WssUserClient::new(creds.clone());
99    user_client.subscribe(market_ids.clone()).await?;
100
101    println!(
102        "Subscribed to user channel for markets {market_ids:?} (waiting for your cancel/update)..."
103    );
104    let signed_order = l2_client
105        .create_order(&order_args, None, None, None)
106        .await?;
107    let response = l2_client.post_order(signed_order, OrderType::GTC).await?;
108    let order_id = response
109        .get("orderID")
110        .and_then(|value| value.as_str())
111        .ok_or_else(|| PolyError::validation("Post order response missing orderID"))?
112        .to_string();
113    println!(
114        "Placed order on {} @ {}: {response:#}",
115        token_id, order_price
116    );
117    println!(
118        "Order id {} - cancel it via the `wss_cancel` example while this stream is running.",
119        order_id
120    );
121
122    loop {
123        match user_client.next_event().await {
124            Ok(WssUserEvent::Order(order)) => {
125                println!(
126                    "order {} {} matched={} price={} side={}",
127                    order.id,
128                    order.message_type,
129                    order.size_matched,
130                    order.price,
131                    order.side.as_str()
132                );
133                if order.message_type.eq_ignore_ascii_case("CANCELLATION") {
134                    println!("Order {} cancelled; exiting.", order.id);
135                    break;
136                }
137            }
138            Ok(WssUserEvent::Trade(trade)) => {
139                println!(
140                    "trade {} {} {}@{} status={}",
141                    trade.id,
142                    trade.side.as_str(),
143                    trade.size,
144                    trade.price,
145                    trade.status
146                );
147            }
148            Err(err) => {
149                eprintln!("user stream error: {}", err);
150                break;
151            }
152        }
153    }
154
155    Ok(())
156}