use crate::{
endpoints::Endpoints,
error::{Result, WebullError},
models::*,
utils::*,
};
use reqwest::{
header::{HeaderMap, HeaderValue, CONTENT_TYPE},
Client,
};
use serde_json::{json, Value};
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use uuid::Uuid;
#[derive(Debug, Clone)]
pub struct LiveWebullClient {
pub client: Client,
pub endpoints: Endpoints,
pub(crate) headers: HeaderMap,
pub(crate) account_id: Option<String>,
pub(crate) trade_token: Option<String>,
pub(crate) access_token: Option<String>,
pub(crate) refresh_token: Option<String>,
pub(crate) token_expire: Option<i64>,
pub(crate) uuid: Option<String>,
pub(crate) did: String,
pub(crate) region_code: i32,
pub(crate) zone_var: String,
pub(crate) timeout: u64,
}
impl LiveWebullClient {
pub fn new(region_code: Option<i32>) -> Result<Self> {
let did = get_did(None)?;
let mut headers = HeaderMap::new();
headers.insert("User-Agent", HeaderValue::from_static("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:99.0) Gecko/20100101 Firefox/99.0"));
headers.insert("Accept", HeaderValue::from_static("*/*"));
headers.insert("Accept-Encoding", HeaderValue::from_static("gzip, deflate"));
headers.insert(
"Accept-Language",
HeaderValue::from_static("en-US,en;q=0.5"),
);
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
headers.insert("platform", HeaderValue::from_static("web"));
headers.insert("hl", HeaderValue::from_static("en"));
headers.insert("os", HeaderValue::from_static("web"));
headers.insert("osv", HeaderValue::from_static("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:99.0) Gecko/20100101 Firefox/99.0"));
headers.insert("app", HeaderValue::from_static("global"));
headers.insert("appid", HeaderValue::from_static("webull-webapp"));
headers.insert("ver", HeaderValue::from_static("3.39.18"));
headers.insert("lzone", HeaderValue::from_static("dc_core_r001"));
headers.insert("ph", HeaderValue::from_static("MacOS Firefox"));
headers.insert("locale", HeaderValue::from_static("eng"));
headers.insert("device-type", HeaderValue::from_static("Web"));
headers.insert("did", HeaderValue::from_str(&did).unwrap());
Ok(Self {
client: Client::new(),
endpoints: Endpoints::new(),
headers,
account_id: None,
trade_token: None,
access_token: None,
refresh_token: None,
token_expire: None,
uuid: None,
did,
region_code: region_code.unwrap_or(6),
zone_var: "dc_core_r001".to_string(),
timeout: 15,
})
}
pub fn set_did(&mut self, did: &str, path: Option<&Path>) -> Result<()> {
save_did(did, path)?;
self.did = did.to_string();
self.headers
.insert("did", HeaderValue::from_str(did).unwrap());
Ok(())
}
pub fn get_did(&self) -> &str {
&self.did
}
pub fn get_account_id_str(&self) -> Option<&str> {
self.account_id.as_deref()
}
pub fn build_req_headers(
&self,
include_trade_token: bool,
include_time: bool,
include_zone_var: bool,
) -> HeaderMap {
let mut headers = self.headers.clone();
let req_id = generate_req_id();
headers.insert("reqid", HeaderValue::from_str(&req_id).unwrap());
headers.insert("did", HeaderValue::from_str(&self.did).unwrap());
if let Some(access_token) = &self.access_token {
headers.insert("access_token", HeaderValue::from_str(access_token).unwrap());
}
if include_trade_token {
if let Some(trade_token) = &self.trade_token {
headers.insert("t_token", HeaderValue::from_str(trade_token).unwrap());
}
}
if include_time {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis()
.to_string();
headers.insert("t_time", HeaderValue::from_str(×tamp).unwrap());
}
if include_zone_var {
headers.insert("lzone", HeaderValue::from_str(&self.zone_var).unwrap());
}
headers
}
pub async fn login(
&mut self,
username: &str,
password: &str,
device_name: Option<&str>,
mfa: Option<&str>,
question_id: Option<&str>,
question_answer: Option<&str>,
) -> Result<LoginResponse> {
if username.is_empty() || password.is_empty() {
return Err(WebullError::InvalidParameter(
"Username or password is empty".to_string(),
));
}
let hashed_password = hash_password(password);
let account_type = get_account_type(username)?;
let device_name = device_name.unwrap_or("default_string");
let mut data = json!({
"account": username,
"accountType": account_type.to_string(),
"deviceId": self.did,
"deviceName": device_name,
"grade": 1,
"pwd": hashed_password,
"regionId": self.region_code
});
let headers = if let Some(mfa_code) = mfa {
data["extInfo"] = json!({
"codeAccountType": account_type,
"verificationCode": mfa_code
});
self.build_req_headers(false, false, true)
} else {
self.headers.clone()
};
if let (Some(qid), Some(qanswer)) = (question_id, question_answer) {
data["accessQuestions"] = json!(format!(
"[{{\"questionId\":\"{}\", \"answer\":\"{}\"}}]",
qid, qanswer
));
}
let response = self
.client
.post(&self.endpoints.login())
.headers(headers)
.json(&data)
.timeout(std::time::Duration::from_secs(self.timeout))
.send()
.await?;
let result: Value = response.json().await?;
if let Some(access_token) = result.get("accessToken").and_then(|v| v.as_str()) {
self.access_token = Some(access_token.to_string());
self.refresh_token = result
.get("refreshToken")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
self.token_expire = result.get("tokenExpireTime").and_then(|v| {
v.as_i64().or_else(|| {
v.as_str().and_then(|s| {
chrono::DateTime::parse_from_rfc3339(s)
.ok()
.map(|dt| dt.timestamp())
})
})
});
self.uuid = result
.get("uuid")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
self.get_account_id().await?;
Ok(serde_json::from_value(result)?)
} else {
Err(WebullError::AuthenticationError("Login failed".to_string()))
}
}
pub async fn login_with(
&mut self,
builder: crate::models::LoginRequestBuilder,
) -> Result<LoginResponse> {
let (username, password, device_name, mfa, question_id, question_answer) = builder
.build()
.map_err(|e| WebullError::InvalidRequest(e))?;
self.login(
&username,
&password,
device_name.as_deref(),
mfa.as_deref(),
question_id.as_deref(),
question_answer.as_deref(),
)
.await
}
pub async fn get_mfa(&self, username: &str) -> Result<bool> {
let account_type = get_account_type(username)?;
let data = json!({
"account": username,
"accountType": account_type.to_string(),
"codeType": 5
});
let response = self
.client
.post(&self.endpoints.get_mfa())
.headers(self.headers.clone())
.json(&data)
.timeout(std::time::Duration::from_secs(self.timeout))
.send()
.await?;
Ok(response.status().is_success())
}
pub async fn check_mfa(&self, username: &str, mfa: &str) -> Result<bool> {
let account_type = get_account_type(username)?;
let data = json!({
"account": username,
"accountType": account_type.to_string(),
"code": mfa,
"codeType": 5
});
let response = self
.client
.post(&self.endpoints.check_mfa())
.headers(self.headers.clone())
.json(&data)
.timeout(std::time::Duration::from_secs(self.timeout))
.send()
.await?;
Ok(response.status().is_success())
}
pub async fn logout(&mut self) -> Result<bool> {
let headers = self.build_req_headers(false, false, true);
let response = self
.client
.post(&self.endpoints.logout())
.headers(headers)
.timeout(std::time::Duration::from_secs(self.timeout))
.send()
.await?;
if response.status().is_success() {
self.access_token = None;
self.refresh_token = None;
self.trade_token = None;
self.account_id = None;
self.token_expire = None;
self.uuid = None;
Ok(true)
} else {
Ok(false)
}
}
pub async fn refresh_login(&mut self) -> Result<LoginResponse> {
let refresh_token = self
.refresh_token
.as_ref()
.ok_or(WebullError::SessionExpired)?;
let response = self
.client
.post(&self.endpoints.refresh_login(refresh_token))
.headers(self.headers.clone())
.timeout(std::time::Duration::from_secs(self.timeout))
.send()
.await?;
let result: Value = response.json().await?;
if let Some(access_token) = result.get("accessToken").and_then(|v| v.as_str()) {
self.access_token = Some(access_token.to_string());
self.refresh_token = result
.get("refreshToken")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
self.token_expire = result.get("tokenExpireTime").and_then(|v| {
v.as_i64().or_else(|| {
v.as_str().and_then(|s| {
chrono::DateTime::parse_from_rfc3339(s)
.ok()
.map(|dt| dt.timestamp())
})
})
});
Ok(serde_json::from_value(result)?)
} else {
Err(WebullError::SessionExpired)
}
}
pub async fn get_account_id(&mut self) -> Result<String> {
let headers = self.build_req_headers(false, false, true);
let response = self
.client
.get(&self.endpoints.account_id())
.headers(headers)
.timeout(std::time::Duration::from_secs(self.timeout))
.send()
.await?;
let result: Value = response.json().await?;
if let Some(data) = result.get("data").and_then(|v| v.as_array()) {
if let Some(first_account) = data.first() {
if let Some(account_id) = first_account.get("secAccountId") {
let account_id_str = match account_id {
Value::String(s) => s.clone(),
Value::Number(n) => n.to_string(),
_ => return Err(WebullError::AccountNotFound),
};
self.account_id = Some(account_id_str.clone());
return Ok(account_id_str);
}
}
}
Err(WebullError::AccountNotFound)
}
pub async fn get_trade_token(&mut self, password: &str) -> Result<String> {
let hashed_password = hash_password(password);
let data = json!({
"pwd": hashed_password
});
let headers = self.build_req_headers(false, false, true);
let response = self
.client
.post(&self.endpoints.trade_token())
.headers(headers)
.json(&data)
.timeout(std::time::Duration::from_secs(self.timeout))
.send()
.await?;
let result: Value = response.json().await?;
let trade_token = result
.get("tradeToken")
.and_then(|v| v.as_str())
.or_else(|| {
result
.get("data")
.and_then(|d| d.get("tradeToken"))
.and_then(|v| v.as_str())
});
if let Some(token) = trade_token {
self.trade_token = Some(token.to_string());
Ok(token.to_string())
} else {
if let Some(msg) = result.get("msg").and_then(|m| m.as_str()) {
Err(WebullError::AuthenticationError(format!(
"Failed to get trade token: {}",
msg
)))
} else {
Err(WebullError::AuthenticationError(
"Failed to get trade token".to_string(),
))
}
}
}
pub async fn get_account(&self) -> Result<AccountDetail> {
let account_id = self
.account_id
.as_ref()
.ok_or(WebullError::AccountNotFound)?;
let headers = self.build_req_headers(false, false, true);
let response = self
.client
.get(&self.endpoints.account(account_id))
.headers(headers)
.timeout(std::time::Duration::from_secs(self.timeout))
.send()
.await?;
let result: Value = response.json().await?;
let mut account: AccountDetail = serde_json::from_value(result)?;
if let Some(ref members) = account.account_members {
for member in members {
match member.key.as_str() {
"totalMarketValue" => {
account.total_market_value = member.value.parse::<f64>().ok();
}
"cashBalance" => {
account.cash_balance = member.value.parse::<f64>().ok();
}
"dayBuyingPower" | "overnightBuyingPower" => {
if member.key == "dayBuyingPower" {
account.buying_power = member.value.parse::<f64>().ok();
}
}
"unsettledFunds" => {
account.unsettled_funds = member.value.parse::<f64>().ok();
}
_ => {}
}
}
if account.total_cash.is_none() && account.cash_balance.is_some() {
account.total_cash = account.cash_balance;
}
}
Ok(account)
}
pub async fn get_positions(&self) -> Result<Vec<Position>> {
let account_id = self
.account_id
.as_ref()
.ok_or(WebullError::AccountNotFound)?;
let headers = self.build_req_headers(false, false, true);
let url = self.endpoints.account(account_id);
let response = self
.client
.get(&url)
.headers(headers)
.timeout(std::time::Duration::from_secs(self.timeout))
.send()
.await?;
let result: Value = response.json().await?;
if let Some(positions) = result.get("positions") {
match serde_json::from_value::<Vec<Position>>(positions.clone()) {
Ok(parsed) => Ok(parsed),
Err(e) => {
eprintln!("Failed to parse positions: {}", e);
eprintln!(
"Raw positions: {}",
serde_json::to_string_pretty(positions).unwrap_or_default()
);
Ok(Vec::new())
}
}
} else {
Ok(Vec::new())
}
}
pub async fn get_orders(&self, _page_size: Option<i32>) -> Result<Vec<Order>> {
let account_data = self.get_account_raw().await?;
if let Some(open_orders) = account_data.get("openOrders") {
if let Some(orders_array) = open_orders.as_array() {
let mut parsed_orders = Vec::new();
for order_value in orders_array {
let mut order = order_value.clone();
if order.get("statusCode").is_some() {
if let Some(obj) = order.as_object_mut() {
obj.remove("status"); }
}
match serde_json::from_value::<Order>(order) {
Ok(parsed) => parsed_orders.push(parsed),
Err(e) => {
eprintln!("Failed to parse order: {}", e);
}
}
}
Ok(parsed_orders)
} else {
Ok(Vec::new())
}
} else {
Ok(Vec::new())
}
}
async fn get_account_raw(&self) -> Result<Value> {
let account_id = self
.account_id
.as_ref()
.ok_or(WebullError::AccountNotFound)?;
let headers = self.build_req_headers(false, false, true);
let response = self
.client
.get(&self.endpoints.account(account_id))
.headers(headers)
.timeout(std::time::Duration::from_secs(self.timeout))
.send()
.await?;
Ok(response.json().await?)
}
pub async fn get_history_orders(&self, status: &str, count: i32) -> Result<Value> {
let account_id = self
.account_id
.as_ref()
.ok_or(WebullError::AccountNotFound)?;
let headers = self.build_req_headers(true, false, true);
let url = format!("{}{}", self.endpoints.orders(account_id, count), status);
let response = self
.client
.get(&url)
.headers(headers)
.timeout(std::time::Duration::from_secs(self.timeout))
.send()
.await?;
Ok(response.json().await?)
}
pub async fn place_order(&self, order: &PlaceOrderRequest) -> Result<String> {
let account_id = self
.account_id
.as_ref()
.ok_or(WebullError::AccountNotFound)?;
if self.trade_token.is_none() {
return Err(WebullError::TradeTokenNotAvailable);
}
let headers = self.build_req_headers(true, true, true);
let mut order_data = serde_json::to_value(order)?;
order_data["comboType"] = json!("NORMAL");
if order_data.get("serialId").is_none() {
let uuid = uuid::Uuid::new_v4().to_string();
order_data["serialId"] = json!(uuid);
}
match order.order_type {
OrderType::Market => {
order_data["outsideRegularTradingHour"] = json!(false);
}
OrderType::Limit => {
if let Some(limit_price) = order.limit_price {
order_data["lmtPrice"] = json!(limit_price);
}
}
OrderType::Stop => {
if let Some(stop_price) = order.stop_price {
order_data["auxPrice"] = json!(stop_price);
}
}
OrderType::StopLimit => {
if let Some(limit_price) = order.limit_price {
order_data["lmtPrice"] = json!(limit_price);
}
if let Some(stop_price) = order.stop_price {
order_data["auxPrice"] = json!(stop_price);
}
}
}
let response = self
.client
.post(&self.endpoints.place_orders(account_id))
.headers(headers)
.json(&order_data)
.timeout(std::time::Duration::from_secs(self.timeout))
.send()
.await?;
let result: Value = response.json().await?;
let order_id = result
.get("data")
.and_then(|d| d.get("orderId"))
.or_else(|| result.get("orderId"));
if let Some(order_id_val) = order_id {
let order_id_str = match order_id_val {
Value::String(s) => s.clone(),
Value::Number(n) => n.to_string(),
_ => return Err(WebullError::ApiError("Invalid orderId format".to_string())),
};
Ok(order_id_str)
} else {
Err(WebullError::ApiError("Failed to place order".to_string()))
}
}
pub async fn cancel_order(&self, order_id: &str) -> Result<bool> {
let account_id = self
.account_id
.as_ref()
.ok_or(WebullError::AccountNotFound)?;
if self.trade_token.is_none() {
return Err(WebullError::TradeTokenNotAvailable);
}
let headers = self.build_req_headers(true, true, true);
let uuid = Uuid::new_v4();
let url = format!(
"{}{}/{}",
self.endpoints.cancel_order(account_id),
order_id,
uuid
);
let data = json!({});
let response = self
.client
.post(&url)
.headers(headers)
.json(&data)
.timeout(std::time::Duration::from_secs(self.timeout))
.send()
.await?;
if response.status().is_success() {
let result: Value = response.json().await?;
if let Some(code) = result.get("code").and_then(|v| v.as_str()) {
if code == "200" {
return Ok(true);
}
}
if let Some(success) = result.get("success").and_then(|v| v.as_bool()) {
return Ok(success);
}
}
Ok(false)
}
pub async fn get_quotes(&self, ticker_id: &str) -> Result<Quote> {
let headers = self.build_req_headers(false, false, true);
let response = self
.client
.get(&self.endpoints.quotes(ticker_id))
.headers(headers)
.timeout(std::time::Duration::from_secs(self.timeout))
.send()
.await?;
let result: Value = response.json().await?;
Ok(serde_json::from_value(result)?)
}
pub async fn get_bars(
&self,
ticker_id: &str,
interval: &str,
count: i32,
timestamp: Option<i64>,
) -> Result<Vec<Bar>> {
let interval = parse_interval(interval)?;
let headers = self.build_req_headers(false, false, true);
let timestamp = timestamp.unwrap_or_else(|| {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64
});
let url = self
.endpoints
.bars(ticker_id, &interval, count, Some(timestamp));
let response = self
.client
.get(&url)
.headers(headers)
.timeout(std::time::Duration::from_secs(self.timeout))
.send()
.await?;
let result: Value = response.json().await?;
if let Some(result_array) = result.as_array() {
if let Some(first_item) = result_array.first() {
if let Some(data_array) = first_item.get("data").and_then(|v| v.as_array()) {
let mut bars = Vec::new();
for data_str in data_array {
if let Some(s) = data_str.as_str() {
let parts: Vec<&str> = s.split(',').collect();
if parts.len() >= 7 {
let timestamp = parts[0].parse::<i64>().unwrap_or(0);
let open = parts[1].parse::<f64>().unwrap_or(0.0);
let close = parts[2].parse::<f64>().unwrap_or(0.0);
let high = parts[3].parse::<f64>().unwrap_or(0.0);
let low = parts[4].parse::<f64>().unwrap_or(0.0);
let volume = parts[6].parse::<i64>().unwrap_or(0);
let vwap = if parts.len() > 7 && parts[7] != "null" {
parts[7].parse::<f64>().unwrap_or(0.0)
} else {
0.0
};
bars.push(Bar {
open,
high,
low,
close,
volume: volume as f64,
vwap,
timestamp,
});
}
}
}
return Ok(bars);
}
}
}
Ok(Vec::new())
}
pub async fn find_ticker(&self, keyword: &str) -> Result<Vec<Ticker>> {
let headers = self.build_req_headers(false, false, true);
let response = self
.client
.get(&self.endpoints.stock_id(keyword, self.region_code))
.headers(headers)
.timeout(std::time::Duration::from_secs(self.timeout))
.send()
.await?;
let result: Value = response.json().await?;
if let Some(data) = result.get("data") {
Ok(serde_json::from_value(data.clone())?)
} else {
Ok(Vec::new())
}
}
pub async fn get_options(&self, ticker: &str) -> Result<Vec<OptionContract>> {
let headers = self.build_req_headers(false, false, true);
let response = self
.client
.get(&self.endpoints.options(ticker))
.headers(headers)
.timeout(std::time::Duration::from_secs(self.timeout))
.send()
.await?;
let result: Value = response.json().await?;
if let Some(data) = result.get("data") {
Ok(serde_json::from_value(data.clone())?)
} else {
Ok(Vec::new())
}
}
pub async fn get_ticker(&self, symbol: &str) -> Result<String> {
let headers = self.build_req_headers(false, false, true);
let response = self
.client
.get(&self.endpoints.stock_id(symbol, 6))
.headers(headers)
.timeout(std::time::Duration::from_secs(self.timeout))
.send()
.await?;
let result: Value = response.json().await?;
if let Some(data) = result.get("data").and_then(|v| v.as_array()) {
for ticker in data {
if ticker.get("disSymbol").and_then(|v| v.as_str()) == Some(symbol)
|| ticker.get("symbol").and_then(|v| v.as_str()) == Some(symbol)
{
if let Some(ticker_id) = ticker.get("tickerId") {
let ticker_id_str = match ticker_id {
Value::String(s) => s.clone(),
Value::Number(n) => n.to_string(),
_ => continue,
};
return Ok(ticker_id_str);
}
}
}
}
Err(WebullError::TickerNotFound(symbol.to_string()))
}
pub async fn get_news(&self, symbol: &str, last_id: i64, count: i32) -> Result<Vec<News>> {
let ticker_id = self.get_ticker(symbol).await?;
let headers = self.build_req_headers(false, false, true);
let response = self
.client
.get(&self.endpoints.news(&ticker_id, last_id, count))
.headers(headers)
.timeout(std::time::Duration::from_secs(self.timeout))
.send()
.await?;
let result: Vec<News> = response.json().await?;
Ok(result)
}
pub async fn get_fundamentals(&self, ticker: &str) -> Result<Fundamental> {
let headers = self.build_req_headers(false, false, true);
let response = self
.client
.get(&self.endpoints.fundamentals(ticker))
.headers(headers)
.timeout(std::time::Duration::from_secs(self.timeout))
.send()
.await?;
let result: Value = response.json().await?;
Ok(serde_json::from_value(result)?)
}
pub async fn screener(&self, request: &ScreenerRequest) -> Result<Vec<Ticker>> {
let headers = self.build_req_headers(false, false, true);
let response = self
.client
.post(&self.endpoints.screener())
.headers(headers)
.json(request)
.timeout(std::time::Duration::from_secs(self.timeout))
.send()
.await?;
let result: Value = response.json().await?;
if let Some(data) = result.get("data") {
Ok(serde_json::from_value(data.clone())?)
} else {
Ok(Vec::new())
}
}
}