-
Notifications
You must be signed in to change notification settings - Fork 41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Latest trade data #43
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
// Copyright (C) 2020-2022 The apca Developers | ||
// SPDX-License-Identifier: GPL-3.0-or-later | ||
|
||
use apca::data::v2::{last_quote, last_trade}; | ||
use apca::ApiInfo; | ||
use apca::Client; | ||
|
||
#[tokio::main] | ||
async fn main() { | ||
// Requires the following environment variables to be present: | ||
// - APCA_API_KEY_ID -> your API key | ||
// - APCA_API_SECRET_KEY -> your secret key | ||
// | ||
// Optionally, the following variable is honored: | ||
// - APCA_API_BASE_URL -> the API base URL to use (set to | ||
// https://api.alpaca.markets for live trading) | ||
let api_info = ApiInfo::from_env().unwrap(); | ||
let client = Client::new(api_info); | ||
|
||
let quote_req = last_quote::LastQuoteReq::new(vec!["AAPL".to_string(), "MSFT".to_string()]); | ||
let quotes = client.issue::<last_quote::Get>("e_req).await.unwrap(); | ||
quotes.iter().for_each(|q| { | ||
println!( | ||
"Latest quote for {}: Ask {}/{} Bid {}/{}", | ||
q.symbol, q.ask_price, q.ask_size, q.bid_price, q.bid_size | ||
) | ||
}); | ||
|
||
let trade_req = last_trade::LastTradeRequest::new(vec![ | ||
"SPY".to_string(), | ||
"QQQ".to_string(), | ||
"IWM".to_string(), | ||
]); | ||
let trades = client.issue::<last_trade::Get>(&trade_req).await.unwrap(); | ||
trades.iter().for_each(|trade| { | ||
println!( | ||
"Latest trade for {}: {} @ {}", | ||
trade.symbol, trade.size, trade.price | ||
); | ||
}); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,77 +10,101 @@ use serde::Deserialize; | |
use serde::Serialize; | ||
use serde_json::from_slice as from_json; | ||
use serde_urlencoded::to_string as to_query; | ||
use std::collections::HashMap; | ||
|
||
use crate::data::v2::Feed; | ||
use crate::data::DATA_BASE_URL; | ||
use crate::Str; | ||
|
||
|
||
/// A GET request to be made to the /v2/stocks/{symbol}/quotes/latest endpoint. | ||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)] | ||
/// A GET request to be made to the /v2/stocks/quotes/latest endpoint. | ||
#[derive(Clone, Serialize, Eq, PartialEq, Debug)] | ||
pub struct LastQuoteReq { | ||
/// The symbol to retrieve the last quote for. | ||
#[serde(skip)] | ||
pub symbol: String, | ||
/// Comma-separated list of symbols to retrieve the last quote for. | ||
pub symbols: String, | ||
/// The data feed to use. | ||
#[serde(rename = "feed")] | ||
pub feed: Option<Feed>, | ||
} | ||
|
||
|
||
/// A helper for initializing [`LastQuoteReq`] objects. | ||
#[derive(Clone, Debug, Default, Eq, PartialEq)] | ||
#[allow(missing_copy_implementations)] | ||
pub struct LastQuoteReqInit { | ||
/// See `LastQuoteReq::feed`. | ||
pub feed: Option<Feed>, | ||
#[doc(hidden)] | ||
pub _non_exhaustive: (), | ||
} | ||
|
||
impl LastQuoteReqInit { | ||
/// Create a [`LastQuoteReq`] from a `LastQuoteReqInit`. | ||
#[inline] | ||
pub fn init<S>(self, symbol: S) -> LastQuoteReq | ||
where | ||
S: Into<String>, | ||
{ | ||
LastQuoteReq { | ||
symbol: symbol.into(), | ||
feed: self.feed, | ||
impl LastQuoteReq { | ||
/// Create a new LastQuoteReq with the given symbols. | ||
pub fn new(symbols: Vec<String>) -> Self { | ||
Self { | ||
symbols: symbols.join(",").into(), | ||
feed: None, | ||
} | ||
} | ||
/// Set the data feed to use. | ||
pub fn with_feed(mut self, feed: Feed) -> Self { | ||
self.feed = Some(feed); | ||
self | ||
} | ||
} | ||
|
||
|
||
/// A quote bar as returned by the /v2/stocks/<symbol>/quotes/latest endpoint. | ||
/// A quote bar as returned by the /v2/stocks/quotes/latest endpoint. | ||
/// See | ||
/// https://alpaca.markets/docs/api-references/market-data-api/stock-pricing-data/historical/#latest-multi-quotes | ||
// TODO: Not all fields are hooked up. | ||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] | ||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this change necessary? |
||
#[non_exhaustive] | ||
pub struct Quote { | ||
/// The time stamp of this quote. | ||
#[serde(rename = "t")] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why are these renames being removed? |
||
pub time: DateTime<Utc>, | ||
/// The ask price. | ||
#[serde(rename = "ap")] | ||
pub ask_price: Num, | ||
/// The ask size. | ||
#[serde(rename = "as")] | ||
pub ask_size: u64, | ||
/// The bid price. | ||
#[serde(rename = "bp")] | ||
pub bid_price: Num, | ||
/// The bid size. | ||
#[serde(rename = "bs")] | ||
pub bid_size: u64, | ||
/// Symbol of this quote | ||
pub symbol: String, | ||
} | ||
|
||
impl Quote { | ||
fn from(symbol: &str, point: QuoteDataPoint) -> Self { | ||
Self { | ||
time: point.t, | ||
ask_price: point.ap.clone(), | ||
ask_size: point.r#as, | ||
bid_price: point.bp.clone(), | ||
bid_size: point.bs, | ||
symbol: symbol.to_string(), | ||
} | ||
} | ||
|
||
fn parse(body: &[u8]) -> Result<Vec<Quote>, serde_json::Error> { | ||
from_json::<LastQuoteResponse>(body).map(|response| { | ||
response | ||
.quotes | ||
.into_iter() | ||
.map(|(sym, point)| Quote::from(&sym, point)) | ||
.collect() | ||
}) | ||
} | ||
} | ||
|
||
/// fields for individual data points in the response JSON | ||
#[derive(Clone, Debug, Deserialize)] | ||
pub struct QuoteDataPoint { | ||
t: DateTime<Utc>, | ||
ap: Num, | ||
r#as: u64, | ||
bp: Num, | ||
bs: u64, | ||
} | ||
|
||
/// A representation of the JSON data in the response | ||
#[derive(Debug, Deserialize)] | ||
pub struct LastQuoteResponse { | ||
quotes: HashMap<String, QuoteDataPoint>, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why a |
||
} | ||
|
||
EndpointNoParse! { | ||
/// The representation of a GET request to the | ||
/// /v2/stocks/<symbol>/quotes/latest endpoint. | ||
/// /v2/stocks/quotes/latest endpoint. | ||
pub Get(LastQuoteReq), | ||
Ok => Quote, [ | ||
Ok => Vec<Quote>, [ | ||
/// The last quote was retrieved successfully. | ||
/* 200 */ OK, | ||
], | ||
|
@@ -94,38 +118,23 @@ EndpointNoParse! { | |
Some(DATA_BASE_URL.into()) | ||
} | ||
|
||
fn path(input: &Self::Input) -> Str { | ||
format!("/v2/stocks/{}/quotes/latest", input.symbol).into() | ||
fn path(_input: &Self::Input) -> Str { | ||
format!("/v2/stocks/quotes/latest").into() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't need to format if you are not formatting. |
||
} | ||
|
||
fn query(input: &Self::Input) -> Result<Option<Str>, Self::ConversionError> { | ||
Ok(Some(to_query(input)?.into())) | ||
} | ||
|
||
fn parse(body: &[u8]) -> Result<Self::Output, Self::ConversionError> { | ||
/// A helper object for parsing the response to a `Get` request. | ||
#[derive(Deserialize)] | ||
struct Response { | ||
/// The symbol for which the quote was reported. | ||
#[allow(unused)] | ||
symbol: String, | ||
/// The quote belonging to the provided symbol. | ||
quote: Quote, | ||
} | ||
|
||
// We are not interested in the actual `Response` object. Clients | ||
// can keep track of what symbol they requested a quote for. | ||
from_json::<Response>(body) | ||
.map(|response| response.quote) | ||
.map_err(Self::ConversionError::from) | ||
Quote::parse(body).map_err(Self::ConversionError::from) | ||
} | ||
|
||
fn parse_err(body: &[u8]) -> Result<Self::ApiError, Vec<u8>> { | ||
from_json::<Self::ApiError>(body).map_err(|_| body.to_vec()) | ||
} | ||
} | ||
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keep needless useless changes to a minimum? |
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
|
@@ -138,33 +147,49 @@ mod tests { | |
use crate::Client; | ||
use crate::RequestError; | ||
|
||
|
||
/// Check that we can parse the reference quote from the | ||
/// documentation. | ||
#[test] | ||
fn parse_reference_quote() { | ||
let response = br#"{ | ||
"t": "2021-02-06T13:35:08.946977536Z", | ||
"ax": "C", | ||
"ap": 387.7, | ||
"as": 1, | ||
"bx": "N", | ||
"bp": 387.67, | ||
"bs": 1, | ||
"c": [ | ||
"R" | ||
] | ||
}"#; | ||
|
||
let quote = from_json::<Quote>(response).unwrap(); | ||
"quotes": { | ||
"TSLA": { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please no tabs in code using spaces? |
||
"t": "2022-04-12T17:26:45.009288296Z", | ||
"ax": "V", | ||
"ap": 1020, | ||
"as": 3, | ||
"bx": "V", | ||
"bp": 990, | ||
"bs": 5, | ||
"c": ["R"], | ||
"z": "C" | ||
}, | ||
"AAPL": { | ||
"t": "2022-04-12T17:26:44.962998616Z", | ||
"ax": "V", | ||
"ap": 170, | ||
"as": 1, | ||
"bx": "V", | ||
"bp": 168.03, | ||
"bs": 1, | ||
"c": ["R"], | ||
"z": "C" | ||
} | ||
} | ||
}"#; | ||
|
||
let mut result = Quote::parse(response).unwrap(); | ||
result.sort_by_key(|t| t.time); | ||
assert_eq!(result.len(), 2); | ||
assert_eq!(result[1].ask_price, Num::new(1020, 1)); | ||
assert_eq!(result[1].ask_size, 3); | ||
assert_eq!(result[1].bid_price, Num::new(990, 1)); | ||
assert_eq!(result[1].bid_size, 5); | ||
assert_eq!(result[1].symbol, "TSLA".to_string()); | ||
assert_eq!( | ||
quote.time, | ||
DateTime::parse_from_rfc3339("2021-02-06T13:35:08.946977536Z").unwrap() | ||
result[1].time, | ||
DateTime::parse_from_rfc3339("2022-04-12T17:26:45.009288296Z").unwrap() | ||
); | ||
assert_eq!(quote.ask_price, Num::new(3877, 10)); | ||
assert_eq!(quote.ask_size, 1); | ||
assert_eq!(quote.bid_price, Num::new(38767, 100)); | ||
assert_eq!(quote.bid_size, 1); | ||
} | ||
|
||
/// Verify that we can retrieve the last quote for an asset. | ||
|
@@ -173,12 +198,28 @@ mod tests { | |
let api_info = ApiInfo::from_env().unwrap(); | ||
let client = Client::new(api_info); | ||
|
||
let req = LastQuoteReqInit::default().init("SPY"); | ||
let quote = client.issue::<Get>(&req).await.unwrap(); | ||
let req = LastQuoteReq::new(vec!["SPY".to_string()]); | ||
let quotes = client.issue::<Get>(&req).await.unwrap(); | ||
// Just as a rough sanity check, we require that the reported time | ||
// is some time after two weeks before today. That should safely | ||
// account for any combination of holidays, weekends, etc. | ||
assert!(quote.time >= Utc::now() - Duration::weeks(2)); | ||
assert!(quotes[0].time >= Utc::now() - Duration::weeks(2)); | ||
} | ||
|
||
/// Retrieve multiple symbols at once. | ||
#[test(tokio::test)] | ||
async fn request_last_quotes_multi() { | ||
let api_info = ApiInfo::from_env().unwrap(); | ||
let client = Client::new(api_info); | ||
|
||
let req = LastQuoteReq::new(vec![ | ||
"SPY".to_string(), | ||
"QQQ".to_string(), | ||
"MSFT".to_string(), | ||
]); | ||
let quotes = client.issue::<Get>(&req).await.unwrap(); | ||
assert_eq!(quotes.len(), 3); | ||
assert!(quotes[0].time >= Utc::now() - Duration::weeks(2)); | ||
} | ||
|
||
/// Verify that we can specify the SIP feed as the data source to use. | ||
|
@@ -187,10 +228,7 @@ mod tests { | |
let api_info = ApiInfo::from_env().unwrap(); | ||
let client = Client::new(api_info); | ||
|
||
let req = LastQuoteReq { | ||
symbol: "SPY".to_string(), | ||
feed: Some(Feed::SIP), | ||
}; | ||
let req = LastQuoteReq::new(vec!["SPY".to_string()]).with_feed(Feed::SIP); | ||
|
||
let result = client.issue::<Get>(&req).await; | ||
// Unfortunately we can't really know whether the user has the | ||
|
@@ -202,13 +240,24 @@ mod tests { | |
} | ||
} | ||
|
||
/// Verify that we can properly parse a reference bar response. | ||
/// Non-existent symbol is skipped in the result. | ||
#[test(tokio::test)] | ||
async fn nonexistent_symbol() { | ||
let api_info = ApiInfo::from_env().unwrap(); | ||
let client = Client::new(api_info); | ||
|
||
let req = LastQuoteReqInit::default().init("ABC123"); | ||
let req = LastQuoteReq::new(vec!["SPY".to_string(), "NOSUCHSYMBOL".to_string()]); | ||
let quotes = client.issue::<Get>(&req).await.unwrap(); | ||
assert_eq!(quotes.len(), 1); | ||
} | ||
|
||
/// Symbol with characters outside A-Z results in an error response from the server. | ||
#[test(tokio::test)] | ||
async fn bad_symbol() { | ||
let api_info = ApiInfo::from_env().unwrap(); | ||
let client = Client::new(api_info); | ||
|
||
let req = LastQuoteReq::new(vec!["ABC123".to_string()]); | ||
let err = client.issue::<Get>(&req).await.unwrap_err(); | ||
match err { | ||
RequestError::Endpoint(GetError::InvalidInput(_)) => (), | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see a good reason why this type is being removed?