From bf8f3668a4a31bdfa0803075e353d9ef9b2f86c6 Mon Sep 17 00:00:00 2001 From: qtzx06 Date: Fri, 20 Mar 2026 11:57:13 -0700 Subject: [PATCH 1/4] feat: expose volume, liquidity, date, and tag filters on markets/events list the SDK supports these query params but the CLI wasn't wiring them through, forcing users to over-fetch and filter client-side. closes #38 --- src/commands/events.rs | 50 ++++++++++++++++++++++++++++++++++++ src/commands/markets.rs | 56 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/src/commands/events.rs b/src/commands/events.rs index d001210..040ca76 100644 --- a/src/commands/events.rs +++ b/src/commands/events.rs @@ -1,9 +1,11 @@ use anyhow::Result; +use chrono::{DateTime, Utc}; use clap::{Args, Subcommand}; use polymarket_client_sdk::gamma::{ self, types::request::{EventByIdRequest, EventBySlugRequest, EventTagsRequest, EventsRequest}, }; +use rust_decimal::Decimal; use super::is_numeric_id; use crate::output::OutputFormat; @@ -47,6 +49,38 @@ pub enum EventsCommand { /// Filter by tag slug (e.g. "politics", "crypto") #[arg(long)] tag: Option, + + /// Minimum trading volume (e.g. 1000000) + #[arg(long)] + volume_min: Option, + + /// Maximum trading volume + #[arg(long)] + volume_max: Option, + + /// Minimum liquidity + #[arg(long)] + liquidity_min: Option, + + /// Maximum liquidity + #[arg(long)] + liquidity_max: Option, + + /// Only events starting after this date (e.g. 2026-03-01T00:00:00Z) + #[arg(long)] + start_date_min: Option>, + + /// Only events starting before this date + #[arg(long)] + start_date_max: Option>, + + /// Only events ending after this date + #[arg(long)] + end_date_min: Option>, + + /// Only events ending before this date + #[arg(long)] + end_date_max: Option>, }, /// Get a single event by ID or slug @@ -72,6 +106,14 @@ pub async fn execute(client: &gamma::Client, args: EventsArgs, output: OutputFor order, ascending, tag, + volume_min, + volume_max, + liquidity_min, + liquidity_max, + start_date_min, + start_date_max, + end_date_min, + end_date_max, } => { let resolved_closed = closed.or_else(|| active.map(|a| !a)); @@ -83,6 +125,14 @@ pub async fn execute(client: &gamma::Client, args: EventsArgs, output: OutputFor .maybe_tag_slug(tag) // EventsRequest::order is Vec; into_iter on Option yields 0 or 1 items. .order(order.into_iter().collect()) + .maybe_volume_min(volume_min) + .maybe_volume_max(volume_max) + .maybe_liquidity_min(liquidity_min) + .maybe_liquidity_max(liquidity_max) + .maybe_start_date_min(start_date_min) + .maybe_start_date_max(start_date_max) + .maybe_end_date_min(end_date_min) + .maybe_end_date_max(end_date_max) .build(); let events = client.events(&request).await?; diff --git a/src/commands/markets.rs b/src/commands/markets.rs index 2544d18..67f6e63 100644 --- a/src/commands/markets.rs +++ b/src/commands/markets.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use chrono::{DateTime, Utc}; use clap::{Args, Subcommand}; use polymarket_client_sdk::gamma::{ self, @@ -10,6 +11,7 @@ use polymarket_client_sdk::gamma::{ response::Market, }, }; +use rust_decimal::Decimal; use super::is_numeric_id; use crate::output::OutputFormat; @@ -49,6 +51,42 @@ pub enum MarketsCommand { /// Sort ascending instead of descending #[arg(long)] ascending: bool, + + /// Minimum trading volume (e.g. 1000000) + #[arg(long)] + volume_min: Option, + + /// Maximum trading volume + #[arg(long)] + volume_max: Option, + + /// Minimum liquidity + #[arg(long)] + liquidity_min: Option, + + /// Maximum liquidity + #[arg(long)] + liquidity_max: Option, + + /// Only markets starting after this date (e.g. 2026-03-01T00:00:00Z) + #[arg(long)] + start_date_min: Option>, + + /// Only markets starting before this date + #[arg(long)] + start_date_max: Option>, + + /// Only markets ending after this date + #[arg(long)] + end_date_min: Option>, + + /// Only markets ending before this date + #[arg(long)] + end_date_max: Option>, + + /// Filter by tag ID + #[arg(long)] + tag: Option, }, /// Get a single market by ID or slug @@ -87,6 +125,15 @@ pub async fn execute( offset, order, ascending, + volume_min, + volume_max, + liquidity_min, + liquidity_max, + start_date_min, + start_date_max, + end_date_min, + end_date_max, + tag, } => { let resolved_closed = closed.or_else(|| active.map(|a| !a)); @@ -96,6 +143,15 @@ pub async fn execute( .maybe_offset(offset) .maybe_order(order) .ascending(ascending) + .maybe_volume_num_min(volume_min) + .maybe_volume_num_max(volume_max) + .maybe_liquidity_num_min(liquidity_min) + .maybe_liquidity_num_max(liquidity_max) + .maybe_start_date_min(start_date_min) + .maybe_start_date_max(start_date_max) + .maybe_end_date_min(end_date_min) + .maybe_end_date_max(end_date_max) + .maybe_tag_id(tag) .build(); let markets = client.markets(&request).await?; From 71fede12e2bd1a82db93d352cedaab18630b40ae Mon Sep 17 00:00:00 2001 From: qtzx06 Date: Fri, 20 Mar 2026 12:12:04 -0700 Subject: [PATCH 2/4] feat: add global --fields flag to filter JSON output agents and scripts consuming CLI output pay per token. a market object has 80+ fields but a typical query only needs 2-3 of them. --fields lets callers specify exactly which keys to keep: polymarket markets list -o json --fields question,volumeNum,slug this filters at the output layer so it works with every command that produces JSON, without touching any individual command logic. --- src/main.rs | 10 ++++++- src/output/mod.rs | 66 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 2abb55f..a58b419 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,6 +26,10 @@ pub(crate) struct Cli { /// Signature type: eoa, proxy, or gnosis-safe #[arg(long, global = true)] signature_type: Option, + + /// Comma-separated list of fields to include in JSON output (e.g. question,volume_num,slug) + #[arg(long, global = true, value_delimiter = ',')] + fields: Option>, } #[derive(Subcommand)] @@ -68,9 +72,13 @@ enum Commands { #[tokio::main] async fn main() -> ExitCode { - let cli = Cli::parse(); + let mut cli = Cli::parse(); let output = cli.output; + if let Some(fields) = cli.fields.take() { + output::set_json_fields(fields); + } + if let Err(e) = run(cli).await { output::print_error(&e, output); return ExitCode::FAILURE; diff --git a/src/output/mod.rs b/src/output/mod.rs index 05de836..d88634a 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -11,13 +11,23 @@ pub(crate) mod series; pub(crate) mod sports; pub(crate) mod tags; +use std::sync::OnceLock; + use chrono::{DateTime, Utc}; use polymarket_client_sdk::types::Decimal; use rust_decimal::prelude::ToPrimitive; +use serde_json::Value; use tabled::Table; use tabled::settings::object::Columns; use tabled::settings::{Modify, Style, Width}; +/// field names to keep in JSON output; set once at startup via --fields +static JSON_FIELDS: OnceLock> = OnceLock::new(); + +pub(crate) fn set_json_fields(fields: Vec) { + let _ = JSON_FIELDS.set(fields); +} + pub(crate) const DASH: &str = "—"; #[derive(Clone, Copy, Debug, clap::ValueEnum)] @@ -63,10 +73,32 @@ pub(crate) fn active_status(closed: Option, active: Option) -> &'sta } pub(crate) fn print_json(data: &(impl serde::Serialize + ?Sized)) -> anyhow::Result<()> { - println!("{}", serde_json::to_string_pretty(data)?); + let value = serde_json::to_value(data)?; + let output = match JSON_FIELDS.get() { + Some(fields) => filter_fields(value, fields), + None => value, + }; + println!("{}", serde_json::to_string_pretty(&output)?); Ok(()) } +/// keep only the requested keys from an object or each object in an array +fn filter_fields(value: Value, fields: &[String]) -> Value { + match value { + Value::Array(arr) => { + Value::Array(arr.into_iter().map(|v| filter_fields(v, fields)).collect()) + } + Value::Object(map) => { + let filtered = map + .into_iter() + .filter(|(k, _)| fields.iter().any(|f| f == k)) + .collect(); + Value::Object(filtered) + } + other => other, + } +} + pub(crate) fn print_error(error: &anyhow::Error, format: OutputFormat) { match format { OutputFormat::Json => { @@ -185,4 +217,36 @@ mod tests { fn format_decimal_just_below_million_uses_k() { assert_eq!(format_decimal(dec!(999_999)), "$1000.0K"); } + + #[test] + fn filter_fields_keeps_only_requested_keys() { + let obj = serde_json::json!({"a": 1, "b": 2, "c": 3}); + let fields = vec!["a".into(), "c".into()]; + let result = filter_fields(obj, &fields); + assert_eq!(result, serde_json::json!({"a": 1, "c": 3})); + } + + #[test] + fn filter_fields_applies_to_each_array_element() { + let arr = serde_json::json!([{"a": 1, "b": 2}, {"a": 3, "b": 4}]); + let fields = vec!["a".into()]; + let result = filter_fields(arr, &fields); + assert_eq!(result, serde_json::json!([{"a": 1}, {"a": 3}])); + } + + #[test] + fn filter_fields_returns_empty_object_when_no_match() { + let obj = serde_json::json!({"a": 1, "b": 2}); + let fields = vec!["z".into()]; + let result = filter_fields(obj, &fields); + assert_eq!(result, serde_json::json!({})); + } + + #[test] + fn filter_fields_passes_through_non_object_values() { + let val = serde_json::json!(42); + let fields = vec!["a".into()]; + let result = filter_fields(val, &fields); + assert_eq!(result, serde_json::json!(42)); + } } From 1aabdc659dd91d07970bc9c1146ff20b42d7c4e9 Mon Sep 17 00:00:00 2001 From: qtzx06 Date: Fri, 20 Mar 2026 13:59:12 -0700 Subject: [PATCH 3/4] fix: address review feedback on --fields - swap OnceLock for RwLock so --fields can change between interactive shell commands instead of being stuck on first value - move field extraction into run() so the shell path also picks it up (previously only main() extracted fields) - only route through serde_json::to_value when --fields is active, preserving original key order for unfiltered output --- src/main.rs | 9 ++++----- src/output/mod.rs | 29 ++++++++++++++++++----------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/main.rs b/src/main.rs index a58b419..6ec0c67 100644 --- a/src/main.rs +++ b/src/main.rs @@ -72,13 +72,9 @@ enum Commands { #[tokio::main] async fn main() -> ExitCode { - let mut cli = Cli::parse(); + let cli = Cli::parse(); let output = cli.output; - if let Some(fields) = cli.fields.take() { - output::set_json_fields(fields); - } - if let Err(e) = run(cli).await { output::print_error(&e, output); return ExitCode::FAILURE; @@ -89,6 +85,9 @@ async fn main() -> ExitCode { #[allow(clippy::too_many_lines)] pub(crate) async fn run(cli: Cli) -> anyhow::Result<()> { + // set (or clear) the JSON field filter for this invocation + output::set_json_fields(cli.fields); + // Lazy-init so we only pay for the client we actually use. let gamma = std::cell::LazyCell::new(polymarket_client_sdk::gamma::Client::default); let data = std::cell::LazyCell::new(polymarket_client_sdk::data::Client::default); diff --git a/src/output/mod.rs b/src/output/mod.rs index d88634a..fbb7f6e 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -11,7 +11,7 @@ pub(crate) mod series; pub(crate) mod sports; pub(crate) mod tags; -use std::sync::OnceLock; +use std::sync::RwLock; use chrono::{DateTime, Utc}; use polymarket_client_sdk::types::Decimal; @@ -21,11 +21,13 @@ use tabled::Table; use tabled::settings::object::Columns; use tabled::settings::{Modify, Style, Width}; -/// field names to keep in JSON output; set once at startup via --fields -static JSON_FIELDS: OnceLock> = OnceLock::new(); +/// field names to keep in JSON output; updated per invocation via --fields +static JSON_FIELDS: RwLock>> = RwLock::new(None); -pub(crate) fn set_json_fields(fields: Vec) { - let _ = JSON_FIELDS.set(fields); +pub(crate) fn set_json_fields(fields: Option>) { + if let Ok(mut guard) = JSON_FIELDS.write() { + *guard = fields; + } } pub(crate) const DASH: &str = "—"; @@ -73,12 +75,17 @@ pub(crate) fn active_status(closed: Option, active: Option) -> &'sta } pub(crate) fn print_json(data: &(impl serde::Serialize + ?Sized)) -> anyhow::Result<()> { - let value = serde_json::to_value(data)?; - let output = match JSON_FIELDS.get() { - Some(fields) => filter_fields(value, fields), - None => value, - }; - println!("{}", serde_json::to_string_pretty(&output)?); + let fields = JSON_FIELDS.read().ok(); + let active = fields.as_ref().and_then(|g| g.as_ref()); + + if let Some(fields) = active { + // only go through to_value when filtering, to preserve key order otherwise + let value = serde_json::to_value(data)?; + let filtered = filter_fields(value, fields); + println!("{}", serde_json::to_string_pretty(&filtered)?); + } else { + println!("{}", serde_json::to_string_pretty(data)?); + } Ok(()) } From 8ecbdcf7a1e0c1c106f65616ec85f95a2100838a Mon Sep 17 00:00:00 2001 From: qtzx06 Date: Fri, 20 Mar 2026 14:07:39 -0700 Subject: [PATCH 4/4] cleanup: simplify RwLock read in print_json --- src/output/mod.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/output/mod.rs b/src/output/mod.rs index fbb7f6e..75d55ce 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -75,14 +75,14 @@ pub(crate) fn active_status(closed: Option, active: Option) -> &'sta } pub(crate) fn print_json(data: &(impl serde::Serialize + ?Sized)) -> anyhow::Result<()> { - let fields = JSON_FIELDS.read().ok(); - let active = fields.as_ref().and_then(|g| g.as_ref()); - - if let Some(fields) = active { - // only go through to_value when filtering, to preserve key order otherwise + let guard = JSON_FIELDS.read().unwrap(); + if let Some(fields) = guard.as_deref() { + // only convert to Value when filtering — preserves key order in the common case let value = serde_json::to_value(data)?; - let filtered = filter_fields(value, fields); - println!("{}", serde_json::to_string_pretty(&filtered)?); + println!( + "{}", + serde_json::to_string_pretty(&filter_fields(value, fields))? + ); } else { println!("{}", serde_json::to_string_pretty(data)?); }