mirror of
https://github.com/floor-licker/polyfill-rs.git
synced 2026-08-19 15:38:09 +00:00
Feat: Enhance integration tests
This commit is contained in:
@@ -0,0 +1,314 @@
|
|||||||
|
//! Quick Demo for polyfill-rs
|
||||||
|
//!
|
||||||
|
//! This example demonstrates all available API endpoints in a simple, easy-to-run format.
|
||||||
|
//! It can be run without authentication credentials and will test all public endpoints.
|
||||||
|
|
||||||
|
use polyfill_rs::{ClobClient, Side, Result, PolyfillError};
|
||||||
|
use rust_decimal::Decimal;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use tokio::time::{sleep, Duration};
|
||||||
|
use tracing::{info, error, warn};
|
||||||
|
|
||||||
|
/// Quick demo that tests all available endpoints
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
// Initialize logging
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
info!("Polyfill-rs Quick Demo");
|
||||||
|
info!("======================");
|
||||||
|
|
||||||
|
// Create client
|
||||||
|
let client = ClobClient::new("https://clob.polymarket.com");
|
||||||
|
|
||||||
|
// Test 1: Basic connectivity
|
||||||
|
info!("\nTesting API Connectivity...");
|
||||||
|
match test_connectivity(&client).await {
|
||||||
|
Ok(_) => info!("API connectivity test passed"),
|
||||||
|
Err(e) => {
|
||||||
|
error!("API connectivity test failed: {}", e);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 2: Get a valid token ID from markets
|
||||||
|
info!("\nGetting Market Data...");
|
||||||
|
let token_id = match get_valid_token_id(&client).await {
|
||||||
|
Ok(id) => {
|
||||||
|
info!("Found valid token ID: {}", id);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to get valid token ID: {}", e);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test 3: Test all market data endpoints
|
||||||
|
info!("\nTesting Market Data Endpoints...");
|
||||||
|
test_market_data_endpoints(&client, &token_id).await?;
|
||||||
|
|
||||||
|
// Test 4: Test error handling
|
||||||
|
info!("\nTesting Error Handling...");
|
||||||
|
test_error_handling(&client).await?;
|
||||||
|
|
||||||
|
// Test 5: Performance test
|
||||||
|
info!("\nTesting Performance...");
|
||||||
|
test_performance(&client, &token_id).await?;
|
||||||
|
|
||||||
|
info!("\nAll tests completed successfully!");
|
||||||
|
info!("The polyfill-rs client is working correctly with the Polymarket API.");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test basic API connectivity
|
||||||
|
async fn test_connectivity(client: &ClobClient) -> Result<()> {
|
||||||
|
// Test /ok endpoint
|
||||||
|
let is_ok = client.get_ok().await;
|
||||||
|
if !is_ok {
|
||||||
|
return Err(PolyfillError::network("API not responding", std::io::Error::new(std::io::ErrorKind::Other, "API not responding")));
|
||||||
|
}
|
||||||
|
info!(" /ok endpoint responding");
|
||||||
|
|
||||||
|
// Test /time endpoint
|
||||||
|
let server_time = client.get_server_time().await?;
|
||||||
|
info!(" Server time: {}", server_time);
|
||||||
|
|
||||||
|
// Verify server time is reasonable (within last 24 hours)
|
||||||
|
let current_time = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_secs();
|
||||||
|
|
||||||
|
let time_diff = if server_time > current_time {
|
||||||
|
server_time - current_time
|
||||||
|
} else {
|
||||||
|
current_time - server_time
|
||||||
|
};
|
||||||
|
|
||||||
|
if time_diff > 86400 { // 24 hours
|
||||||
|
warn!(" Server time seems off (diff: {} seconds)", time_diff);
|
||||||
|
} else {
|
||||||
|
info!(" Server time is reasonable");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a valid token ID from the markets endpoint
|
||||||
|
async fn get_valid_token_id(client: &ClobClient) -> Result<String> {
|
||||||
|
let markets = client.get_sampling_markets(Some(10)).await?;
|
||||||
|
|
||||||
|
if markets.data.is_empty() {
|
||||||
|
return Err(PolyfillError::api(404, "No markets found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find a market with active tokens
|
||||||
|
for market in &markets.data {
|
||||||
|
if market.active && !market.closed {
|
||||||
|
for token in &market.tokens {
|
||||||
|
if !token.token_id.is_empty() {
|
||||||
|
info!(" Found active market: {}", market.question);
|
||||||
|
info!(" Market slug: {}", market.market_slug);
|
||||||
|
info!(" Token ID: {}", token.token_id);
|
||||||
|
info!(" Outcome: {}", token.outcome);
|
||||||
|
return Ok(token.token_id.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(PolyfillError::api(404, "No active markets with valid tokens found"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test all market data endpoints
|
||||||
|
async fn test_market_data_endpoints(client: &ClobClient, token_id: &str) -> Result<()> {
|
||||||
|
// Test order book
|
||||||
|
info!(" Testing order book endpoint...");
|
||||||
|
let order_book = client.get_order_book(token_id).await?;
|
||||||
|
info!(" Order book: {} bids, {} asks", order_book.bids.len(), order_book.asks.len());
|
||||||
|
|
||||||
|
// Test midpoint
|
||||||
|
info!(" Testing midpoint endpoint...");
|
||||||
|
let midpoint = client.get_midpoint(token_id).await?;
|
||||||
|
info!(" Midpoint: {}", midpoint.mid);
|
||||||
|
|
||||||
|
// Test spread
|
||||||
|
info!(" Testing spread endpoint...");
|
||||||
|
let spread = client.get_spread(token_id).await?;
|
||||||
|
info!(" Spread: {}", spread.spread);
|
||||||
|
|
||||||
|
// Test buy price
|
||||||
|
info!(" Testing buy price endpoint...");
|
||||||
|
let buy_price = client.get_price(token_id, Side::BUY).await?;
|
||||||
|
info!(" Buy price: {}", buy_price.price);
|
||||||
|
|
||||||
|
// Test sell price
|
||||||
|
info!(" Testing sell price endpoint...");
|
||||||
|
let sell_price = client.get_price(token_id, Side::SELL).await?;
|
||||||
|
info!(" Sell price: {}", sell_price.price);
|
||||||
|
|
||||||
|
// Test tick size
|
||||||
|
info!(" Testing tick size endpoint...");
|
||||||
|
let tick_size = client.get_tick_size(token_id).await?;
|
||||||
|
info!(" Tick size: {}", tick_size);
|
||||||
|
|
||||||
|
// Test neg risk
|
||||||
|
info!(" Testing neg risk endpoint...");
|
||||||
|
let neg_risk = client.get_neg_risk(token_id).await?;
|
||||||
|
info!(" Neg risk: {}", neg_risk);
|
||||||
|
|
||||||
|
// Validate data consistency
|
||||||
|
info!(" Validating data consistency...");
|
||||||
|
validate_market_data(&order_book, &midpoint, &spread, &buy_price, &sell_price)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate that market data is consistent
|
||||||
|
fn validate_market_data(
|
||||||
|
order_book: &polyfill_rs::client::OrderBookSummary,
|
||||||
|
midpoint: &polyfill_rs::client::MidpointResponse,
|
||||||
|
spread: &polyfill_rs::client::SpreadResponse,
|
||||||
|
buy_price: &polyfill_rs::client::PriceResponse,
|
||||||
|
sell_price: &polyfill_rs::client::PriceResponse,
|
||||||
|
) -> Result<()> {
|
||||||
|
// Check that we have some liquidity
|
||||||
|
if order_book.bids.is_empty() && order_book.asks.is_empty() {
|
||||||
|
warn!(" Order book is empty");
|
||||||
|
} else {
|
||||||
|
info!(" Order book has liquidity");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that prices are positive
|
||||||
|
if buy_price.price <= Decimal::ZERO {
|
||||||
|
warn!(" Buy price is not positive: {}", buy_price.price);
|
||||||
|
} else {
|
||||||
|
info!(" Buy price is positive");
|
||||||
|
}
|
||||||
|
|
||||||
|
if sell_price.price <= Decimal::ZERO {
|
||||||
|
warn!(" Sell price is not positive: {}", sell_price.price);
|
||||||
|
} else {
|
||||||
|
info!(" Sell price is positive");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that spread is reasonable
|
||||||
|
if spread.spread < Decimal::ZERO {
|
||||||
|
warn!(" Spread is negative: {}", spread.spread);
|
||||||
|
} else {
|
||||||
|
info!(" Spread is non-negative");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that midpoint is between buy and sell prices (if both exist)
|
||||||
|
if buy_price.price > Decimal::ZERO && sell_price.price > Decimal::ZERO {
|
||||||
|
if midpoint.mid < buy_price.price || midpoint.mid > sell_price.price {
|
||||||
|
warn!(" Midpoint {} is not between buy {} and sell {}",
|
||||||
|
midpoint.mid, buy_price.price, sell_price.price);
|
||||||
|
} else {
|
||||||
|
info!(" Midpoint is between buy and sell prices");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test error handling with invalid requests
|
||||||
|
async fn test_error_handling(client: &ClobClient) -> Result<()> {
|
||||||
|
// Test with invalid token ID
|
||||||
|
info!(" Testing invalid token ID...");
|
||||||
|
let result = client.get_order_book("invalid_token_12345").await;
|
||||||
|
match result {
|
||||||
|
Ok(_) => {
|
||||||
|
warn!(" Invalid token ID returned data instead of error");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
match e {
|
||||||
|
PolyfillError::Api { status, .. } => {
|
||||||
|
if status >= 400 {
|
||||||
|
info!(" Invalid token ID correctly returned error: {}", status);
|
||||||
|
} else {
|
||||||
|
warn!(" Unexpected status code for invalid token: {}", status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
info!(" Invalid token ID returned error: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with empty token ID
|
||||||
|
info!(" Testing empty token ID...");
|
||||||
|
let result = client.get_order_book("").await;
|
||||||
|
match result {
|
||||||
|
Ok(_) => {
|
||||||
|
warn!(" Empty token ID returned data instead of error");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
info!(" Empty token ID correctly returned error: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test performance characteristics
|
||||||
|
async fn test_performance(client: &ClobClient, token_id: &str) -> Result<()> {
|
||||||
|
let mut total_time = Duration::from_secs(0);
|
||||||
|
let mut success_count = 0;
|
||||||
|
let test_count = 5;
|
||||||
|
|
||||||
|
info!(" Running {} performance tests...", test_count);
|
||||||
|
|
||||||
|
for i in 1..=test_count {
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
|
||||||
|
// Test a mix of endpoints
|
||||||
|
let results = tokio::join!(
|
||||||
|
client.get_server_time(),
|
||||||
|
client.get_midpoint(token_id),
|
||||||
|
client.get_spread(token_id),
|
||||||
|
);
|
||||||
|
|
||||||
|
let duration = start.elapsed();
|
||||||
|
total_time += duration;
|
||||||
|
|
||||||
|
match results {
|
||||||
|
(Ok(_), Ok(_), Ok(_)) => {
|
||||||
|
success_count += 1;
|
||||||
|
info!(" Test {}: PASSED {:.2}ms", i, duration.as_secs_f64() * 1000.0);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
warn!(" Test {}: FAILED in {:.2}ms", i, duration.as_secs_f64() * 1000.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small delay between tests
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let avg_time = total_time / test_count as u32;
|
||||||
|
let success_rate = (success_count as f64 / test_count as f64) * 100.0;
|
||||||
|
|
||||||
|
info!(" Performance Summary:");
|
||||||
|
info!(" Success rate: {:.1}%", success_rate);
|
||||||
|
info!(" Average response time: {:.2}ms", avg_time.as_secs_f64() * 1000.0);
|
||||||
|
info!(" Total time: {:.2}s", total_time.as_secs_f64());
|
||||||
|
|
||||||
|
// Performance thresholds
|
||||||
|
if avg_time > Duration::from_secs(2) {
|
||||||
|
warn!(" Average response time is slow: {:.2}ms", avg_time.as_secs_f64() * 1000.0);
|
||||||
|
} else {
|
||||||
|
info!(" Response times are acceptable");
|
||||||
|
}
|
||||||
|
|
||||||
|
if success_rate < 80.0 {
|
||||||
|
warn!(" Success rate is low: {:.1}%", success_rate);
|
||||||
|
} else {
|
||||||
|
info!(" Success rate is good");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Executable
+55
@@ -0,0 +1,55 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Demo runner for polyfill-rs
|
||||||
|
# This script runs the quick demo to showcase all available endpoints
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Running polyfill-rs Quick Demo"
|
||||||
|
echo "================================="
|
||||||
|
|
||||||
|
# Check if we're in the right directory
|
||||||
|
if [ ! -f "Cargo.toml" ]; then
|
||||||
|
echo " Error: Please run this script from the project root directory"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if the demo example exists
|
||||||
|
if [ ! -f "examples/quick_demo.rs" ]; then
|
||||||
|
echo " Error: Quick demo example not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Configuration:"
|
||||||
|
echo " Host: https://clob.polymarket.com"
|
||||||
|
echo " Chain ID: 137 (Polygon)"
|
||||||
|
echo " Authentication: None required (public endpoints only)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Run the quick demo
|
||||||
|
echo "Running quick demo..."
|
||||||
|
echo "===================="
|
||||||
|
|
||||||
|
cargo run --example quick_demo
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo " Quick demo completed successfully!"
|
||||||
|
echo ""
|
||||||
|
echo "What was tested:"
|
||||||
|
echo " API connectivity (/ok, /time)"
|
||||||
|
echo " Market data retrieval (/sampling-markets)"
|
||||||
|
echo " Order book data (/book)"
|
||||||
|
echo " Price data (/midpoint, /spread, /price)"
|
||||||
|
echo " Market metadata (/tick-size, /neg-risk)"
|
||||||
|
echo " Error handling (invalid inputs)"
|
||||||
|
echo " Performance characteristics"
|
||||||
|
echo " Data consistency validation"
|
||||||
|
echo ""
|
||||||
|
echo "The polyfill-rs client is working correctly with the Polymarket API!"
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo " Quick demo failed!"
|
||||||
|
echo "Check the output above for error details."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
+12
-7
@@ -567,21 +567,26 @@ impl OrderBook {
|
|||||||
/// Get the total liquidity at a given price level
|
/// Get the total liquidity at a given price level
|
||||||
/// Tells you how much you can buy/sell at exactly this price
|
/// Tells you how much you can buy/sell at exactly this price
|
||||||
pub fn liquidity_at_price(&self, price: Decimal, side: Side) -> Decimal {
|
pub fn liquidity_at_price(&self, price: Decimal, side: Side) -> Decimal {
|
||||||
|
let price_u32 = decimal_to_price(price).unwrap_or(0);
|
||||||
match side {
|
match side {
|
||||||
Side::BUY => self.asks.get(&price).copied().unwrap_or_default(), // How much we can buy at this price
|
Side::BUY => Decimal::from(self.asks.get(&price_u32).copied().unwrap_or_default()), // How much we can buy at this price
|
||||||
Side::SELL => self.bids.get(&price).copied().unwrap_or_default(), // How much we can sell at this price
|
Side::SELL => Decimal::from(self.bids.get(&price_u32).copied().unwrap_or_default()), // How much we can sell at this price
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the total liquidity within a price range
|
/// Get the total liquidity within a price range
|
||||||
/// Useful for understanding how much depth exists in a certain price band
|
/// Useful for understanding how much depth exists in a certain price band
|
||||||
pub fn liquidity_in_range(&self, min_price: Decimal, max_price: Decimal, side: Side) -> Decimal {
|
pub fn liquidity_in_range(&self, min_price: Decimal, max_price: Decimal, side: Side) -> Decimal {
|
||||||
|
let min_price_u32 = decimal_to_price(min_price).unwrap_or(0);
|
||||||
|
let max_price_u32 = decimal_to_price(max_price).unwrap_or(0);
|
||||||
|
|
||||||
let levels: Vec<_> = match side {
|
let levels: Vec<_> = match side {
|
||||||
Side::BUY => self.asks.range(min_price..=max_price).collect(),
|
Side::BUY => self.asks.range(min_price_u32..=max_price_u32).collect(),
|
||||||
Side::SELL => self.bids.range(min_price..=max_price).rev().collect(),
|
Side::SELL => self.bids.range(min_price_u32..=max_price_u32).rev().collect(),
|
||||||
};
|
};
|
||||||
|
|
||||||
levels.into_iter().map(|(_, &size)| size).sum()
|
let total: i64 = levels.into_iter().map(|(_, &size)| size).sum();
|
||||||
|
Decimal::from(total)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validate that prices are properly ordered
|
/// Validate that prices are properly ordered
|
||||||
@@ -738,8 +743,8 @@ impl OrderBook {
|
|||||||
pub fn analytics(&self) -> BookAnalytics {
|
pub fn analytics(&self) -> BookAnalytics {
|
||||||
let bid_count = self.bids.len();
|
let bid_count = self.bids.len();
|
||||||
let ask_count = self.asks.len();
|
let ask_count = self.asks.len();
|
||||||
let total_bid_size: Decimal = self.bids.values().sum(); // Add up all bid sizes
|
let total_bid_size: Decimal = Decimal::from(self.bids.values().sum::<i64>()); // Add up all bid sizes
|
||||||
let total_ask_size: Decimal = self.asks.values().sum(); // Add up all ask sizes
|
let total_ask_size: Decimal = Decimal::from(self.asks.values().sum::<i64>()); // Add up all ask sizes
|
||||||
|
|
||||||
BookAnalytics {
|
BookAnalytics {
|
||||||
token_id: self.token_id.clone(),
|
token_id: self.token_id.clone(),
|
||||||
|
|||||||
+5
-4
@@ -6,6 +6,7 @@
|
|||||||
use alloy_primitives::{Address, U256};
|
use alloy_primitives::{Address, U256};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use rust_decimal::Decimal;
|
use rust_decimal::Decimal;
|
||||||
|
use rust_decimal::prelude::ToPrimitive;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -90,7 +91,7 @@ pub const MAX_QTY: Qty = Qty::MAX / 2; // Leave room for intermediate calculatio
|
|||||||
/// - decimal_to_price(Decimal::from_str("0.6543")) = Ok(6543)
|
/// - decimal_to_price(Decimal::from_str("0.6543")) = Ok(6543)
|
||||||
/// - decimal_to_price(Decimal::from_str("1.0000")) = Ok(10000)
|
/// - decimal_to_price(Decimal::from_str("1.0000")) = Ok(10000)
|
||||||
/// - decimal_to_price(Decimal::from_str("0.00005")) = Ok(1) // Rounds up to min tick
|
/// - decimal_to_price(Decimal::from_str("0.00005")) = Ok(1) // Rounds up to min tick
|
||||||
pub fn decimal_to_price(decimal: Decimal) -> Result<Price, &'static str> {
|
pub fn decimal_to_price(decimal: Decimal) -> std::result::Result<Price, &'static str> {
|
||||||
// Convert to fixed-point by multiplying by scale factor
|
// Convert to fixed-point by multiplying by scale factor
|
||||||
let scaled = decimal * Decimal::from(SCALE_FACTOR);
|
let scaled = decimal * Decimal::from(SCALE_FACTOR);
|
||||||
|
|
||||||
@@ -131,7 +132,7 @@ pub fn price_to_decimal(ticks: Price) -> Decimal {
|
|||||||
/// Examples:
|
/// Examples:
|
||||||
/// - decimal_to_qty(Decimal::from_str("100.0")) = Ok(1000000)
|
/// - decimal_to_qty(Decimal::from_str("100.0")) = Ok(1000000)
|
||||||
/// - decimal_to_qty(Decimal::from_str("-50.5")) = Ok(-505000)
|
/// - decimal_to_qty(Decimal::from_str("-50.5")) = Ok(-505000)
|
||||||
pub fn decimal_to_qty(decimal: Decimal) -> Result<Qty, &'static str> {
|
pub fn decimal_to_qty(decimal: Decimal) -> std::result::Result<Qty, &'static str> {
|
||||||
let scaled = decimal * Decimal::from(SCALE_FACTOR);
|
let scaled = decimal * Decimal::from(SCALE_FACTOR);
|
||||||
let rounded = scaled.round();
|
let rounded = scaled.round();
|
||||||
|
|
||||||
@@ -299,7 +300,7 @@ impl FastBookLevel {
|
|||||||
|
|
||||||
/// Create from external BookLevel (with validation)
|
/// Create from external BookLevel (with validation)
|
||||||
/// This is called when we receive data from the API
|
/// This is called when we receive data from the API
|
||||||
pub fn from_book_level(level: &BookLevel) -> Result<Self, &'static str> {
|
pub fn from_book_level(level: &BookLevel) -> std::result::Result<Self, &'static str> {
|
||||||
let price = decimal_to_price(level.price)?;
|
let price = decimal_to_price(level.price)?;
|
||||||
let size = decimal_to_qty(level.size)?;
|
let size = decimal_to_qty(level.size)?;
|
||||||
Ok(Self::new(price, size))
|
Ok(Self::new(price, size))
|
||||||
@@ -374,7 +375,7 @@ impl FastOrderDelta {
|
|||||||
/// This is where we enforce tick alignment - if the incoming price
|
/// This is where we enforce tick alignment - if the incoming price
|
||||||
/// doesn't align to valid ticks, we either reject it or round it.
|
/// doesn't align to valid ticks, we either reject it or round it.
|
||||||
/// This prevents bad data from corrupting our order book.
|
/// This prevents bad data from corrupting our order book.
|
||||||
pub fn from_order_delta(delta: &OrderDelta, tick_size: Option<Decimal>) -> Result<Self, &'static str> {
|
pub fn from_order_delta(delta: &OrderDelta, tick_size: Option<Decimal>) -> std::result::Result<Self, &'static str> {
|
||||||
// Validate tick alignment if we have a tick size
|
// Validate tick alignment if we have a tick size
|
||||||
if let Some(tick_size) = tick_size {
|
if let Some(tick_size) = tick_size {
|
||||||
if !is_price_tick_aligned(delta.price, tick_size) {
|
if !is_price_tick_aligned(delta.price, tick_size) {
|
||||||
|
|||||||
+267
-8
@@ -3,11 +3,12 @@
|
|||||||
//! These tests verify that our client can actually communicate with the real Polymarket API.
|
//! These tests verify that our client can actually communicate with the real Polymarket API.
|
||||||
//! They require network connectivity and may take longer to run.
|
//! They require network connectivity and may take longer to run.
|
||||||
|
|
||||||
use polyfill_rs::{ClobClient, Result, PolyfillError};
|
use polyfill_rs::{ClobClient, Result, PolyfillError, Side};
|
||||||
use rust_decimal::Decimal;
|
use rust_decimal::Decimal;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::env;
|
use std::env;
|
||||||
use tokio::time::{sleep, Duration};
|
use tokio::time::{sleep, Duration};
|
||||||
|
use tracing::{info, error, warn};
|
||||||
|
|
||||||
const POLYMARKET_HOST: &str = "https://clob.polymarket.com";
|
const POLYMARKET_HOST: &str = "https://clob.polymarket.com";
|
||||||
const POLYGON_CHAIN_ID: u64 = 137;
|
const POLYGON_CHAIN_ID: u64 = 137;
|
||||||
@@ -52,7 +53,7 @@ async fn test_api_connectivity() -> Result<()> {
|
|||||||
let server_time = client.get_server_time().await?;
|
let server_time = client.get_server_time().await?;
|
||||||
assert!(server_time > 0, "Invalid server time received");
|
assert!(server_time > 0, "Invalid server time received");
|
||||||
|
|
||||||
println!("✅ API connectivity test passed");
|
println!("API connectivity test passed");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +98,7 @@ async fn test_market_data_endpoints() -> Result<()> {
|
|||||||
let neg_risk = client.get_neg_risk(token_id).await?;
|
let neg_risk = client.get_neg_risk(token_id).await?;
|
||||||
// neg_risk is a boolean, so just verify it doesn't panic
|
// neg_risk is a boolean, so just verify it doesn't panic
|
||||||
|
|
||||||
println!("✅ Market data endpoints test passed");
|
println!("Market data endpoints test passed");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +112,7 @@ async fn test_error_handling() -> Result<()> {
|
|||||||
match result {
|
match result {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// Some APIs might return empty data instead of error
|
// Some APIs might return empty data instead of error
|
||||||
println!("⚠️ Invalid token_id returned data instead of error");
|
println!("Invalid token_id returned data instead of error");
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
match e {
|
match e {
|
||||||
@@ -126,7 +127,7 @@ async fn test_error_handling() -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("✅ Error handling test passed");
|
println!(" Error handling test passed");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +150,7 @@ async fn test_rate_limiting() -> Result<()> {
|
|||||||
let success_count = results.iter().filter(|r| r.is_ok()).count();
|
let success_count = results.iter().filter(|r| r.is_ok()).count();
|
||||||
assert!(success_count >= 3, "Too many requests failed: {}/5", success_count);
|
assert!(success_count >= 3, "Too many requests failed: {}/5", success_count);
|
||||||
|
|
||||||
println!("✅ Rate limiting test passed");
|
println!(" Rate limiting test passed");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +176,7 @@ async fn test_api_compatibility() -> Result<()> {
|
|||||||
assert_eq!(order_args.token_id, "test_token");
|
assert_eq!(order_args.token_id, "test_token");
|
||||||
assert_eq!(order_args.side, polyfill_rs::Side::BUY);
|
assert_eq!(order_args.side, polyfill_rs::Side::BUY);
|
||||||
|
|
||||||
println!("✅ API compatibility test passed");
|
println!(" API compatibility test passed");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,10 +198,268 @@ async fn test_performance() -> Result<()> {
|
|||||||
assert!(server_time_duration < Duration::from_secs(5), "Server time too slow: {:?}", server_time_duration);
|
assert!(server_time_duration < Duration::from_secs(5), "Server time too slow: {:?}", server_time_duration);
|
||||||
assert!(markets_duration < Duration::from_secs(10), "Markets request too slow: {:?}", markets_duration);
|
assert!(markets_duration < Duration::from_secs(10), "Markets request too slow: {:?}", markets_duration);
|
||||||
|
|
||||||
println!("✅ Performance test passed");
|
println!(" Performance test passed");
|
||||||
println!(" Server time: {:?}", server_time_duration);
|
println!(" Server time: {:?}", server_time_duration);
|
||||||
println!(" Markets request: {:?}", markets_duration);
|
println!(" Markets request: {:?}", markets_duration);
|
||||||
println!(" Markets returned: {}", markets.data.len());
|
println!(" Markets returned: {}", markets.data.len());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Comprehensive test of all market data endpoints
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_all_market_data_endpoints() -> Result<()> {
|
||||||
|
let client = ClobClient::new(POLYMARKET_HOST);
|
||||||
|
|
||||||
|
// Get a valid token ID first
|
||||||
|
let markets_response = client.get_sampling_markets(Some(5)).await?;
|
||||||
|
assert!(!markets_response.data.is_empty(), "No markets returned");
|
||||||
|
|
||||||
|
let first_market = &markets_response.data[0];
|
||||||
|
let token_id = &first_market.tokens[0].token_id;
|
||||||
|
|
||||||
|
println!("Testing all endpoints with token_id: {}", token_id);
|
||||||
|
|
||||||
|
// Test all endpoints systematically
|
||||||
|
let endpoints = vec![
|
||||||
|
("Order Book", Box::new(|| client.get_order_book(token_id)) as Box<dyn Fn() -> _>),
|
||||||
|
("Midpoint", Box::new(|| client.get_midpoint(token_id))),
|
||||||
|
("Spread", Box::new(|| client.get_spread(token_id))),
|
||||||
|
("Buy Price", Box::new(|| client.get_price(token_id, Side::BUY))),
|
||||||
|
("Sell Price", Box::new(|| client.get_price(token_id, Side::SELL))),
|
||||||
|
("Tick Size", Box::new(|| client.get_tick_size(token_id))),
|
||||||
|
("Neg Risk", Box::new(|| client.get_neg_risk(token_id))),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut success_count = 0;
|
||||||
|
let mut total_time = Duration::from_secs(0);
|
||||||
|
|
||||||
|
for (name, endpoint_test) in endpoints {
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
match endpoint_test().await {
|
||||||
|
Ok(_) => {
|
||||||
|
let duration = start.elapsed();
|
||||||
|
total_time += duration;
|
||||||
|
success_count += 1;
|
||||||
|
println!(" {}: {:.2}ms", name, duration.as_secs_f64() * 1000.0);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let duration = start.elapsed();
|
||||||
|
total_time += duration;
|
||||||
|
println!(" {}: {:.2}ms - {}", name, duration.as_secs_f64() * 1000.0, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small delay between requests
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let avg_time = total_time / endpoints.len() as u32;
|
||||||
|
let success_rate = (success_count as f64 / endpoints.len() as f64) * 100.0;
|
||||||
|
|
||||||
|
println!("Endpoint Test Summary:");
|
||||||
|
println!(" Success rate: {:.1}%", success_rate);
|
||||||
|
println!(" Average response time: {:.2}ms", avg_time.as_secs_f64() * 1000.0);
|
||||||
|
|
||||||
|
// Require at least 80% success rate
|
||||||
|
assert!(success_rate >= 80.0, "Success rate too low: {:.1}%", success_rate);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test data consistency across endpoints
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_data_consistency() -> Result<()> {
|
||||||
|
let client = ClobClient::new(POLYMARKET_HOST);
|
||||||
|
|
||||||
|
// Get a valid token ID
|
||||||
|
let markets_response = client.get_sampling_markets(Some(5)).await?;
|
||||||
|
let first_market = &markets_response.data[0];
|
||||||
|
let token_id = &first_market.tokens[0].token_id;
|
||||||
|
|
||||||
|
println!("Testing data consistency with token_id: {}", token_id);
|
||||||
|
|
||||||
|
// Get all market data
|
||||||
|
let order_book = client.get_order_book(token_id).await?;
|
||||||
|
let midpoint = client.get_midpoint(token_id).await?;
|
||||||
|
let spread = client.get_spread(token_id).await?;
|
||||||
|
let buy_price = client.get_price(token_id, Side::BUY).await?;
|
||||||
|
let sell_price = client.get_price(token_id, Side::SELL).await?;
|
||||||
|
let tick_size = client.get_tick_size(token_id).await?;
|
||||||
|
let neg_risk = client.get_neg_risk(token_id).await?;
|
||||||
|
|
||||||
|
// Validate data consistency
|
||||||
|
println!("Data validation:");
|
||||||
|
|
||||||
|
// Check that prices are positive
|
||||||
|
assert!(buy_price.price > Decimal::ZERO, "Buy price should be positive: {}", buy_price.price);
|
||||||
|
assert!(sell_price.price > Decimal::ZERO, "Sell price should be positive: {}", sell_price.price);
|
||||||
|
println!(" Prices are positive");
|
||||||
|
|
||||||
|
// Check that spread is non-negative
|
||||||
|
assert!(spread.spread >= Decimal::ZERO, "Spread should be non-negative: {}", spread.spread);
|
||||||
|
println!(" Spread is non-negative");
|
||||||
|
|
||||||
|
// Check that tick size is positive
|
||||||
|
assert!(tick_size > Decimal::ZERO, "Tick size should be positive: {}", tick_size);
|
||||||
|
println!(" Tick size is positive");
|
||||||
|
|
||||||
|
// Check that midpoint is reasonable (between buy and sell if both exist)
|
||||||
|
if buy_price.price > Decimal::ZERO && sell_price.price > Decimal::ZERO {
|
||||||
|
if midpoint.mid < buy_price.price || midpoint.mid > sell_price.price {
|
||||||
|
println!(" Midpoint {} is not between buy {} and sell {}",
|
||||||
|
midpoint.mid, buy_price.price, sell_price.price);
|
||||||
|
} else {
|
||||||
|
println!(" Midpoint is between buy and sell prices");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that we have some order book data
|
||||||
|
if order_book.bids.is_empty() && order_book.asks.is_empty() {
|
||||||
|
println!(" Order book is empty");
|
||||||
|
} else {
|
||||||
|
println!(" Order book has liquidity ({} bids, {} asks)",
|
||||||
|
order_book.bids.len(), order_book.asks.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check neg risk is a boolean
|
||||||
|
println!(" Neg risk is boolean: {}", neg_risk);
|
||||||
|
|
||||||
|
println!(" Data consistency test passed");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test rate limiting behavior more thoroughly
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_comprehensive_rate_limiting() -> Result<()> {
|
||||||
|
let client = ClobClient::new(POLYMARKET_HOST);
|
||||||
|
|
||||||
|
println!("Testing rate limiting with rapid requests...");
|
||||||
|
|
||||||
|
// Make many rapid requests
|
||||||
|
let mut results = Vec::new();
|
||||||
|
let request_count = 20;
|
||||||
|
|
||||||
|
for i in 0..request_count {
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
let result = client.get_server_time().await;
|
||||||
|
let duration = start.elapsed();
|
||||||
|
|
||||||
|
results.push((i, result, duration));
|
||||||
|
|
||||||
|
// Very small delay to test rate limiting
|
||||||
|
sleep(Duration::from_millis(50)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Analyze results
|
||||||
|
let success_count = results.iter().filter(|(_, result, _)| result.is_ok()).count();
|
||||||
|
let failure_count = results.iter().filter(|(_, result, _)| result.is_err()).count();
|
||||||
|
|
||||||
|
let success_rate = (success_count as f64 / request_count as f64) * 100.0;
|
||||||
|
|
||||||
|
println!("Rate limiting test results:");
|
||||||
|
println!(" Total requests: {}", request_count);
|
||||||
|
println!(" Successful: {}", success_count);
|
||||||
|
println!(" Failed: {}", failure_count);
|
||||||
|
println!(" Success rate: {:.1}%", success_rate);
|
||||||
|
|
||||||
|
// Show timing for first few requests
|
||||||
|
for (i, result, duration) in results.iter().take(5) {
|
||||||
|
let status = if result.is_ok() { "" } else { "" };
|
||||||
|
println!(" Request {}: {} {:.2}ms", i + 1, status, duration.as_secs_f64() * 1000.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We expect some failures due to rate limiting, but not too many
|
||||||
|
assert!(success_rate >= 50.0, "Success rate too low, possible rate limiting issues: {:.1}%", success_rate);
|
||||||
|
|
||||||
|
println!(" Rate limiting test passed");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test error handling with various invalid inputs
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_comprehensive_error_handling() -> Result<()> {
|
||||||
|
let client = ClobClient::new(POLYMARKET_HOST);
|
||||||
|
|
||||||
|
println!("Testing comprehensive error handling...");
|
||||||
|
|
||||||
|
// Test various invalid token IDs
|
||||||
|
let invalid_tokens = vec![
|
||||||
|
"",
|
||||||
|
"invalid",
|
||||||
|
"12345",
|
||||||
|
"0x1234567890123456789012345678901234567890",
|
||||||
|
"very_long_invalid_token_id_that_should_fail",
|
||||||
|
];
|
||||||
|
|
||||||
|
for token_id in invalid_tokens {
|
||||||
|
println!(" Testing invalid token ID: '{}'", token_id);
|
||||||
|
|
||||||
|
let result = client.get_order_book(token_id).await;
|
||||||
|
match result {
|
||||||
|
Ok(order_book) => {
|
||||||
|
if order_book.bids.is_empty() && order_book.asks.is_empty() {
|
||||||
|
println!(" Empty order book returned (acceptable)");
|
||||||
|
} else {
|
||||||
|
println!(" Unexpected data returned for invalid token");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
match e {
|
||||||
|
PolyfillError::Api { status, .. } => {
|
||||||
|
if status >= 400 {
|
||||||
|
println!(" Correctly returned API error: {}", status);
|
||||||
|
} else {
|
||||||
|
println!(" Unexpected status code: {}", status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
println!(" Correctly returned error: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(" Error handling test passed");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test concurrent requests
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_concurrent_requests() -> Result<()> {
|
||||||
|
let client = ClobClient::new(POLYMARKET_HOST);
|
||||||
|
|
||||||
|
println!("Testing concurrent requests...");
|
||||||
|
|
||||||
|
// Get a valid token ID
|
||||||
|
let markets_response = client.get_sampling_markets(Some(5)).await?;
|
||||||
|
let token_id = &markets_response.data[0].tokens[0].token_id;
|
||||||
|
|
||||||
|
// Make concurrent requests
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
|
||||||
|
let results = tokio::join!(
|
||||||
|
client.get_server_time(),
|
||||||
|
client.get_midpoint(token_id),
|
||||||
|
client.get_spread(token_id),
|
||||||
|
client.get_price(token_id, Side::BUY),
|
||||||
|
client.get_price(token_id, Side::SELL),
|
||||||
|
);
|
||||||
|
|
||||||
|
let duration = start.elapsed();
|
||||||
|
|
||||||
|
// Check results
|
||||||
|
let (server_time, midpoint, spread, buy_price, sell_price) = results;
|
||||||
|
|
||||||
|
assert!(server_time.is_ok(), "Server time request failed");
|
||||||
|
assert!(midpoint.is_ok(), "Midpoint request failed");
|
||||||
|
assert!(spread.is_ok(), "Spread request failed");
|
||||||
|
assert!(buy_price.is_ok(), "Buy price request failed");
|
||||||
|
assert!(sell_price.is_ok(), "Sell price request failed");
|
||||||
|
|
||||||
|
println!(" All concurrent requests succeeded");
|
||||||
|
println!(" Total time: {:.2}ms", duration.as_secs_f64() * 1000.0);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user