polymarket_api/
display.rs

1use {
2    crate::{
3        gamma::MarketInfo,
4        rtds::RTDSMessage,
5        websocket::{OrderUpdate, OrderbookUpdate, PriceUpdate, TradeUpdate, WebSocketMessage},
6    },
7    chrono::DateTime,
8    colored::*,
9};
10
11pub struct MarketUpdateFormatter;
12
13pub struct RTDSFormatter;
14
15impl MarketUpdateFormatter {
16    pub fn format_message(msg: &WebSocketMessage, market_info: Option<&MarketInfo>) -> String {
17        match msg {
18            WebSocketMessage::Orderbook(update) => Self::format_orderbook(update, market_info),
19            WebSocketMessage::Trade(update) => Self::format_trade(update, market_info),
20            WebSocketMessage::Order(update) => Self::format_order(update, market_info),
21            WebSocketMessage::Price(update) => Self::format_price(update, market_info),
22            WebSocketMessage::Error(err) => {
23                format!("{} {}", "ERROR".red().bold(), err.error.red())
24            },
25            WebSocketMessage::Subscribed(sub) => {
26                format!("{} {}", "✓ SUBSCRIBED".green().bold(), sub.message.green())
27            },
28            WebSocketMessage::Unknown => "Unknown message type".yellow().to_string(),
29        }
30    }
31
32    fn format_orderbook(update: &OrderbookUpdate, market_info: Option<&MarketInfo>) -> String {
33        let title = market_info
34            .map(|m| format!("{} - {}", m.event_title.bold(), m.market_question))
35            .unwrap_or_else(|| "Unknown Market".to_string());
36
37        let timestamp = update
38            .timestamp
39            .and_then(|ts| DateTime::from_timestamp(ts / 1000, 0))
40            .map(|dt| dt.format("%H:%M:%S").to_string())
41            .unwrap_or_else(|| "now".to_string());
42
43        let mut output = format!(
44            "\n{} {} {}\n",
45            "📊 ORDERBOOK".cyan().bold(),
46            timestamp.dimmed(),
47            title
48        );
49
50        // Best bid/ask
51        if let Some(best_bid) = update.bids.first() {
52            output.push_str(&format!(
53                "  {} {} @ {}\n",
54                "BID".green().bold(),
55                best_bid.size.bright_green(),
56                best_bid.price.bright_green()
57            ));
58        }
59
60        if let Some(best_ask) = update.asks.first() {
61            output.push_str(&format!(
62                "  {} {} @ {}\n",
63                "ASK".red().bold(),
64                best_ask.size.bright_red(),
65                best_ask.price.bright_red()
66            ));
67        }
68
69        // Spread
70        if let (Some(bid), Some(ask)) = (update.bids.first(), update.asks.first())
71            && let (Ok(bid_price), Ok(ask_price)) =
72                (bid.price.parse::<f64>(), ask.price.parse::<f64>())
73        {
74            let spread = ask_price - bid_price;
75            let spread_pct = (spread / bid_price) * 100.0;
76            output.push_str(&format!(
77                "  {} {:.4} ({:.2}%)\n",
78                "SPREAD".yellow(),
79                spread,
80                spread_pct
81            ));
82        }
83
84        output
85    }
86
87    fn format_trade(update: &TradeUpdate, market_info: Option<&MarketInfo>) -> String {
88        let title = market_info
89            .map(|m| format!("{} - {}", m.event_title.bold(), m.market_question))
90            .unwrap_or_else(|| "Unknown Market".to_string());
91
92        let timestamp = update
93            .timestamp
94            .and_then(|ts| DateTime::from_timestamp(ts / 1000, 0))
95            .map(|dt| dt.format("%H:%M:%S").to_string())
96            .unwrap_or_else(|| "now".to_string());
97
98        let side_color = if update.side == "buy" {
99            "🟢 BUY".green().bold()
100        } else {
101            "🔴 SELL".red().bold()
102        };
103
104        format!(
105            "\n{} {} {} {} @ {} - {}\n",
106            "💸 TRADE".bright_yellow().bold(),
107            timestamp.dimmed(),
108            side_color,
109            update.size.bright_white(),
110            update.price.bright_white().bold(),
111            title
112        )
113    }
114
115    fn format_order(update: &OrderUpdate, market_info: Option<&MarketInfo>) -> String {
116        let title = market_info
117            .map(|m| format!("{} - {}", m.event_title.bold(), m.market_question))
118            .unwrap_or_else(|| "Unknown Market".to_string());
119
120        let timestamp = update
121            .timestamp
122            .and_then(|ts| DateTime::from_timestamp(ts / 1000, 0))
123            .map(|dt| dt.format("%H:%M:%S").to_string())
124            .unwrap_or_else(|| "now".to_string());
125
126        let status_color = match update.status.as_str() {
127            "open" => "OPEN".green(),
128            "filled" => "FILLED".bright_green(),
129            "cancelled" => "CANCELLED".red(),
130            _ => update.status.as_str().yellow(),
131        };
132
133        format!(
134            "\n{} {} {} {} @ {} - {} - {}\n",
135            "📝 ORDER".blue().bold(),
136            timestamp.dimmed(),
137            status_color.bold(),
138            update.size,
139            update.price,
140            update.side.to_uppercase(),
141            title
142        )
143    }
144
145    fn format_price(update: &PriceUpdate, market_info: Option<&MarketInfo>) -> String {
146        let title = market_info
147            .map(|m| format!("{} - {}", m.event_title.bold(), m.market_question))
148            .unwrap_or_else(|| "Unknown Market".to_string());
149
150        let timestamp = update
151            .timestamp
152            .and_then(|ts| DateTime::from_timestamp(ts / 1000, 0))
153            .map(|dt| dt.format("%H:%M:%S").to_string())
154            .unwrap_or_else(|| "now".to_string());
155
156        format!(
157            "\n{} {} Price: {} - {}\n",
158            "💰 PRICE".magenta().bold(),
159            timestamp.dimmed(),
160            update.price.bright_white().bold(),
161            title
162        )
163    }
164}
165
166impl RTDSFormatter {
167    pub fn format_message(msg: &RTDSMessage) -> String {
168        let timestamp = DateTime::from_timestamp(msg.payload.timestamp, 0)
169            .map(|dt| dt.format("%H:%M:%S").to_string())
170            .unwrap_or_else(|| "now".to_string());
171
172        let side_color = if msg.payload.side == "BUY" {
173            "🟢 BUY".green().bold()
174        } else {
175            "🔴 SELL".red().bold()
176        };
177
178        let outcome_color = if msg.payload.outcome == "Yes" {
179            msg.payload.outcome.bright_green()
180        } else {
181            msg.payload.outcome.bright_red()
182        };
183
184        // Round shares to 2 decimal places
185        let rounded_shares = (msg.payload.size * 100.0).round() / 100.0;
186
187        // Calculate total value in dollars
188        let total_value = msg.payload.price * msg.payload.size;
189
190        format!(
191            "\n{} {} {} {} @ ${:.4} ({} shares, ${:.2}) - {} - {}\n  User: {} ({})\n",
192            "💸 TRADE".bright_yellow().bold(),
193            timestamp.dimmed(),
194            side_color,
195            outcome_color.bold(),
196            msg.payload.price,
197            rounded_shares,
198            total_value,
199            msg.payload.title.bold(),
200            msg.payload.event_slug.dimmed(),
201            msg.payload.name.bright_white(),
202            msg.payload.pseudonym.dimmed()
203        )
204    }
205}