Add order types

Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2024-02-12 16:45:11 +00:00
parent dee21d5324
commit 5961717520
47 changed files with 840 additions and 145 deletions

1
Cargo.lock generated
View File

@@ -2573,6 +2573,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a"
dependencies = [
"getrandom",
"serde",
]
[[package]]

View File

@@ -39,7 +39,9 @@ clickhouse = { version = "0.11.6", features = [
"time",
"uuid",
] }
uuid = "1.6.1"
uuid = { version = "1.6.1", features = [
"serde",
] }
time = { version = "0.3.31", features = [
"serde",
"formatting",

View File

@@ -1,4 +1,4 @@
use crate::types::alpaca::Source;
use crate::types::alpaca::shared::Source;
use governor::{DefaultDirectRateLimiter, Quota, RateLimiter};
use reqwest::{
header::{HeaderMap, HeaderName, HeaderValue},
@@ -15,7 +15,9 @@ use std::{env, num::NonZeroU32, path::PathBuf, sync::Arc};
use tokio::sync::Mutex;
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_CLOCK_API_URL: &str = "https://api.alpaca.markets/v2/clock";
pub const ALPACA_STOCK_DATA_URL: &str = "https://data.alpaca.markets/v2/stocks/bars";
pub const ALPACA_CRYPTO_DATA_URL: &str = "https://data.alpaca.markets/v1beta3/crypto/us/bars";
pub const ALPACA_NEWS_DATA_URL: &str = "https://data.alpaca.markets/v1beta1/news";

View File

@@ -2,3 +2,4 @@ pub mod assets;
pub mod backfills;
pub mod bars;
pub mod news;
pub mod orders;

0
src/database/orders.rs Normal file
View File

View File

@@ -6,7 +6,7 @@ use crate::{
alpaca::{
self,
api::{self, outgoing::Sort},
Source,
shared::Source,
},
news::Prediction,
Backfill, Bar, Class, News,
@@ -248,7 +248,7 @@ impl Handler for BarHandler {
async fn backfill(&self, symbol: String, fetch_from: OffsetDateTime, fetch_to: OffsetDateTime) {
info!("Backfilling bars for {}.", symbol);
let mut bars = Vec::new();
let mut bars = vec![];
let mut next_page_token = None;
loop {
@@ -348,7 +348,7 @@ impl Handler for NewsHandler {
async fn backfill(&self, symbol: String, fetch_from: OffsetDateTime, fetch_to: OffsetDateTime) {
info!("Backfilling news for {}.", symbol);
let mut news = Vec::new();
let mut news = vec![];
let mut next_page_token = None;
loop {

View File

@@ -8,7 +8,7 @@ use crate::{
},
create_send_await, database,
types::{alpaca, Asset, Class},
utils::{authenticate, backoff, cleanup},
utils::{backoff, cleanup},
};
use futures_util::{future::join_all, StreamExt};
use itertools::{Either, Itertools};
@@ -19,7 +19,7 @@ use tokio::{
};
use tokio_tungstenite::connect_async;
#[derive(Clone)]
#[derive(Clone, Copy)]
pub enum Action {
Add,
Remove,
@@ -45,7 +45,7 @@ impl Message {
}
}
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy)]
pub enum ThreadType {
Bars(Class),
News,
@@ -107,7 +107,8 @@ async fn init_thread(
let (websocket, _) = connect_async(websocket_url).await.unwrap();
let (mut websocket_sink, mut websocket_stream) = websocket.split();
authenticate(&config, &mut websocket_sink, &mut websocket_stream).await;
alpaca::websocket::data::authenticate(&config, &mut websocket_sink, &mut websocket_stream)
.await;
let (backfill_sender, backfill_receiver) = mpsc::channel(100);
spawn(backfill::run(
@@ -160,14 +161,14 @@ async fn handle_message(
create_send_await!(
bars_us_equity_websocket_sender,
websocket::Message::new,
message.action.clone().into(),
message.action.into(),
us_equity_symbols.clone()
);
create_send_await!(
bars_us_equity_backfill_sender,
backfill::Message::new,
message.action.clone().into(),
message.action.into(),
us_equity_symbols
);
};
@@ -180,14 +181,14 @@ async fn handle_message(
create_send_await!(
bars_crypto_websocket_sender,
websocket::Message::new,
message.action.clone().into(),
message.action.into(),
crypto_symbols.clone()
);
create_send_await!(
bars_crypto_backfill_sender,
backfill::Message::new,
message.action.clone().into(),
message.action.into(),
crypto_symbols
);
};
@@ -196,14 +197,14 @@ async fn handle_message(
create_send_await!(
news_websocket_sender,
websocket::Message::new,
message.action.clone().into(),
message.action.into(),
symbols.clone()
);
create_send_await!(
news_backfill_sender,
backfill::Message::new,
message.action.clone().into(),
message.action.into(),
symbols.clone()
);
};

View File

@@ -66,11 +66,11 @@ pub trait Handler: Send + Sync {
fn create_subscription_message(
&self,
symbols: Vec<String>,
) -> websocket::outgoing::subscribe::Message;
) -> websocket::data::outgoing::subscribe::Message;
async fn handle_parsed_websocket_message(
&self,
pending: Arc<RwLock<Pending>>,
message: websocket::incoming::Message,
message: websocket::data::incoming::Message,
);
}
@@ -138,7 +138,7 @@ async fn handle_message(
.lock()
.await
.send(tungstenite::Message::Text(
to_string(&websocket::outgoing::Message::Subscribe(
to_string(&websocket::data::outgoing::Message::Subscribe(
handler.create_subscription_message(message.symbols),
))
.unwrap(),
@@ -168,7 +168,7 @@ async fn handle_message(
.lock()
.await
.send(tungstenite::Message::Text(
to_string(&websocket::outgoing::Message::Unsubscribe(
to_string(&websocket::data::outgoing::Message::Unsubscribe(
handler.create_subscription_message(message.symbols.clone()),
))
.unwrap(),
@@ -191,7 +191,7 @@ async fn handle_websocket_message(
) {
match message {
tungstenite::Message::Text(message) => {
let message = from_str::<Vec<websocket::incoming::Message>>(&message);
let message = from_str::<Vec<websocket::data::incoming::Message>>(&message);
if let Ok(message) = message {
for message in message {
@@ -222,7 +222,8 @@ async fn handle_websocket_message(
struct BarsHandler {
config: Arc<Config>,
subscription_message_constructor: fn(Vec<String>) -> websocket::outgoing::subscribe::Message,
subscription_message_constructor:
fn(Vec<String>) -> websocket::data::outgoing::subscribe::Message,
}
#[async_trait]
@@ -230,19 +231,20 @@ impl Handler for BarsHandler {
fn create_subscription_message(
&self,
symbols: Vec<String>,
) -> websocket::outgoing::subscribe::Message {
) -> websocket::data::outgoing::subscribe::Message {
(self.subscription_message_constructor)(symbols)
}
async fn handle_parsed_websocket_message(
&self,
pending: Arc<RwLock<Pending>>,
message: websocket::incoming::Message,
message: websocket::data::incoming::Message,
) {
match message {
websocket::incoming::Message::Subscription(message) => {
let websocket::incoming::subscription::Message::Market { bars: symbols, .. } =
message
websocket::data::incoming::Message::Subscription(message) => {
let websocket::data::incoming::subscription::Message::Market {
bars: symbols, ..
} = message
else {
unreachable!()
};
@@ -283,8 +285,8 @@ impl Handler for BarsHandler {
}
}
}
websocket::incoming::Message::Bar(message)
| websocket::incoming::Message::UpdatedBar(message) => {
websocket::data::incoming::Message::Bar(message)
| websocket::data::incoming::Message::UpdatedBar(message) => {
let bar = Bar::from(message);
debug!("Received bar for {}: {}.", bar.symbol, bar.time);
@@ -292,15 +294,15 @@ impl Handler for BarsHandler {
.await
.unwrap();
}
websocket::incoming::Message::Status(message) => {
websocket::data::incoming::Message::Status(message) => {
debug!(
"Received status message for {}: {}.",
message.symbol, message.status_message
);
match message.status {
websocket::incoming::status::Status::TradingHalt
| websocket::incoming::status::Status::VolatilityTradingPause => {
websocket::data::incoming::status::Status::TradingHalt
| websocket::data::incoming::status::Status::VolatilityTradingPause => {
database::assets::update_status_where_symbol(
&self.config.clickhouse_client,
&message.symbol,
@@ -309,8 +311,8 @@ impl Handler for BarsHandler {
.await
.unwrap();
}
websocket::incoming::status::Status::Resume
| websocket::incoming::status::Status::TradingResumption => {
websocket::data::incoming::status::Status::Resume
| websocket::data::incoming::status::Status::TradingResumption => {
database::assets::update_status_where_symbol(
&self.config.clickhouse_client,
&message.symbol,
@@ -322,7 +324,7 @@ impl Handler for BarsHandler {
_ => {}
}
}
websocket::incoming::Message::Error(message) => {
websocket::data::incoming::Message::Error(message) => {
error!("Received error message: {}.", message.message);
}
_ => unreachable!(),
@@ -339,18 +341,19 @@ impl Handler for NewsHandler {
fn create_subscription_message(
&self,
symbols: Vec<String>,
) -> websocket::outgoing::subscribe::Message {
websocket::outgoing::subscribe::Message::new_news(symbols)
) -> websocket::data::outgoing::subscribe::Message {
websocket::data::outgoing::subscribe::Message::new_news(symbols)
}
async fn handle_parsed_websocket_message(
&self,
pending: Arc<RwLock<Pending>>,
message: websocket::incoming::Message,
message: websocket::data::incoming::Message,
) {
match message {
websocket::incoming::Message::Subscription(message) => {
let websocket::incoming::subscription::Message::News { news: symbols } = message
websocket::data::incoming::Message::Subscription(message) => {
let websocket::data::incoming::subscription::Message::News { news: symbols } =
message
else {
unreachable!()
};
@@ -396,7 +399,7 @@ impl Handler for NewsHandler {
}
}
}
websocket::incoming::Message::News(message) => {
websocket::data::incoming::Message::News(message) => {
let news = News::from(message);
debug!(
@@ -426,7 +429,7 @@ impl Handler for NewsHandler {
.await
.unwrap();
}
websocket::incoming::Message::Error(message) => {
websocket::data::incoming::Message::Error(message) => {
error!("Received error message: {}.", message.message);
}
_ => unreachable!(),
@@ -439,12 +442,12 @@ pub fn create_handler(thread_type: ThreadType, config: Arc<Config>) -> Box<dyn H
ThreadType::Bars(Class::UsEquity) => Box::new(BarsHandler {
config,
subscription_message_constructor:
websocket::outgoing::subscribe::Message::new_market_us_equity,
websocket::data::outgoing::subscribe::Message::new_market_us_equity,
}),
ThreadType::Bars(Class::Crypto) => Box::new(BarsHandler {
config,
subscription_message_constructor:
websocket::outgoing::subscribe::Message::new_market_crypto,
websocket::data::outgoing::subscribe::Message::new_market_crypto,
}),
ThreadType::News => Box::new(NewsHandler { config }),
}

View File

@@ -1,2 +1,3 @@
pub mod clock;
pub mod data;
pub mod trading;

View File

View File

@@ -1,68 +1,21 @@
use crate::{
config::{Config, ALPACA_ASSET_API_URL},
impl_from_enum, types,
types::{
self,
alpaca::shared::asset::{Class, Exchange, Status},
},
};
use backoff::{future::retry_notify, ExponentialBackoff};
use log::warn;
use reqwest::Error;
use serde::Deserialize;
use std::{sync::Arc, time::Duration};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Class {
UsEquity,
Crypto,
}
impl_from_enum!(types::Class, Class, UsEquity, Crypto);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum Exchange {
Amex,
Arca,
Bats,
Nyse,
Nasdaq,
Nysearca,
Otc,
Crypto,
}
impl_from_enum!(
types::Exchange,
Exchange,
Amex,
Arca,
Bats,
Nyse,
Nasdaq,
Nysearca,
Otc,
Crypto
);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Status {
Active,
Inactive,
}
impl From<Status> for bool {
fn from(status: Status) -> Self {
match status {
Status::Active => true,
Status::Inactive => false,
}
}
}
use uuid::Uuid;
#[allow(clippy::struct_excessive_bools)]
#[derive(Clone, Debug, PartialEq, Deserialize)]
#[derive(Deserialize)]
pub struct Asset {
pub id: String,
pub id: Uuid,
pub class: Class,
pub exchange: Exchange,
pub symbol: String,
@@ -78,12 +31,12 @@ pub struct Asset {
}
impl From<Asset> for types::Asset {
fn from(item: Asset) -> Self {
fn from(asset: Asset) -> Self {
Self {
symbol: item.symbol,
class: item.class.into(),
exchange: item.exchange.into(),
status: item.status.into(),
symbol: asset.symbol,
class: asset.class.into(),
exchange: asset.exchange.into(),
status: asset.status.into(),
time_added: time::OffsetDateTime::now_utc(),
}
}

View File

@@ -9,7 +9,7 @@ use serde::Deserialize;
use std::{collections::HashMap, sync::Arc, time::Duration};
use time::OffsetDateTime;
#[derive(Clone, Debug, PartialEq, Deserialize)]
#[derive(Deserialize)]
pub struct Bar {
#[serde(rename = "t")]
#[serde(with = "time::serde::rfc3339")]
@@ -46,7 +46,7 @@ impl From<(Bar, String)> for types::Bar {
}
}
#[derive(Clone, Debug, PartialEq, Deserialize)]
#[derive(Deserialize)]
pub struct Message {
pub bars: HashMap<String, Vec<Bar>>,
pub next_page_token: Option<String>,

View File

@@ -6,7 +6,7 @@ use serde::Deserialize;
use std::{sync::Arc, time::Duration};
use time::OffsetDateTime;
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
#[derive(Deserialize)]
pub struct Clock {
#[serde(with = "time::serde::rfc3339")]
pub timestamp: OffsetDateTime,

View File

@@ -2,3 +2,4 @@ pub mod asset;
pub mod bar;
pub mod clock;
pub mod news;
pub mod order;

View File

@@ -10,21 +10,21 @@ use serde::Deserialize;
use std::{sync::Arc, time::Duration};
use time::OffsetDateTime;
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
#[derive(Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ImageSize {
Thumb,
Small,
Large,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
#[derive(Deserialize)]
pub struct Image {
pub size: ImageSize,
pub url: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
#[derive(Deserialize)]
pub struct News {
pub id: i64,
#[serde(with = "time::serde::rfc3339")]
@@ -66,7 +66,7 @@ impl From<News> for types::News {
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
#[derive(Deserialize)]
pub struct Message {
pub news: Vec<News>,
pub next_page_token: Option<String>,

View File

@@ -0,0 +1,45 @@
use crate::{
config::{Config, ALPACA_ORDER_API_URL},
types::alpaca::{api::outgoing, shared},
};
use backoff::{future::retry_notify, ExponentialBackoff};
use log::warn;
use reqwest::Error;
use std::{sync::Arc, time::Duration};
pub use shared::order::Order;
pub async fn get(
config: &Arc<Config>,
query: &outgoing::order::Order,
backoff: Option<ExponentialBackoff>,
) -> Result<Vec<Order>, Error> {
retry_notify(
backoff.unwrap_or_default(),
|| async {
config.alpaca_rate_limit.until_ready().await;
config
.alpaca_client
.get(ALPACA_ORDER_API_URL)
.query(query)
.send()
.await?
.error_for_status()
.map_err(|e| match e.status() {
Some(reqwest::StatusCode::FORBIDDEN) => backoff::Error::Permanent(e),
_ => e.into(),
})?
.json::<Vec<Order>>()
.await
.map_err(backoff::Error::Permanent)
},
|e, duration: Duration| {
warn!(
"Failed to get orders, will retry in {} seconds: {}",
duration.as_secs(),
e
);
},
)
.await
}

View File

@@ -1,5 +1,5 @@
use super::{serialize_symbols, Sort};
use crate::types::alpaca::Source;
use crate::types::alpaca::shared::Source;
use serde::Serialize;
use std::time::Duration;
use time::OffsetDateTime;

View File

@@ -1,10 +1,11 @@
pub mod bar;
pub mod news;
pub mod order;
use serde::{Serialize, Serializer};
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
#[serde(rename_all = "snake_case")]
#[allow(dead_code)]
pub enum Sort {
Asc,
@@ -18,3 +19,16 @@ where
let string = symbols.join(",");
serializer.serialize_str(&string)
}
fn serialize_symbols_option<S>(
symbols: &Option<Vec<String>>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match symbols {
Some(symbols) => serialize_symbols(symbols, serializer),
None => serializer.serialize_none(),
}
}

View File

@@ -0,0 +1,35 @@
use super::{serialize_symbols_option, Sort};
use crate::types::alpaca::shared::order::Side;
use serde::Serialize;
use time::OffsetDateTime;
#[derive(Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Status {
Open,
Closed,
All,
}
#[derive(Serialize)]
pub struct Order {
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<Status>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(with = "time::serde::rfc3339::option")]
pub after: Option<OffsetDateTime>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(with = "time::serde::rfc3339::option")]
pub until: Option<OffsetDateTime>,
#[serde(skip_serializing_if = "Option::is_none")]
pub direction: Option<Sort>,
#[serde(skip_serializing_if = "Option::is_none")]
pub nested: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(serialize_with = "serialize_symbols_option")]
pub symbols: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub side: Option<Side>,
}

View File

@@ -1,5 +1,3 @@
pub mod api;
pub mod source;
pub mod shared;
pub mod websocket;
pub use source::Source;

View File

@@ -0,0 +1,53 @@
use crate::{impl_from_enum, types};
use serde::Deserialize;
#[derive(Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum Class {
UsEquity,
Crypto,
}
impl_from_enum!(types::Class, Class, UsEquity, Crypto);
#[derive(Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum Exchange {
Amex,
Arca,
Bats,
Nyse,
Nasdaq,
Nysearca,
Otc,
Crypto,
}
impl_from_enum!(
types::Exchange,
Exchange,
Amex,
Arca,
Bats,
Nyse,
Nasdaq,
Nysearca,
Otc,
Crypto
);
#[derive(Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Status {
Active,
Inactive,
}
impl From<Status> for bool {
fn from(status: Status) -> Self {
match status {
Status::Active => true,
Status::Inactive => false,
}
}
}

View File

@@ -0,0 +1,5 @@
pub mod asset;
pub mod order;
pub mod source;
pub use source::Source;

View File

@@ -0,0 +1,214 @@
use crate::{impl_from_enum, types};
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use uuid::Uuid;
#[derive(Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum Class {
#[serde(alias = "")]
Simple,
Bracket,
Oco,
Oto,
}
impl_from_enum!(types::order::Class, Class, Simple, Bracket, Oco, Oto);
#[derive(Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum Type {
Market,
Limit,
Stop,
StopLimit,
TrailingStop,
}
impl_from_enum!(
types::order::Type,
Type,
Market,
Limit,
Stop,
StopLimit,
TrailingStop
);
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum Side {
Buy,
Sell,
}
impl_from_enum!(types::order::Side, Side, Buy, Sell);
#[derive(Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum TimeInForce {
Day,
Gtc,
Opg,
Cls,
Ioc,
Fok,
}
impl_from_enum!(
types::order::TimeInForce,
TimeInForce,
Day,
Gtc,
Opg,
Cls,
Ioc,
Fok
);
#[derive(Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum Status {
New,
PartiallyFilled,
Filled,
DoneForDay,
Canceled,
Expired,
Replaced,
PendingCancel,
PendingReplace,
Accepted,
PendingNew,
AcceptedForBidding,
Stopped,
Rejected,
Suspended,
Calculated,
}
impl_from_enum!(
types::order::Status,
Status,
New,
PartiallyFilled,
Filled,
DoneForDay,
Canceled,
Expired,
Replaced,
PendingCancel,
PendingReplace,
Accepted,
PendingNew,
AcceptedForBidding,
Stopped,
Rejected,
Suspended,
Calculated
);
#[derive(Deserialize, Clone, Debug, PartialEq)]
#[allow(clippy::struct_field_names)]
pub struct Order {
pub id: Uuid,
pub client_order_id: Uuid,
#[serde(with = "time::serde::rfc3339")]
pub created_at: OffsetDateTime,
#[serde(with = "time::serde::rfc3339::option")]
pub updated_at: Option<OffsetDateTime>,
#[serde(with = "time::serde::rfc3339")]
pub submitted_at: OffsetDateTime,
#[serde(with = "time::serde::rfc3339::option")]
pub filled_at: Option<OffsetDateTime>,
#[serde(with = "time::serde::rfc3339::option")]
pub expired_at: Option<OffsetDateTime>,
#[serde(with = "time::serde::rfc3339::option")]
pub cancel_requested_at: Option<OffsetDateTime>,
#[serde(with = "time::serde::rfc3339::option")]
pub canceled_at: Option<OffsetDateTime>,
#[serde(with = "time::serde::rfc3339::option")]
pub failed_at: Option<OffsetDateTime>,
#[serde(with = "time::serde::rfc3339::option")]
pub replaced_at: Option<OffsetDateTime>,
pub replaced_by: Option<Uuid>,
pub replaces: Option<Uuid>,
pub asset_id: Uuid,
pub symbol: String,
pub asset_class: super::asset::Class,
pub notional: Option<f64>,
pub qty: Option<f64>,
pub filled_qty: f64,
pub filled_avg_price: Option<f64>,
pub order_class: Class,
#[serde(rename = "type")]
pub order_type: Type,
pub side: Side,
pub time_in_force: TimeInForce,
pub limit_price: Option<f64>,
pub stop_price: Option<f64>,
pub status: Status,
pub extended_hours: bool,
pub legs: Option<Vec<Order>>,
pub trail_percent: Option<f64>,
pub trail_price: Option<f64>,
pub hwm: Option<f64>,
}
impl From<Order> for types::Order {
fn from(order: Order) -> Self {
Self {
id: order.id,
client_order_id: order.client_order_id,
time_submitted: order.submitted_at,
time_created: order.created_at,
time_updated: order.updated_at.unwrap_or(OffsetDateTime::UNIX_EPOCH),
time_filled: order.filled_at.unwrap_or(OffsetDateTime::UNIX_EPOCH),
time_expired: order.expired_at.unwrap_or(OffsetDateTime::UNIX_EPOCH),
time_cancel_requested: order
.cancel_requested_at
.unwrap_or(OffsetDateTime::UNIX_EPOCH),
time_canceled: order.canceled_at.unwrap_or(OffsetDateTime::UNIX_EPOCH),
time_failed: order.failed_at.unwrap_or(OffsetDateTime::UNIX_EPOCH),
time_replaced: order.replaced_at.unwrap_or(OffsetDateTime::UNIX_EPOCH),
replaced_by: order.replaced_by.unwrap_or_default(),
replaces: order.replaces.unwrap_or_default(),
symbol: order.symbol,
order_class: order.order_class.into(),
order_type: order.order_type.into(),
side: order.side.into(),
time_in_force: order.time_in_force.into(),
notional: order.notional.unwrap_or_default(),
qty: order.qty.unwrap_or_default(),
filled_qty: order.filled_qty,
filled_avg_price: order.filled_avg_price.unwrap_or_default(),
status: order.status.into(),
extended_hours: order.extended_hours,
limit_price: order.limit_price.unwrap_or_default(),
stop_price: order.stop_price.unwrap_or_default(),
trail_percent: order.trail_percent.unwrap_or_default(),
trail_price: order.trail_price.unwrap_or_default(),
hwm: order.hwm.unwrap_or_default(),
legs: order
.legs
.unwrap_or_default()
.into_iter()
.map(|order| order.id)
.collect(),
}
}
}
impl Order {
pub fn normalize(self) -> Vec<types::Order> {
let mut orders = vec![self.clone().into()];
if let Some(legs) = self.legs {
for leg in legs {
orders.extend(leg.normalize());
}
}
orders
}
}

View File

@@ -5,7 +5,7 @@ use std::{
};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(rename_all = "snake_case")]
pub enum Source {
Iex,
Sip,

View File

@@ -2,7 +2,7 @@ use crate::types;
use serde::Deserialize;
use time::OffsetDateTime;
#[derive(Clone, Debug, PartialEq, Deserialize)]
#[derive(Deserialize, Debug, PartialEq)]
pub struct Message {
#[serde(rename = "t")]
#[serde(with = "time::serde::rfc3339")]

View File

@@ -1,7 +1,6 @@
use serde::Deserialize;
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
#[derive(Deserialize, Debug, PartialEq, Eq)]
pub struct Message {
pub code: u16,
#[serde(rename = "msg")]

View File

@@ -7,7 +7,7 @@ pub mod success;
use serde::Deserialize;
#[derive(Clone, Debug, PartialEq, Deserialize)]
#[derive(Deserialize, Debug, PartialEq)]
#[serde(tag = "T")]
pub enum Message {
#[serde(rename = "success")]

View File

@@ -5,7 +5,7 @@ use crate::{
use serde::Deserialize;
use time::OffsetDateTime;
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
#[derive(Deserialize, Debug, PartialEq, Eq)]
pub struct Message {
pub id: i64,
#[serde(with = "time::serde::rfc3339")]

View File

@@ -1,7 +1,7 @@
use serde::Deserialize;
use time::OffsetDateTime;
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
#[derive(Deserialize, Debug, PartialEq, Eq)]
pub enum Status {
#[serde(rename = "2")]
#[serde(alias = "H")]
@@ -36,7 +36,7 @@ pub enum Status {
VolatilityTradingPause,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
#[derive(Deserialize, Debug, PartialEq, Eq)]
#[serde(tag = "rc", content = "rm")]
pub enum Reason {
#[serde(rename = "D")]
@@ -125,7 +125,7 @@ pub enum Reason {
MarketWideCircuitBreakerResumption,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
#[derive(Deserialize, Debug, PartialEq, Eq)]
pub enum Tape {
A,
B,
@@ -133,7 +133,7 @@ pub enum Tape {
O,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
#[derive(Deserialize, Debug, PartialEq, Eq)]
#[allow(clippy::struct_field_names)]
pub struct Message {
#[serde(rename = "t")]

View File

@@ -1,6 +1,6 @@
use serde::Deserialize;
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
#[derive(Deserialize, Debug, PartialEq, Eq)]
#[serde(untagged)]
pub enum Message {
#[serde(rename_all = "camelCase")]
@@ -15,6 +15,7 @@ pub enum Message {
lulds: Option<Vec<String>>,
cancel_errors: Option<Vec<String>>,
},
#[serde(rename_all = "camelCase")]
News { news: Vec<String> },
News {
news: Vec<String>,
},
}

View File

@@ -1,8 +1,8 @@
use serde::Deserialize;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
#[derive(Deserialize, Debug, PartialEq, Eq)]
#[serde(tag = "msg")]
#[serde(rename_all = "camelCase")]
#[serde(rename_all = "snake_case")]
pub enum Message {
Connected,
Authenticated,

View File

@@ -1,3 +1,6 @@
pub mod incoming;
pub mod outgoing;
use crate::{config::Config, types::alpaca::websocket};
use core::panic;
use futures_util::{
@@ -16,18 +19,18 @@ pub async fn authenticate(
) {
match stream.next().await.unwrap().unwrap() {
Message::Text(data)
if from_str::<Vec<websocket::incoming::Message>>(&data)
if from_str::<Vec<websocket::data::incoming::Message>>(&data)
.unwrap()
.first()
== Some(&websocket::incoming::Message::Success(
websocket::incoming::success::Message::Connected,
== Some(&websocket::data::incoming::Message::Success(
websocket::data::incoming::success::Message::Connected,
)) => {}
_ => panic!("Failed to connect to Alpaca websocket."),
}
sink.send(Message::Text(
to_string(&websocket::outgoing::Message::Auth(
websocket::outgoing::auth::Message {
to_string(&websocket::data::outgoing::Message::Auth(
websocket::auth::Message {
key: config.alpaca_api_key.clone(),
secret: config.alpaca_api_secret.clone(),
},
@@ -39,11 +42,11 @@ pub async fn authenticate(
match stream.next().await.unwrap().unwrap() {
Message::Text(data)
if from_str::<Vec<websocket::incoming::Message>>(&data)
if from_str::<Vec<websocket::data::incoming::Message>>(&data)
.unwrap()
.first()
== Some(&websocket::incoming::Message::Success(
websocket::incoming::success::Message::Authenticated,
== Some(&websocket::data::incoming::Message::Success(
websocket::data::incoming::success::Message::Authenticated,
)) => {}
_ => panic!("Failed to authenticate with Alpaca websocket."),
};

View File

@@ -1,11 +1,11 @@
pub mod auth;
pub mod subscribe;
use crate::types::alpaca::websocket::auth;
use serde::Serialize;
#[derive(Serialize)]
#[serde(tag = "action")]
#[serde(rename_all = "camelCase")]
#[serde(rename_all = "snake_case")]
pub enum Message {
Auth(auth::Message),
Subscribe(subscribe::Message),

View File

@@ -21,7 +21,6 @@ pub enum Market {
#[serde(untagged)]
pub enum Message {
Market(Market),
#[serde(rename_all = "camelCase")]
News {
news: Vec<String>,
},

View File

@@ -1,2 +1,3 @@
pub mod incoming;
pub mod outgoing;
pub mod auth;
pub mod data;
pub mod trading;

View File

@@ -0,0 +1,22 @@
use serde::Deserialize;
#[derive(Deserialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum Status {
Authorized,
Unauthorized,
}
#[derive(Deserialize, Debug, PartialEq, Eq)]
pub enum Action {
#[serde(rename = "authenticate")]
Auth,
#[serde(rename = "listen")]
Subscribe,
}
#[derive(Deserialize, Debug, PartialEq, Eq)]
pub struct Message {
pub status: Status,
pub action: Action,
}

View File

@@ -0,0 +1,16 @@
pub mod auth;
pub mod order;
pub mod subscription;
use serde::Deserialize;
#[derive(Deserialize, Debug, PartialEq)]
#[serde(tag = "stream", content = "data")]
pub enum Message {
#[serde(rename = "authorization")]
Auth(auth::Message),
#[serde(rename = "listening")]
Subscription(subscription::Message),
#[serde(rename = "trade_updates")]
Order(order::Message),
}

View File

@@ -0,0 +1,86 @@
use crate::types::alpaca::shared;
use serde::Deserialize;
use time::OffsetDateTime;
use uuid::Uuid;
pub use shared::order::Order;
#[derive(Deserialize, Debug, PartialEq)]
#[serde(rename_all = "snake_case")]
#[serde(tag = "event")]
pub enum Message {
New {
execution_id: Uuid,
order: Order,
},
Fill {
execution_id: Uuid,
order: Order,
timestamp: OffsetDateTime,
position_qty: f64,
price: f64,
},
PartialFill {
execution_id: Uuid,
order: Order,
timestamp: OffsetDateTime,
position_qty: f64,
price: f64,
},
Canceled {
execution_id: Uuid,
order: Order,
timestamp: OffsetDateTime,
},
Expired {
execution_id: Uuid,
order: Order,
timestamp: OffsetDateTime,
},
DoneForDay {
execution_id: Uuid,
order: Order,
},
Replaced {
execution_id: Uuid,
order: Order,
timestamp: OffsetDateTime,
},
Rejected {
execution_id: Uuid,
order: Order,
timestamp: OffsetDateTime,
},
PendingNew {
execution_id: Uuid,
order: Order,
},
Stopped {
execution_id: Uuid,
order: Order,
},
PendingCancel {
execution_id: Uuid,
order: Order,
},
PendingReplace {
execution_id: Uuid,
order: Order,
},
Calculated {
execution_id: Uuid,
order: Order,
},
Suspended {
execution_id: Uuid,
order: Order,
},
OrderReplaceRejected {
execution_id: Uuid,
order: Order,
},
OrderCancelRejected {
execution_id: Uuid,
order: Order,
},
}

View File

@@ -0,0 +1,6 @@
use serde::Deserialize;
#[derive(Deserialize, Debug, PartialEq, Eq)]
pub struct Message {
pub streams: Vec<String>,
}

View File

@@ -0,0 +1,51 @@
pub mod incoming;
pub mod outgoing;
use crate::{config::Config, types::alpaca::websocket};
use core::panic;
use futures_util::{
stream::{SplitSink, SplitStream},
SinkExt, StreamExt,
};
use serde_json::{from_str, to_string};
use std::sync::Arc;
use tokio::net::TcpStream;
use tokio_tungstenite::{tungstenite::Message, MaybeTlsStream, WebSocketStream};
pub async fn authenticate(
config: &Arc<Config>,
sink: &mut SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>,
stream: &mut SplitStream<WebSocketStream<MaybeTlsStream<TcpStream>>>,
) {
sink.send(Message::Text(
to_string(&websocket::trading::outgoing::Message::Auth(
websocket::auth::Message {
key: config.alpaca_api_key.clone(),
secret: config.alpaca_api_secret.clone(),
},
))
.unwrap(),
))
.await
.unwrap();
match stream.next().await.unwrap().unwrap() {
Message::Binary(data) => {
let data = String::from_utf8(data).unwrap();
if from_str::<Vec<websocket::trading::incoming::Message>>(&data)
.unwrap()
.first()
!= Some(&websocket::trading::incoming::Message::Auth(
websocket::trading::incoming::auth::Message {
status: websocket::trading::incoming::auth::Status::Authorized,
action: websocket::trading::incoming::auth::Action::Auth,
},
))
{
panic!("Failed to authenticate with Alpaca websocket.");
}
}
_ => panic!("Failed to authenticate with Alpaca websocket."),
};
}

View File

@@ -0,0 +1,15 @@
pub mod subscribe;
use crate::types::alpaca::websocket::auth;
use serde::Serialize;
#[derive(Serialize)]
#[serde(tag = "action")]
#[serde(rename_all = "snake_case")]
pub enum Message {
Auth(auth::Message),
#[serde(rename = "listen")]
Subscribe {
data: subscribe::Message,
},
}

View File

@@ -0,0 +1,6 @@
use serde::Serialize;
#[derive(Serialize)]
pub struct Message {
streams: Vec<String>,
}

View File

@@ -3,8 +3,10 @@ pub mod asset;
pub mod backfill;
pub mod bar;
pub mod news;
pub mod order;
pub use asset::{Asset, Class, Exchange};
pub use backfill::Backfill;
pub use bar::Bar;
pub use news::News;
pub use order::Order;

107
src/types/order.rs Normal file
View File

@@ -0,0 +1,107 @@
use clickhouse::Row;
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use time::OffsetDateTime;
use uuid::Uuid;
#[derive(Clone, Debug, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
#[repr(i8)]
pub enum Class {
Simple = 1,
Bracket = 2,
Oco = 3,
Oto = 4,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
#[repr(i8)]
pub enum Type {
Market = 1,
Limit = 2,
Stop = 3,
StopLimit = 4,
TrailingStop = 5,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
#[repr(i8)]
pub enum Side {
Buy = 1,
Sell = -1,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
#[repr(i8)]
pub enum TimeInForce {
Day = 1,
Gtc = 2,
Opg = 3,
Cls = 4,
Ioc = 5,
Fok = 6,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
#[repr(i8)]
pub enum Status {
New = 1,
PartiallyFilled = 2,
Filled = 3,
DoneForDay = 4,
Canceled = 5,
Expired = 6,
Replaced = 7,
PendingCancel = 8,
PendingReplace = 9,
Accepted = 10,
PendingNew = 11,
AcceptedForBidding = 12,
Stopped = 13,
Rejected = 14,
Suspended = 15,
Calculated = 16,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Row)]
#[allow(clippy::struct_field_names)]
pub struct Order {
pub id: Uuid,
pub client_order_id: Uuid,
#[serde(with = "clickhouse::serde::time::datetime")]
pub time_submitted: OffsetDateTime,
#[serde(with = "clickhouse::serde::time::datetime")]
pub time_created: OffsetDateTime,
#[serde(with = "clickhouse::serde::time::datetime")]
pub time_updated: OffsetDateTime,
#[serde(with = "clickhouse::serde::time::datetime")]
pub time_filled: OffsetDateTime,
#[serde(with = "clickhouse::serde::time::datetime")]
pub time_expired: OffsetDateTime,
#[serde(with = "clickhouse::serde::time::datetime")]
pub time_cancel_requested: OffsetDateTime,
#[serde(with = "clickhouse::serde::time::datetime")]
pub time_canceled: OffsetDateTime,
#[serde(with = "clickhouse::serde::time::datetime")]
pub time_failed: OffsetDateTime,
#[serde(with = "clickhouse::serde::time::datetime")]
pub time_replaced: OffsetDateTime,
pub replaced_by: Uuid,
pub replaces: Uuid,
pub symbol: String,
pub order_class: Class,
pub order_type: Type,
pub side: Side,
pub time_in_force: TimeInForce,
pub extended_hours: bool,
pub notional: f64,
pub qty: f64,
pub filled_qty: f64,
pub filled_avg_price: f64,
pub status: Status,
pub limit_price: f64,
pub stop_price: f64,
pub trail_percent: f64,
pub trail_price: f64,
pub hwm: f64,
pub legs: Vec<Uuid>,
}

View File

@@ -3,9 +3,7 @@ pub mod cleanup;
pub mod macros;
pub mod news;
pub mod time;
pub mod websocket;
pub use cleanup::cleanup;
pub use news::{add_slash_to_pair, normalize_news_content, remove_slash_from_pair};
pub use time::{duration_until, last_minute, FIFTEEN_MINUTES, ONE_MINUTE, ONE_SECOND};
pub use websocket::authenticate;

View File

@@ -64,3 +64,57 @@ CREATE TABLE IF NOT EXISTS qrust.backfills_news (
)
ENGINE = ReplacingMergeTree()
PRIMARY KEY symbol;
CREATE TABLE IF NOT EXISTS qrust.orders (
id UUID,
client_order_id UUID,
time_submitted DateTime,
time_created DateTime,
time_updated DateTime,
time_filled DateTime,
time_expired DateTime,
time_cancel_requested DateTime,
time_canceled DateTime,
time_failed DateTime,
time_replaced DateTime,
replaced_by UUID,
replaces UUID,
symbol LowCardinality(String),
order_class Enum('simple' = 1, 'bracket' = 2, 'oco' = 3, 'oto' = 4),
order_type Enum('market' = 1, 'limit' = 2, 'stop' = 3, 'stop_limit' = 4, 'trailing_stop' = 5),
side Enum('buy' = 1, 'sell' = -1),
time_in_force Enum('day' = 1, 'gtc' = 2, 'opg' = 3, 'cls' = 4, 'ioc' = 5, 'fok' = 6),
extended_hours Boolean,
notional Float64,
qty Float64,
filled_qty Float64,
filled_avg_price Float64,
status Enum(
'new' = 1,
'partially_filled' = 2,
'filled' = 3,
'done_for_day' = 4,
'canceled' = 5,
'expired' = 6,
'replaced' = 7,
'pending_cancel' = 8,
'pending_replace' = 9,
'accepted' = 10,
'pending_new' = 11,
'accepted_for_bidding' = 12,
'stopped' = 13,
'rejected' = 14,
'suspended' = 15,
'calculated' = 16
),
limit_price Float64,
stop_price Float64,
trail_percent Float64,
trail_price Float64,
hwm Float64,
legs Array(UUID)
)
ENGINE = ReplacingMergeTree()
PARTITION BY toYYYYMM(time_submitted)
PRIMARY KEY id;