webull_unofficial 1.0.1

The unofficial Rust interface for the WeBull API
Documentation

Webull Rust API

An unofficial Rust library for the Webull API, providing full functionality for trading, market data, and streaming. Because this library uses the webull app API's, Official API key access is not required.

This library is a Rust port of the excellent webull Python library by @tedchou12. The Python library served as the foundation for understanding Webull's API structure and authentication mechanisms.

⚠️ Important Disclaimer

This is an unofficial library that is not affiliated with or endorsed by Webull.

Webull may change their API endpoints, authentication methods, or data structures at any time without notice, which could cause this library to stop working partially or completely. Users of this library should:

  • Be prepared for potential breaking changes
  • Test thoroughly in paper trading before using with real money
  • Implement proper error handling for API failures
  • Consider this library experimental and use at your own risk

The maintainers of this library cannot guarantee its continued functionality and are not responsible for any losses incurred from its use.

Features

  • ✅ Full authentication support (including MFA)
  • ✅ Real trading and paper trading
  • ✅ Market data and quotes
  • ✅ Order placement and management
  • ✅ Real-time streaming via MQTT
  • ✅ News and fundamentals

Installation

Add this to your Cargo.toml:

[dependencies]
webull = "1.0.0"

Quick Start

Unified Client Interface

The library provides a unified WebullClient enum that can work with both live and paper trading:

use webull_unofficial::{WebullClient, models::*, error::Result};

#[tokio::main]
async fn main() -> Result<()> {
    // Create paper trading client
    let mut client = WebullClient::new_paper(Some(6))?; // 6 = US region

    // Or create live trading client
    // let mut client = WebullClient::new_live(Some(6))?;

    // Login
    client.login("email@example.com", "password", None, None, None, None).await?;

    // Get account info
    let account = client.get_account().await?;
    if let Some(net_liquidation) = account.net_liquidation {
        println!("Account balance: ${:.2}", net_liquidation);
    }

    // Find ticker
    let tickers = client.find_ticker("AAPL").await?;
    if let Some(ticker) = tickers.first() {
        // Get quotes
        let quote = client.get_quotes(&ticker.ticker_id.to_string()).await?;
        println!("AAPL price: ${}", quote.close);

        // IMPORTANT: Live trading requires a trade token before placing orders
        // Paper trading does NOT require a trade token
        if !client.is_paper() {
            client.get_trade_token("your_trading_pin").await?;  // 6-digit PIN
        }

        let order = PlaceOrderRequest {
            ticker_id: ticker.ticker_id,
            action: OrderAction::Buy,
            order_type: OrderType::Limit,
            time_in_force: TimeInForce::Day,
            quantity: 1.0,
            limit_price: Some(150.0),
            stop_price: None,
            outside_regular_trading_hour: false,
            serial_id: None,
            combo_type: None,
        };

        let order_id = client.place_order(&order).await?;
        println!("Order placed: {}", order_id);
    }

    Ok(())
}

Direct Client Usage

You can also use the specific client implementations directly:

use webull_unofficial::{LiveWebullClient, PaperWebullClient, models::*, error::Result};

// For live trading
let mut live_client = LiveWebullClient::new(Some(6))?;
live_client.login("email", "password", None, None, None, None).await?;
live_client.get_trade_token("123456").await?;  // Your 6-digit trading PIN - Required for placing orders!

// For paper trading
let mut paper_client = PaperWebullClient::new(Some(6))?;
paper_client.login("email", "password", None, None, None, None).await?;
// No trade token needed for paper trading

Architecture

The library is organized into three main client types:

  1. WebullClient - A unified enum that provides a common interface for both live and paper trading
  2. LiveWebullClient - Direct implementation for live trading operations
  3. PaperWebullClient - Implementation for paper (simulated) trading

The unified WebullClient enum automatically delegates method calls to the appropriate underlying implementation, making it easy to switch between live and paper trading modes.

Streaming Example

use webull_unofficial::{StreamConn, stream::{StreamConfig, TopicTypes}};

// Create streaming connection
let config = StreamConfig {
    debug: true,  // Enable debug logging
    ..Default::default()
};
let mut stream = StreamConn::new(Some(config));

// Set callbacks
stream.set_price_callback(|topic, data| {
    println!("Price update: {:?}", data);
});

// Connect using access token and device ID from login
stream.connect(&access_token, &device_id).await?;

// Subscribe to ticker updates
stream.subscribe_ticker("913256135", TopicTypes::basic()).await?;

Environment Variables

Create a .env file:

WEBULL_USERNAME=your_email@example.com
WEBULL_PASSWORD=your_password
WEBULL_TRADING_PIN=123456  # Your 6-digit trading PIN

Examples

See the examples/ directory for more complete examples:

  • basic_usage.rs - Login, get account info, positions, and quotes
  • trading_test.rs - Interactive trading test with both live and paper support
  • paper_trading.rs - Paper trading specific functionality
  • place_order.rs - Place and cancel orders with live trading
  • streaming.rs - Real-time data streaming
  • set_device_id.rs - Device ID management utility
  • test_bars.rs - Historical data retrieval example

Run examples with:

cargo run --example basic_usage

# Interactive trading test (supports both live and paper)
cargo run --example trading_test

# Paper trading only
cargo run --example paper_trading

Important: Live Trading Requirements

Trade Token

Live trading requires obtaining a trade token before placing any orders. This is a security measure that requires your 6-digit trading PIN (NOT your login password).

// For live trading, get trade token after login
if !client.is_paper() {
    client.get_trade_token("123456").await?;  // Your 6-digit trading PIN
}

// Now you can place orders
let order_id = client.place_order(&order).await?;

Paper trading does NOT require a trade token - you can place orders immediately after login.

Working with Orders

Getting Current Orders

// Get open orders
let orders = client.get_orders(None).await?;
for order in orders {
    let symbol = order.ticker.as_ref()
        .map(|t| t.symbol.as_str())
        .unwrap_or("Unknown");
    println!("Order {}: {:?} {} shares of {} at ${:.2}",
        order.order_id,
        order.action,
        order.quantity,
        symbol,
        order.limit_price.unwrap_or(0.0)
    );
}

Canceling Orders

// Get and cancel all open orders
let orders = client.get_orders(None).await?;
for order in orders {
    if order.status == OrderStatus::Working {
        client.cancel_order(&order.order_id).await?;
        println!("Cancelled order {}", order.order_id);
    }
}

API Coverage

Account Management

  • Login/Logout
  • MFA support
  • Get account details
  • Get positions
  • Get orders history
  • Get account activities

Trading

  • Place orders (stocks)
  • Cancel orders
  • Modify orders
  • Place option orders
  • OTOCO orders

Market Data

  • Get quotes
  • Get bars/candles
  • Get Level 2 data
  • Search tickers

Streaming

  • Real-time quotes
  • Order updates
  • Trade executions
  • Level 2 updates

Analysis

  • Get news
  • Get analyst ratings
  • Screener
  • Rankings

Error Handling

The library uses a custom WebullError type for comprehensive error handling:

match client.login(&username, &password, None, None, None, None).await {
    Ok(response) => println!("Logged in!"),
    Err(WebullError::MfaRequired) => println!("Need MFA code"),
    Err(WebullError::InvalidCredentials) => println!("Bad credentials"),
    Err(e) => eprintln!("Error: {}", e),
}

License

MIT

Acknowledgements

  • @tedchou12 - Creator of the original webull Python library which this Rust implementation is based on. The Python library's clean design and comprehensive API coverage made this port possible.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.