use crate::domain::{Money, Order, OrderDirection, Wallet};
use crate::exchange::{
ExchangeError, ExchangeInfo, ExchangeProvider, ExchangeResult, Instrument, InstrumentType,
MarketData, MarketStatus, OrderBook,
};
use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::RwLock;
#[derive(Debug)]
pub struct TBankAdapter {
info: ExchangeInfo,
token: Option<String>,
sandbox: bool,
connected: RwLock<bool>,
accounts: RwLock<HashMap<String, AccountState>>,
}
#[derive(Debug, Clone)]
struct AccountState {
wallet: Wallet,
}
impl TBankAdapter {
#[must_use]
pub fn new() -> Self {
Self {
info: ExchangeInfo {
id: "tbank".into(),
name: "T-Bank (Tinkoff)".into(),
url: "https://www.tbank.ru/invest/".into(),
supported_types: vec![
InstrumentType::Stock,
InstrumentType::Etf,
InstrumentType::Bond,
InstrumentType::Currency,
],
},
token: None,
sandbox: true,
connected: RwLock::new(false),
accounts: RwLock::new(HashMap::new()),
}
}
#[must_use]
pub fn with_token(token: impl Into<String>) -> Self {
let mut adapter = Self::new();
adapter.token = Some(token.into());
adapter
}
#[must_use]
pub fn with_sandbox(mut self, sandbox: bool) -> Self {
self.sandbox = sandbox;
self
}
pub fn connect(&self) -> ExchangeResult<()> {
if self.token.is_none() {
return Err(ExchangeError::ConfigurationError(
"API token is required".into(),
));
}
*self.connected.write().unwrap() = true;
Ok(())
}
fn ensure_connected(&self) -> ExchangeResult<()> {
if !*self.connected.read().unwrap() {
return Err(ExchangeError::NetworkError("Not connected".into()));
}
Ok(())
}
}
impl Default for TBankAdapter {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl ExchangeProvider for TBankAdapter {
fn info(&self) -> &ExchangeInfo {
&self.info
}
async fn ping(&self) -> ExchangeResult<()> {
self.ensure_connected()?;
Ok(())
}
async fn get_wallet(&self, account_id: &str) -> ExchangeResult<Wallet> {
self.ensure_connected()?;
let accounts = self.accounts.read().unwrap();
accounts
.get(account_id)
.map(|s| s.wallet.clone())
.ok_or_else(|| ExchangeError::Internal(format!("Account {} not found", account_id)))
}
async fn get_instrument(&self, symbol: &str) -> ExchangeResult<Instrument> {
self.ensure_connected()?;
Err(ExchangeError::InstrumentNotFound(symbol.into()))
}
async fn list_instruments(&self) -> ExchangeResult<Vec<Instrument>> {
self.ensure_connected()?;
Ok(Vec::new())
}
async fn get_market_data(&self, symbol: &str) -> ExchangeResult<MarketData> {
self.ensure_connected()?;
Ok(MarketData {
symbol: symbol.into(),
last_price: Money::zero("RUB"),
bid: None,
ask: None,
volume_24h: None,
high_24h: None,
low_24h: None,
change_24h: None,
change_percent_24h: None,
market_status: MarketStatus::Unknown,
timestamp: chrono::Utc::now(),
})
}
async fn get_market_data_batch(&self, symbols: &[String]) -> ExchangeResult<Vec<MarketData>> {
self.ensure_connected()?;
let mut results = Vec::new();
for symbol in symbols {
results.push(self.get_market_data(symbol).await?);
}
Ok(results)
}
async fn get_order_book(&self, symbol: &str, _depth: u32) -> ExchangeResult<OrderBook> {
self.ensure_connected()?;
Ok(OrderBook::empty(symbol))
}
async fn place_order(&self, _account_id: &str, order: Order) -> ExchangeResult<Order> {
self.ensure_connected()?;
Err(ExchangeError::OrderRejected(format!(
"Order placement not implemented for {}",
order.symbol()
)))
}
async fn cancel_order(&self, _account_id: &str, order_id: &str) -> ExchangeResult<Order> {
self.ensure_connected()?;
Err(ExchangeError::OrderNotFound(order_id.into()))
}
async fn get_order(&self, _account_id: &str, order_id: &str) -> ExchangeResult<Order> {
self.ensure_connected()?;
Err(ExchangeError::OrderNotFound(order_id.into()))
}
async fn get_active_orders(&self, _account_id: &str) -> ExchangeResult<Vec<Order>> {
self.ensure_connected()?;
Ok(Vec::new())
}
async fn is_market_open(&self, _symbol: &str) -> ExchangeResult<bool> {
self.ensure_connected()?;
Ok(true)
}
async fn market_buy(&self, account_id: &str, symbol: &str, lots: u32) -> ExchangeResult<Order> {
let order = Order::market(
uuid::Uuid::new_v4().to_string(),
symbol,
&self.info.id,
OrderDirection::Buy,
lots,
);
self.place_order(account_id, order).await
}
async fn market_sell(
&self,
account_id: &str,
symbol: &str,
lots: u32,
) -> ExchangeResult<Order> {
let order = Order::market(
uuid::Uuid::new_v4().to_string(),
symbol,
&self.info.id,
OrderDirection::Sell,
lots,
);
self.place_order(account_id, order).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_adapter_creation() {
let adapter = TBankAdapter::new();
assert_eq!(adapter.info().id, "tbank");
assert!(adapter.sandbox);
}
#[test]
fn test_adapter_with_token() {
let adapter = TBankAdapter::with_token("test-token");
assert!(adapter.token.is_some());
}
#[test]
fn test_connect_without_token() {
let adapter = TBankAdapter::new();
let result = adapter.connect();
assert!(result.is_err());
}
}