use std::collections::HashMap;
use std::sync::Mutex;
use tracing::debug;
use crate::client::Client;
use crate::error::{Error, Result};
pub type Position = bezant_api::IndividualPosition;
pub type ContractSummary = bezant_api::SecdefSearchResponseSecdefSearchResponse;
pub const POSITIONS_PAGE_SIZE: usize = 30;
pub const MAX_POSITION_PAGES: u32 = 100;
impl Client {
#[tracing::instrument(skip(self), fields(account = %account_id), level = "debug")]
pub async fn all_positions(&self, account_id: &str) -> Result<Vec<Position>> {
let mut all = Vec::new();
for page in 0..MAX_POSITION_PAGES {
let req = bezant_api::GetPaginatedPositionsRequest {
path: bezant_api::GetPaginatedPositionsRequestPath {
account_id: account_id.to_owned(),
page_id: i32::try_from(page).unwrap_or(i32::MAX),
},
query: bezant_api::GetPaginatedPositionsRequestQuery::default(),
};
let resp = self.api().get_paginated_positions(req).await?;
let batch = match resp {
bezant_api::GetPaginatedPositionsResponse::Ok(items) => items,
bezant_api::GetPaginatedPositionsResponse::Unauthorized => {
return Err(Error::NotAuthenticated)
}
bezant_api::GetPaginatedPositionsResponse::BadRequest => {
return Err(Error::BadRequest(format!(
"portfolio/{account_id}/positions/{page} returned 400"
)))
}
bezant_api::GetPaginatedPositionsResponse::InternalServerError => {
return Err(Error::UpstreamStatus {
endpoint: "portfolio/positions",
status: 500,
body_preview: None,
})
}
bezant_api::GetPaginatedPositionsResponse::ServiceUnavailable => {
return Err(Error::UpstreamStatus {
endpoint: "portfolio/positions",
status: 503,
body_preview: None,
})
}
bezant_api::GetPaginatedPositionsResponse::Unknown => {
return Err(Error::Unknown {
endpoint: "portfolio/positions",
})
}
};
let n = batch.len();
debug!(page, fetched = n, "position page fetched");
all.extend(batch);
if n < POSITIONS_PAGE_SIZE {
break;
}
}
Ok(all)
}
}
#[derive(Debug)]
pub struct SymbolCache {
client: Client,
cache: Mutex<HashMap<String, Option<i64>>>,
}
fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
m.lock().unwrap_or_else(|e| e.into_inner())
}
impl SymbolCache {
#[must_use]
pub fn new(client: Client) -> Self {
Self {
client,
cache: Mutex::new(HashMap::new()),
}
}
#[tracing::instrument(skip(self), fields(symbol = %symbol), level = "debug")]
pub async fn conid_for(&self, symbol: &str) -> Result<i64> {
if let Some(entry) = lock(&self.cache).get(symbol).copied() {
return match entry {
Some(conid) => Ok(conid),
None => Err(Error::SymbolNotFound {
symbol: symbol.to_owned(),
}),
};
}
let req = bezant_api::GetContractSymbolsFromBodyRequest {
body: bezant_api::SearchRequestBody {
symbol: symbol.to_owned(),
sec_type: Some(bezant_api::GetContractSymbolsRequestQuerySecType::Stk),
name: Some(false),
more: Some(false),
..bezant_api::SearchRequestBody::default()
},
};
let resp = self
.client
.api()
.get_contract_symbols_from_body(req)
.await?;
let items = match resp {
bezant_api::GetContractSymbolsResponse::Ok(items) => items,
bezant_api::GetContractSymbolsResponse::BadRequest => {
lock(&self.cache).insert(symbol.to_owned(), None);
return Err(Error::SymbolNotFound {
symbol: symbol.to_owned(),
});
}
bezant_api::GetContractSymbolsResponse::Unauthorized => {
return Err(Error::NotAuthenticated)
}
bezant_api::GetContractSymbolsResponse::InternalServerError => {
return Err(Error::UpstreamStatus {
endpoint: "iserver/secdef/search",
status: 500,
body_preview: None,
})
}
bezant_api::GetContractSymbolsResponse::ServiceUnavailable => {
return Err(Error::UpstreamStatus {
endpoint: "iserver/secdef/search",
status: 503,
body_preview: None,
})
}
bezant_api::GetContractSymbolsResponse::Unknown => {
return Err(Error::Unknown {
endpoint: "iserver/secdef/search",
})
}
};
let Some(first) = items.first() else {
lock(&self.cache).insert(symbol.to_owned(), None);
return Err(Error::SymbolNotFound {
symbol: symbol.to_owned(),
});
};
let conid_str = first
.conid
.as_deref()
.ok_or_else(|| Error::SymbolNotFound {
symbol: symbol.to_owned(),
})?;
let conid: i64 = conid_str.parse().map_err(|source| Error::BadConid {
symbol: symbol.to_owned(),
raw: conid_str.to_owned(),
source,
})?;
lock(&self.cache).insert(symbol.to_owned(), Some(conid));
Ok(conid)
}
pub fn forget(&self, symbol: &str) {
lock(&self.cache).remove(symbol);
}
pub fn clear(&self) {
lock(&self.cache).clear();
}
#[must_use]
pub fn client(&self) -> &Client {
&self.client
}
}