use async_trait::async_trait;
use reqwest::{Client, header};
use std::error::Error;
use std::time::Duration as StdDuration;
use tracing::{error, warn};
use crate::errors::CustomError;
use crate::types::{IntradayTrading, VpsForeignTradingData, VpsIntradayTradingResponseV2};
#[derive(thiserror::Error, Debug)]
pub enum AppError {
#[error("HTTP error: {0}")]
HttpError(#[from] reqwest::Error),
}
impl From<AppError> for CustomError {
fn from(err: AppError) -> Self {
CustomError {
message: err.to_string(),
}
}
}
#[async_trait]
pub trait VpsServiceTrait: Send + Sync {
async fn get_list_stock_trade_v2(
&self,
ticker: String,
) -> Result<Vec<IntradayTrading>, reqwest::Error>;
async fn fetch_stock_trade_list_by_ticker_code(
&self,
ticker: String,
) -> Result<Vec<IntradayTrading>, reqwest::Error>;
async fn fetch_latest_stock_trade_item_by_ticker_code(
&self,
ticker: String,
) -> Option<IntradayTrading>;
async fn fetch_foreign_trading_data(
&self,
ticker: String,
) -> Result<VpsForeignTradingData, CustomError>;
}
pub struct VpsService {
base_url: String,
client: Client,
}
impl VpsService {
pub fn new() -> Self {
let client = Client::builder()
.timeout(StdDuration::from_secs(30))
.build()
.expect("Failed to create HTTP client");
Self {
base_url: "https://bgapidatafeed.vps.com.vn".to_string(),
client,
}
}
}
#[async_trait]
impl VpsServiceTrait for VpsService {
async fn get_list_stock_trade_v2(
&self,
ticker: String,
) -> Result<Vec<IntradayTrading>, reqwest::Error> {
let mut next_idx = -1;
let num_page = 5000;
let mut results: Vec<IntradayTrading> = Vec::new();
loop {
let url = format!(
"{}/getliststocktrade_cs/{}?numpage={}&nextidx={}",
self.base_url, ticker, num_page, next_idx
);
let client = self.client.clone();
let response = client
.get(&url)
.header(header::CONTENT_TYPE, "application/json; charset=utf-8")
.send()
.await?;
let result = response
.json::<Option<VpsIntradayTradingResponseV2>>()
.await;
match result {
Ok(intraday_trading_data_v2) => {
let stock_trade_list = intraday_trading_data_v2.unwrap();
next_idx = stock_trade_list.next_idx;
let intraday_trading_data_v1 =
stock_trade_list
.clone()
.data
.into_iter()
.map(|item| IntradayTrading {
id: 0, ticker: ticker.clone(),
last_price: item.last_price,
open_price: 0, high_price: 0, low_price: 0, change: item.change,
last_vol: item.last_vol,
total_vol: item.total_vol,
trading_timestamp: item.trading_timestamp,
side: item.side,
});
results.extend(intraday_trading_data_v1);
if stock_trade_list.data.is_empty() || stock_trade_list.next_idx == 0 {
break;
}
}
Err(e) => {
warn!("Error getliststocktrade_cs: {:?}", e);
if e.is_decode() {
warn!("Decode error encountered, treating as empty list.");
break;
} else {
return Err(e);
}
}
}
}
Ok(results)
}
async fn fetch_stock_trade_list_by_ticker_code(
&self,
ticker: String,
) -> Result<Vec<IntradayTrading>, reqwest::Error> {
let url = format!("{}/getliststocktrade/{}", self.base_url, ticker);
let client = self.client.clone();
let response = client
.get(&url)
.header(header::CONTENT_TYPE, "application/json; charset=utf-8")
.send()
.await?;
let result = response.json::<Option<Vec<IntradayTrading>>>().await;
match result {
Ok(list_option) => {
let stock_trade_list = list_option.unwrap_or_default();
Ok(stock_trade_list)
}
Err(e) => {
if e.is_decode() {
Ok(Vec::new())
} else {
Err(e)
}
}
}
}
async fn fetch_foreign_trading_data(
&self,
ticker: String,
) -> Result<VpsForeignTradingData, CustomError> {
let url = format!("{}/getliststockdata/{}", self.base_url, ticker);
let client = self.client.clone();
let response = client
.get(&url)
.header(header::CONTENT_TYPE, "application/json; charset=utf-8")
.send()
.await
.map_err(|err| CustomError::from(AppError::from(err)))?;
let data: Vec<VpsForeignTradingData> = response.json().await.map_err(|err| {
println!("Error {:?}", err.source());
CustomError::from(AppError::from(err))
})?;
let record = data.into_iter().next().ok_or_else(|| CustomError {
message: "No trading data found".to_string(),
})?;
Ok(record)
}
async fn fetch_latest_stock_trade_item_by_ticker_code(
&self,
ticker: String,
) -> Option<IntradayTrading> {
let result: Option<IntradayTrading> = None;
let stock_trade_list = self.get_list_stock_trade_v2(ticker).await;
match stock_trade_list {
Ok(list) => list.into_iter().last(),
Err(err) => {
error!(
"fetch_latest_stock_trade_item_by_ticker_code error: {:?}",
err
);
result
} }
}
}