From 6adf2b46c8e9fcb9116276653fc7ccc259243965 Mon Sep 17 00:00:00 2001 From: Nikolaos Karaolidis Date: Wed, 14 Feb 2024 17:38:56 +0000 Subject: [PATCH] Add partial account management Signed-off-by: Nikolaos Karaolidis --- README.md | 4 +- src/config.rs | 1 + src/main.rs | 3 + src/threads/trading/mod.rs | 29 +++++++- src/types/alpaca/api/incoming/account.rs | 91 ++++++++++++++++++++++++ src/types/alpaca/api/incoming/mod.rs | 1 + src/types/alpaca/shared/asset.rs | 2 +- 7 files changed, 126 insertions(+), 5 deletions(-) create mode 100644 src/types/alpaca/api/incoming/account.rs diff --git a/README.md b/README.md index d392c0c..8c25666 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ -# QRust +# qrust -QRust (/kɹʌst/, QuantitativeRust) is an algorithmic trading library written in Rust. +`qrust` (/kɹʌst/, QuantitativeRust) is an algorithmic trading library written in Rust. diff --git a/src/config.rs b/src/config.rs index 9f76124..8e2aa73 100644 --- a/src/config.rs +++ b/src/config.rs @@ -14,6 +14,7 @@ use rust_bert::{ use std::{env, num::NonZeroU32, path::PathBuf, sync::Arc}; use tokio::sync::Mutex; +pub const ALPACA_ACCOUNT_API_URL: &str = "https://api.alpaca.markets/v2/account"; pub const ALPACA_ASSET_API_URL: &str = "https://api.alpaca.markets/v2/assets"; pub const ALPACA_ORDER_API_URL: &str = "https://api.alpaca.markets/v2/orders"; pub const ALPACA_POSITION_API_URL: &str = "https://api.alpaca.markets/v2/positions"; diff --git a/src/main.rs b/src/main.rs index e00bde9..0bf5aeb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,6 +33,9 @@ async fn main() { ) .unwrap(); + threads::trading::check_account(&config).await; + threads::trading::check_positions(&config).await; + spawn(threads::trading::run(config.clone())); let (data_sender, data_receiver) = mpsc::channel::(100); diff --git a/src/threads/trading/mod.rs b/src/threads/trading/mod.rs index 449cf96..a68ab1c 100644 --- a/src/threads/trading/mod.rs +++ b/src/threads/trading/mod.rs @@ -22,11 +22,36 @@ pub async fn run(config: Arc) { alpaca::websocket::trading::subscribe(&mut websocket_sink, &mut websocket_stream).await; rehydrate(&config).await; - check_positions(&config).await; - spawn(websocket::run(config, websocket_stream, websocket_sink)); } +pub async fn check_account(config: &Arc) { + let account = alpaca::api::incoming::account::get(config, None) + .await + .unwrap(); + + assert!( + !(account.status != alpaca::api::incoming::account::Status::Active), + "Account status is not active: {:?}.", + account.status + ); + assert!( + !account.trade_suspend_by_user, + "Account trading is suspended by user." + ); + assert!(!account.trading_blocked, "Account trading is blocked."); + assert!(!account.blocked, "Account is blocked."); + + if account.cash == 0.0 { + warn!("Account cash is zero, qrust will not be able to trade."); + } + + warn!( + "qrust active with {}{}, avoid transferring funds without shutting down.", + account.currency, account.cash + ); +} + pub async fn check_positions(config: &Arc) { let positions_future = async { alpaca::api::incoming::position::get(config, None) diff --git a/src/types/alpaca/api/incoming/account.rs b/src/types/alpaca/api/incoming/account.rs new file mode 100644 index 0000000..c84af77 --- /dev/null +++ b/src/types/alpaca/api/incoming/account.rs @@ -0,0 +1,91 @@ +use backoff::{future::retry_notify, ExponentialBackoff}; +use log::warn; +use reqwest::Error; +use serde::Deserialize; +use std::{sync::Arc, time::Duration}; +use time::OffsetDateTime; +use uuid::Uuid; + +use crate::config::{Config, ALPACA_ACCOUNT_API_URL}; + +#[derive(Deserialize, Debug, PartialEq, Eq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum Status { + Onboarding, + SubmissionFailed, + Submitted, + AccountUpdated, + ApprovalPending, + Active, + Rejected, +} + +#[derive(Deserialize)] +#[allow(clippy::struct_excessive_bools)] +pub struct Account { + pub id: Uuid, + #[serde(rename = "account_number")] + pub number: i64, + pub status: Status, + pub currency: String, + pub cash: f64, + pub non_marginable_buying_power: f64, + pub accrued_fees: f64, + pub pending_transfer_in: f64, + pub pending_transfer_out: f64, + pub pattern_day_trader: bool, + pub trade_suspend_by_user: bool, + pub trading_blocked: bool, + pub transfers_blocked: bool, + #[serde(rename = "account_blocked")] + pub blocked: bool, + #[serde(with = "time::serde::rfc3339")] + pub created_at: OffsetDateTime, + pub shorting_enabled: bool, + pub long_market_value: f64, + pub short_market_value: f64, + pub equity: f64, + pub last_equity: f64, + pub multiplier: i8, + pub buying_power: f64, + pub initial_margin: f64, + pub maintenance_margin: f64, + pub sma: f64, + pub daytrade_count: i64, + pub last_maintenance_margin: f64, + pub daytrading_buying_power: f64, + pub regt_buying_power: f64, +} + +pub async fn get( + config: &Arc, + backoff: Option, +) -> Result { + retry_notify( + backoff.unwrap_or_default(), + || async { + config.alpaca_rate_limit.until_ready().await; + config + .alpaca_client + .get(ALPACA_ACCOUNT_API_URL) + .send() + .await? + .error_for_status() + .map_err(|e| match e.status() { + Some(reqwest::StatusCode::FORBIDDEN) => backoff::Error::Permanent(e), + _ => e.into(), + })? + .json::() + .await + .map_err(backoff::Error::Permanent) + }, + |e, duration: Duration| { + warn!( + "Failed to get account, will retry in {} seconds: {}", + duration.as_secs(), + e + ); + }, + ) + .await +} diff --git a/src/types/alpaca/api/incoming/mod.rs b/src/types/alpaca/api/incoming/mod.rs index e7c00c6..6bac836 100644 --- a/src/types/alpaca/api/incoming/mod.rs +++ b/src/types/alpaca/api/incoming/mod.rs @@ -1,3 +1,4 @@ +pub mod account; pub mod asset; pub mod bar; pub mod clock; diff --git a/src/types/alpaca/shared/asset.rs b/src/types/alpaca/shared/asset.rs index 76f4d95..735576e 100644 --- a/src/types/alpaca/shared/asset.rs +++ b/src/types/alpaca/shared/asset.rs @@ -11,7 +11,7 @@ pub enum Class { impl_from_enum!(types::Class, Class, UsEquity, Crypto); #[derive(Deserialize)] -#[serde(rename_all = "UPPERCASE")] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum Exchange { Amex, Arca,