Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,9 @@ polymarket markets list --limit 2
```

```
Question Price (Yes) Volume Liquidity Status
Will Trump win the 2024 election? 52.00¢ $145.2M $1.2M Active
Will BTC hit $100k by Dec 2024? 67.30¢ $89.4M $430.5K Active
Question Price Volume Liquidity Status
Will Trump win the 2024 election? Yes: 52.00¢ $145.2M $1.2M Active
Will BTC hit $100k by Dec 2024? Yes: 67.30¢ $89.4M $430.5K Active
```

```bash
Expand Down
26 changes: 22 additions & 4 deletions src/commands/comments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ pub struct CommentsArgs {

#[derive(Subcommand)]
pub enum CommentsCommand {
/// List comments on an event, market, or series
/// List comments on an event or series
List {
/// Parent entity type: event, market, or series
/// Parent entity type: event or series
#[arg(long)]
entity_type: EntityType,

Expand Down Expand Up @@ -78,15 +78,13 @@ pub enum CommentsCommand {
#[derive(Clone, Debug, clap::ValueEnum)]
pub enum EntityType {
Event,
Market,
Series,
}

impl From<EntityType> for ParentEntityType {
fn from(e: EntityType) -> Self {
match e {
EntityType::Event => ParentEntityType::Event,
EntityType::Market => ParentEntityType::Market,
EntityType::Series => ParentEntityType::Series,
}
}
Expand Down Expand Up @@ -164,3 +162,23 @@ pub async fn execute(

Ok(())
}

#[cfg(test)]
mod tests {
use super::EntityType;
use clap::ValueEnum;

#[test]
fn entity_type_does_not_expose_market_variant() {
let names: Vec<String> = EntityType::value_variants()
.iter()
.filter_map(|variant| {
variant
.to_possible_value()
.map(|value| value.get_name().to_string())
})
.collect();

assert!(!names.iter().any(|name| name == "market"));
}
}
66 changes: 61 additions & 5 deletions src/commands/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ use anyhow::Result;
use clap::{Args, Subcommand};
use polymarket_client_sdk::gamma::{
self,
types::request::{EventByIdRequest, EventBySlugRequest, EventTagsRequest, EventsRequest},
types::{
request::{EventByIdRequest, EventBySlugRequest, EventTagsRequest, EventsRequest},
response::Event,
},
};

use super::is_numeric_id;
Expand Down Expand Up @@ -62,6 +65,23 @@ pub enum EventsCommand {
},
}

fn apply_status_filters(
events: Vec<Event>,
active_filter: Option<bool>,
closed_filter: Option<bool>,
) -> Vec<Event> {
events
.into_iter()
.filter(|event| {
flag_matches(event.active, active_filter) && flag_matches(event.closed, closed_filter)
})
.collect()
}

fn flag_matches(value: Option<bool>, filter: Option<bool>) -> bool {
filter.is_none_or(|expected| value == Some(expected))
}

pub async fn execute(client: &gamma::Client, args: EventsArgs, output: OutputFormat) -> Result<()> {
match args.command {
EventsCommand::List {
Expand All @@ -73,18 +93,17 @@ pub async fn execute(client: &gamma::Client, args: EventsArgs, output: OutputFor
ascending,
tag,
} => {
let resolved_closed = closed.or_else(|| active.map(|a| !a));

let request = EventsRequest::builder()
.limit(limit)
.maybe_closed(resolved_closed)
.maybe_active(active)
.maybe_closed(closed)
.maybe_offset(offset)
.maybe_ascending(if ascending { Some(true) } else { None })
.maybe_tag_slug(tag)
.order(order.into_iter().collect::<Vec<_>>())
.build();

let events = client.events(&request).await?;
let events = apply_status_filters(client.events(&request).await?, active, closed);

match output {
OutputFormat::Table => print_events_table(&events),
Expand Down Expand Up @@ -121,3 +140,40 @@ pub async fn execute(client: &gamma::Client, args: EventsArgs, output: OutputFor

Ok(())
}

#[cfg(test)]
mod tests {
use super::apply_status_filters;
use polymarket_client_sdk::gamma::types::response::Event;
use serde_json::json;

fn make_event(value: serde_json::Value) -> Event {
serde_json::from_value(value).unwrap()
}

#[test]
fn status_filters_are_independent() {
let events = vec![
make_event(json!({"id":"1", "active": true, "closed": true})),
make_event(json!({"id":"2", "active": false, "closed": true})),
make_event(json!({"id":"3", "active": false, "closed": false})),
];

let filtered = apply_status_filters(events, Some(false), Some(true));

assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].id, "2");
}

#[test]
fn active_filter_does_not_imply_closed_filter() {
let events = vec![
make_event(json!({"id":"1", "active": false, "closed": true})),
make_event(json!({"id":"2", "active": false, "closed": false})),
];

let filtered = apply_status_filters(events, Some(false), None);

assert_eq!(filtered.len(), 2);
}
}
60 changes: 56 additions & 4 deletions src/commands/markets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,23 @@ pub enum MarketsCommand {
},
}

fn apply_status_filters(
markets: Vec<Market>,
active_filter: Option<bool>,
closed_filter: Option<bool>,
) -> Vec<Market> {
markets
.into_iter()
.filter(|market| {
flag_matches(market.active, active_filter) && flag_matches(market.closed, closed_filter)
})
.collect()
}

fn flag_matches(value: Option<bool>, filter: Option<bool>) -> bool {
filter.is_none_or(|expected| value == Some(expected))
}
Comment thread
cursor[bot] marked this conversation as resolved.

pub async fn execute(
client: &gamma::Client,
args: MarketsArgs,
Expand All @@ -88,17 +105,15 @@ pub async fn execute(
order,
ascending,
} => {
let resolved_closed = closed.or_else(|| active.map(|a| !a));

let request = MarketsRequest::builder()
.limit(limit)
.maybe_closed(resolved_closed)
.maybe_closed(closed)
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
.maybe_offset(offset)
.maybe_order(order)
.maybe_ascending(if ascending { Some(true) } else { None })
.build();

let markets = client.markets(&request).await?;
let markets = apply_status_filters(client.markets(&request).await?, active, closed);

match output {
OutputFormat::Table => print_markets_table(&markets),
Expand Down Expand Up @@ -156,3 +171,40 @@ pub async fn execute(

Ok(())
}

#[cfg(test)]
mod tests {
use super::apply_status_filters;
use polymarket_client_sdk::gamma::types::response::Market;
use serde_json::json;

fn make_market(value: serde_json::Value) -> Market {
serde_json::from_value(value).unwrap()
}

#[test]
fn status_filters_are_independent() {
let markets = vec![
make_market(json!({"id":"1", "active": true, "closed": true})),
make_market(json!({"id":"2", "active": false, "closed": true})),
make_market(json!({"id":"3", "active": false, "closed": false})),
];

let filtered = apply_status_filters(markets, Some(false), Some(true));

assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].id, "2");
}

#[test]
fn active_filter_does_not_imply_closed_filter() {
let markets = vec![
make_market(json!({"id":"1", "active": false, "closed": true})),
make_market(json!({"id":"2", "active": false, "closed": false})),
];

let filtered = apply_status_filters(markets, Some(false), None);

assert_eq!(filtered.len(), 2);
}
}
46 changes: 39 additions & 7 deletions src/output/markets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use super::{detail_field, format_decimal, print_detail_table, truncate};
struct MarketRow {
#[tabled(rename = "Question")]
question: String,
#[tabled(rename = "Price (Yes)")]
#[tabled(rename = "Price")]
price_yes: String,
#[tabled(rename = "Volume")]
volume: String,
Expand All @@ -31,11 +31,10 @@ fn market_status(m: &Market) -> &'static str {

fn market_to_row(m: &Market) -> MarketRow {
let question = m.question.as_deref().unwrap_or("—");
let price_yes = m
.outcome_prices
.as_ref()
.and_then(|p| p.first())
.map_or_else(|| "—".into(), |p| format!("{:.2}¢", p * Decimal::from(100)));
let price_yes = primary_outcome_price(m).map_or_else(
|| "—".into(),
|(outcome, price)| format!("{outcome}: {:.2}¢", price * Decimal::from(100)),
);

MarketRow {
question: truncate(question, 60),
Expand All @@ -46,6 +45,18 @@ fn market_to_row(m: &Market) -> MarketRow {
}
}

fn primary_outcome_price(m: &Market) -> Option<(String, Decimal)> {
let outcomes = m.outcomes.as_ref()?;
let prices = m.outcome_prices.as_ref()?;

outcomes
.iter()
.zip(prices.iter())
.find(|(outcome, _)| outcome.eq_ignore_ascii_case("yes"))
.or_else(|| outcomes.iter().zip(prices.iter()).next())
.map(|(outcome, price)| (outcome.clone(), *price))
}
Comment thread
cursor[bot] marked this conversation as resolved.

pub fn print_markets_table(markets: &[Market]) {
if markets.is_empty() {
println!("No markets found.");
Expand Down Expand Up @@ -208,9 +219,30 @@ mod tests {
fn row_formats_price_as_cents() {
let m = make_market(json!({
"id": "1",
"outcomes": "[\"Yes\",\"No\"]",
"outcomePrices": "[\"0.65\",\"0.35\"]"
}));
assert_eq!(market_to_row(&m).price_yes, "65.00¢");
assert_eq!(market_to_row(&m).price_yes, "Yes: 65.00¢");
}

#[test]
fn row_prefers_yes_outcome_when_not_first() {
let m = make_market(json!({
"id": "1",
"outcomes": "[\"No\",\"Yes\"]",
"outcomePrices": "[\"0.35\",\"0.65\"]"
}));
assert_eq!(market_to_row(&m).price_yes, "Yes: 65.00¢");
}

#[test]
fn row_uses_first_outcome_for_non_binary_market() {
let m = make_market(json!({
"id": "1",
"outcomes": "[\"Long\",\"Short\"]",
"outcomePrices": "[\"0.58\",\"0.42\"]"
}));
assert_eq!(market_to_row(&m).price_yes, "Long: 58.00¢");
}

#[test]
Expand Down