polymarket 0.1.0

Rust SDK for Polymarket prediction market - CLOB trading, on-chain operations, and WebSocket streaming
Documentation
use std::time::{SystemTime, UNIX_EPOCH};

use crate::clob::{
    error::{ClobError, Result},
    types::{TickSize, RoundConfig},
};

/// Get the current Unix timestamp in seconds
pub fn get_current_unix_time_secs() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_secs()
}

/// Get the current Unix timestamp in milliseconds
pub fn get_current_unix_time_millis() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_millis() as u64
}

/// Round a value to the nearest tick size
///
/// # Arguments
/// * `value` - The value to round
/// * `tick_size` - The tick size to round to
///
/// # Returns
/// The rounded value
pub fn round_to_tick(value: f64, tick_size: TickSize) -> f64 {
    let tick = tick_size.as_f64();
    (value / tick).round() * tick
}

/// Round a price to the nearest tick size
///
/// # Arguments
/// * `price` - The price to round
/// * `tick_size` - The tick size to round to
///
/// # Returns
/// The rounded price, clamped between 0.0 and 1.0
pub fn round_price(price: f64, tick_size: TickSize) -> f64 {
    let rounded = round_to_tick(price, tick_size);
    rounded.clamp(0.0, 1.0)
}

/// Round a size to the nearest tick size
///
/// # Arguments
/// * `size` - The size to round
/// * `tick_size` - The tick size to round to
///
/// # Returns
/// The rounded size
pub fn round_size(size: f64, tick_size: TickSize) -> f64 {
    round_to_tick(size, tick_size).max(0.0)
}

/// Calculate the amount for an order
///
/// # Arguments
/// * `price` - The price
/// * `size` - The size
///
/// # Returns
/// The amount (price * size)
pub fn calculate_amount(price: f64, size: f64) -> f64 {
    price * size
}

/// Round an amount to the nearest tick size
///
/// # Arguments
/// * `amount` - The amount to round
/// * `tick_size` - The tick size to round to
///
/// # Returns
/// The rounded amount
pub fn round_amount(amount: f64, tick_size: TickSize) -> f64 {
    round_to_tick(amount, tick_size).max(0.0)
}

/// Create a round configuration from a tick size
///
/// # Arguments
/// * `tick_size` - The tick size
///
/// # Returns
/// A RoundConfig with the appropriate rounding values
pub fn create_round_config(tick_size: TickSize) -> RoundConfig {
    RoundConfig {
        price: tick_size.as_f64(),
        size: tick_size.as_f64(),
        amount: tick_size.as_f64(),
    }
}

/// Validate a price is within bounds
///
/// # Arguments
/// * `price` - The price to validate
///
/// # Returns
/// Ok if valid, Err otherwise
pub fn validate_price(price: f64) -> Result<()> {
    if price < 0.0 || price > 1.0 {
        return Err(ClobError::InvalidOrder(format!(
            "Price must be between 0 and 1, got: {}",
            price
        )));
    }
    Ok(())
}

/// Validate a size is positive
///
/// # Arguments
/// * `size` - The size to validate
///
/// # Returns
/// Ok if valid, Err otherwise
pub fn validate_size(size: f64) -> Result<()> {
    if size <= 0.0 {
        return Err(ClobError::InvalidOrder(format!(
            "Size must be positive, got: {}",
            size
        )));
    }
    Ok(())
}

/// Validate an amount is positive
///
/// # Arguments
/// * `amount` - The amount to validate
///
/// # Returns
/// Ok if valid, Err otherwise
pub fn validate_amount(amount: f64) -> Result<()> {
    if amount <= 0.0 {
        return Err(ClobError::InvalidOrder(format!(
            "Amount must be positive, got: {}",
            amount
        )));
    }
    Ok(())
}

/// Convert a float to a raw amount string (no decimals)
///
/// This multiplies by 10^6 to convert to the smallest unit
///
/// # Arguments
/// * `value` - The value to convert
///
/// # Returns
/// String representation of the raw amount
pub fn to_raw_amount(value: f64) -> String {
    let raw = (value * 1_000_000.0).round() as u64;
    raw.to_string()
}

/// Convert a Decimal to a raw amount string (using rust_decimal for precision)
///
/// This multiplies by 10^6 and rounds to integer
///
/// # Arguments
/// * `value` - The Decimal value to convert
///
/// # Returns
/// String representation of the raw amount
pub fn to_raw_amount_decimal(value: rust_decimal::Decimal) -> String {
    // Multiply by 1M and round to integer
    (value * rust_decimal::Decimal::from(1_000_000))
        .round_dp(0)
        .to_string()
}

/// Convert a raw amount string to a float
///
/// This divides by 10^6 to convert from the smallest unit
///
/// # Arguments
/// * `raw` - The raw amount string
///
/// # Returns
/// The float value
pub fn from_raw_amount(raw: &str) -> Result<f64> {
    let amount: u64 = raw.parse().map_err(|e| {
        ClobError::InvalidOrder(format!("Invalid raw amount '{}': {}", raw, e))
    })?;
    Ok(amount as f64 / 1_000_000.0)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_get_current_unix_time_secs() {
        let timestamp = get_current_unix_time_secs();
        assert!(timestamp > 1_600_000_000); // After Sept 2020
    }

    #[test]
    fn test_get_current_unix_time_millis() {
        let timestamp = get_current_unix_time_millis();
        assert!(timestamp > 1_600_000_000_000); // After Sept 2020
    }

    #[test]
    fn test_round_to_tick() {
        let rounded = round_to_tick(0.567, TickSize::ZeroPointZeroOne);
        assert!((rounded - 0.57).abs() < 0.0001);
    }

    #[test]
    fn test_round_price() {
        let rounded = round_price(0.567, TickSize::ZeroPointZeroOne);
        assert!((rounded - 0.57).abs() < 0.0001);

        // Test clamping
        let clamped = round_price(1.5, TickSize::ZeroPointZeroOne);
        assert_eq!(clamped, 1.0);

        let clamped = round_price(-0.5, TickSize::ZeroPointZeroOne);
        assert_eq!(clamped, 0.0);
    }

    #[test]
    fn test_round_size() {
        let rounded = round_size(123.456, TickSize::ZeroPointOne);
        assert_eq!(rounded, 123.5);

        // Test negative becomes zero
        let rounded = round_size(-10.0, TickSize::ZeroPointOne);
        assert_eq!(rounded, 0.0);
    }

    #[test]
    fn test_calculate_amount() {
        let amount = calculate_amount(0.5, 100.0);
        assert_eq!(amount, 50.0);
    }

    #[test]
    fn test_round_amount() {
        let rounded = round_amount(123.456, TickSize::ZeroPointZeroOne);
        assert!((rounded - 123.46).abs() < 0.0001);
    }

    #[test]
    fn test_validate_price() {
        assert!(validate_price(0.5).is_ok());
        assert!(validate_price(0.0).is_ok());
        assert!(validate_price(1.0).is_ok());
        assert!(validate_price(-0.1).is_err());
        assert!(validate_price(1.1).is_err());
    }

    #[test]
    fn test_validate_size() {
        assert!(validate_size(1.0).is_ok());
        assert!(validate_size(100.0).is_ok());
        assert!(validate_size(0.0).is_err());
        assert!(validate_size(-1.0).is_err());
    }

    #[test]
    fn test_validate_amount() {
        assert!(validate_amount(1.0).is_ok());
        assert!(validate_amount(100.0).is_ok());
        assert!(validate_amount(0.0).is_err());
        assert!(validate_amount(-1.0).is_err());
    }

    #[test]
    fn test_to_raw_amount() {
        assert_eq!(to_raw_amount(1.0), "1000000");
        assert_eq!(to_raw_amount(0.5), "500000");
        assert_eq!(to_raw_amount(123.456), "123456000");
    }

    #[test]
    fn test_from_raw_amount() {
        assert_eq!(from_raw_amount("1000000").unwrap(), 1.0);
        assert_eq!(from_raw_amount("500000").unwrap(), 0.5);
        assert_eq!(from_raw_amount("123456000").unwrap(), 123.456);
    }

    #[test]
    fn test_round_trip_conversion() {
        let original = 123.456;
        let raw = to_raw_amount(original);
        let converted = from_raw_amount(&raw).unwrap();
        assert_eq!(converted, original);
    }

    #[test]
    fn test_create_round_config() {
        let config = create_round_config(TickSize::ZeroPointZeroOne);
        assert_eq!(config.price, 0.01);
        assert_eq!(config.size, 0.01);
        assert_eq!(config.amount, 0.01);
    }
}