#![allow(clippy::unwrap_used, clippy::expect_used)]
use chrono::{Duration as ChronoDuration, Utc};
use rustrade_data::event::DataKind;
use rustrade_data::exchange::lse::export::{
LseExportRange, LseExportRequest, LseExportStatus, LseExportTimeframe,
};
use rustrade_data::exchange::lse::market::{self, LseDataset};
use rustrade_data::exchange::lse::parquet::read_export;
use rustrade_data::exchange::lse::vault::LseVaultClient;
use rustrade_instrument::Underlying;
use rustrade_instrument::asset::Asset;
use rustrade_instrument::index::IndexedInstruments;
use rustrade_instrument::instrument::name::{InstrumentNameExchange, InstrumentNameInternal};
use rustrade_instrument::instrument::quote::InstrumentQuoteAsset;
use rustrade_instrument::instrument::{Instrument, kind::InstrumentKind};
use std::time::Duration;
use tracing::{info, warn};
const SYMBOL: &str = "EUR/USD";
#[tokio::main]
async fn main() {
init_logging();
let client = LseVaultClient::from_env()
.expect("set LSE_API_KEY - get a free key at https://londonstrategicedge.com/data");
let usage = client.usage().await.expect("usage");
info!(
exports_remaining_hour = usage.exports_remaining_hour(),
exports_cap_hour = usage.exports_cap_hour,
"allowance before submitting"
);
let start = (Utc::now() - ChronoDuration::days(4)).date_naive();
let range =
LseExportRange::new(start, start + ChronoDuration::days(1)).expect("a one-day range");
let request = LseExportRequest::new(LseDataset::Fx, SYMBOL, LseExportTimeframe::Tick, range)
.expect("a valid export request");
let job = client
.submit_export(&request)
.await
.expect("submitting the export");
info!(job_id = %job.job_id, status = %job.status, "export submitted");
let status = client
.await_export(
&job.job_id,
Duration::from_secs(3),
Duration::from_secs(300),
)
.await
.expect("the export job to reach a terminal state");
assert_eq!(
status.status,
LseExportStatus::Ready,
"export finished in {} ({:?})",
status.status,
status.error
);
let rows = status.rows.unwrap_or_default();
if rows == 0 {
warn!("the export is ready but empty - the request matched no rows");
return;
}
let directory = tempfile::tempdir().expect("a temporary directory");
let destination = directory.path().join("eurusd_tick.parquet");
let export = client
.download_export(&status, &destination, &request)
.await
.expect("downloading and verifying the artifact");
info!(
path = %export.path().display(),
rows,
bytes = ?status.bytes,
table = ?status.table_name,
"artifact downloaded and verified"
);
let exchange = LseDataset::Fx.exchange_id();
let name = InstrumentNameExchange::new(SYMBOL);
let underlying = market::underlying(SYMBOL);
let instruments = IndexedInstruments::new(vec![Instrument::new(
exchange,
InstrumentNameInternal::new_from_exchange(exchange, name.clone()),
name,
Underlying::new(
Asset::new_from_exchange(underlying.base),
Asset::new_from_exchange(underlying.quote),
),
InstrumentQuoteAsset::UnderlyingQuote,
InstrumentKind::Spot,
None,
)]);
let instrument = market::instrument_index_for(&instruments, exchange, SYMBOL)
.expect("the symbol to be registered, priced in the asset derived for its venue suffix");
let decoded = tokio::task::spawn_blocking(move || {
let events = read_export(&export, instrument).expect("the artifact to decode");
let mut decoded = 0_usize;
for event in events {
let event = event.expect("every row to decode");
decoded += 1;
if decoded <= 3 {
match &event.kind {
DataKind::OrderBookL1(book) => info!(
time = %event.time_exchange,
bid = ?book.best_bid.map(|level| level.price),
ask = ?book.best_ask.map(|level| level.price),
"quote"
),
other => info!(time = %event.time_exchange, ?other, "event"),
}
}
}
decoded
})
.await
.expect("the decode task not to panic");
info!(decoded, "decode complete");
}
fn init_logging() {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::filter::EnvFilter::builder()
.with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into())
.from_env_lossy(),
)
.with_ansi(cfg!(debug_assertions))
.json()
.init()
}