use async_trait::async_trait;
use reqwest::Client;
use std::collections::HashMap;
use crate::core::types::*;
use crate::core::traits::*;
use super::endpoints::*;
use super::auth::*;
use super::parser::*;
pub struct KrxConnector {
client: Client,
auth: KrxAuth,
endpoints: KrxEndpoints,
}
impl KrxConnector {
pub fn new(auth: KrxAuth) -> Self {
Self {
client: Client::new(),
auth,
endpoints: KrxEndpoints::default(),
}
}
#[deprecated(
since = "0.1.0",
note = "KRX now requires authentication. Use new() with KrxAuth::from_env() or obtain keys from openapi.krx.co.kr"
)]
pub fn new_public() -> Self {
Self {
client: Client::new(),
auth: KrxAuth {
auth_key: None,
public_data_portal_key: None,
},
endpoints: KrxEndpoints::default(),
}
}
pub fn from_env() -> Self {
Self::new(KrxAuth::from_env())
}
async fn post_openapi(
&self,
endpoint: KrxEndpoint,
body: serde_json::Value,
) -> ExchangeResult<serde_json::Value> {
if !self.auth.has_openapi_auth() {
return Err(ExchangeError::Auth(
"KRX Open API requires AUTH_KEY. Register at https://openapi.krx.co.kr/ and set KRX_AUTH_KEY environment variable".to_string(),
));
}
let url = format!("{}{}", self.endpoints.openapi_base, endpoint.path());
let mut headers = HashMap::new();
self.auth.sign_openapi_headers(&mut headers);
let mut request = self.client.post(&url);
for (key, value) in headers {
request = request.header(key, value);
}
request = request.json(&body);
let response = request
.send()
.await
.map_err(|e| ExchangeError::Network(format!("Request failed: {}", e)))?;
let status = response.status();
let response_text = response
.text()
.await
.map_err(|e| ExchangeError::Network(format!("Failed to read response: {}", e)))?;
if !status.is_success() {
return Err(match status.as_u16() {
401 => ExchangeError::Auth(format!("API key not authorized: {}", response_text)),
403 => ExchangeError::PermissionDenied(format!("Access forbidden - check API permissions: {}", response_text)),
429 => ExchangeError::RateLimit,
_ => ExchangeError::Http(format!("HTTP {}: {}", status, response_text)),
});
}
let json: serde_json::Value = serde_json::from_str(&response_text)
.map_err(|e| ExchangeError::Parse(format!("JSON parse error: {}. Response: {}", e, response_text)))?;
KrxParser::check_api_error(&json)?;
Ok(json)
}
async fn post_data_marketplace(
&self,
params: &[(&str, &str)],
) -> ExchangeResult<serde_json::Value> {
let url = self.endpoints.data_marketplace;
let response = self
.client
.post(url)
.header(
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
)
.header("Referer", "http://data.krx.co.kr/")
.header("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
.form(params)
.send()
.await
.map_err(|e| ExchangeError::Network(format!("KRX marketplace request failed: {}", e)))?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(ExchangeError::Http(format!("HTTP {}: {}", status, body)));
}
let json: serde_json::Value = response
.json()
.await
.map_err(|e| ExchangeError::Parse(format!("JSON parse error: {}", e)))?;
Ok(json)
}
async fn get_portal(&self, mut params: HashMap<String, String>) -> ExchangeResult<serde_json::Value> {
let url = self.endpoints.public_data_portal;
self.auth.sign_portal_query(&mut params);
params.entry("resultType".to_string()).or_insert("json".to_string());
params.entry("numOfRows".to_string()).or_insert("100".to_string());
params.entry("pageNo".to_string()).or_insert("1".to_string());
let response = self
.client
.get(url)
.query(¶ms)
.send()
.await
.map_err(|e| ExchangeError::Network(format!("Request failed: {}", e)))?;
if !response.status().is_success() {
return Err(ExchangeError::Http(format!("HTTP {}", response.status())));
}
let json = response
.json()
.await
.map_err(|e| ExchangeError::Parse(format!("JSON parse error: {}", e)))?;
KrxParser::check_api_error(&json)?;
Ok(json)
}
}
impl ExchangeIdentity for KrxConnector {
fn exchange_name(&self) -> &'static str {
"krx"
}
fn exchange_id(&self) -> ExchangeId {
ExchangeId::Krx
}
fn is_testnet(&self) -> bool {
false
}
fn supported_account_types(&self) -> Vec<AccountType> {
vec![AccountType::Spot]
}
}
#[async_trait]
impl MarketData for KrxConnector {
async fn get_price(&self, symbol: SymbolInput<'_>, account_type: AccountType) -> ExchangeResult<Price> {
let klines = self.get_klines(symbol, "1d", Some(1), account_type, None).await?;
klines
.first()
.map(|k| k.close)
.ok_or_else(|| ExchangeError::NotFound("No data returned for symbol".to_string()))
}
async fn get_ticker(&self, symbol: SymbolInput<'_>, account_type: AccountType) -> ExchangeResult<Ticker> {
let sym_str: String = match symbol {
SymbolInput::Raw(s) => s.to_string(),
SymbolInput::Canonical(c) => c.to_concat(),
};
let klines = self
.get_klines(SymbolInput::Raw(&sym_str), "1d", Some(1), account_type, None)
.await?;
let k = klines
.first()
.ok_or_else(|| ExchangeError::NotFound("No data returned for symbol".to_string()))?;
Ok(Ticker {
symbol: sym_str,
last_price: k.close,
bid_price: None,
ask_price: None,
high_24h: Some(k.high),
low_24h: Some(k.low),
volume_24h: Some(k.volume),
quote_volume_24h: k.quote_volume,
price_change_24h: None,
price_change_percent_24h: None,
timestamp: k.open_time,
})
}
async fn get_orderbook(
&self,
_symbol: SymbolInput<'_>,
_depth: Option<u16>,
_account_type: AccountType,
) -> ExchangeResult<OrderBook> {
Err(ExchangeError::UnsupportedOperation(
"KRX does not provide orderbook data - data feed only".to_string(),
))
}
async fn get_klines(
&self,
symbol: SymbolInput<'_>,
interval: &str,
limit: Option<u16>,
_account_type: AccountType,
_end_time: Option<i64>,
) -> ExchangeResult<Vec<Kline>> {
if interval != "1d" && interval != "1day" {
return Err(ExchangeError::InvalidRequest(
"KRX only provides daily (1d) candles".to_string(),
));
}
let sym_str: String = match symbol {
SymbolInput::Raw(s) => s.to_string(),
SymbolInput::Canonical(c) => c.to_concat(),
};
let code = sym_str.as_str();
let n_days = limit.unwrap_or(30) as i64;
use chrono::{Duration, Local, Datelike};
let end = Local::now();
let start = end - Duration::days(n_days - 1);
let strt_dd = format_date(start.year(), start.month(), start.day());
let end_dd = format_date(end.year(), end.month(), end.day());
let isin = format!("KR7{}003", code);
let params: &[(&str, &str)] = &[
("bld", "dbms/MDC/STAT/standard/MDCSTAT01701"),
("isuCd", &isin),
("isuCd2", code),
("strtDd", &strt_dd),
("endDd", &end_dd),
("adjStkPrc", "2"),
("adjStkPrcTpCd", "S"),
];
let response = self.post_data_marketplace(params).await?;
let mut klines = KrxParser::parse_klines(&response, code)?;
klines.sort_by_key(|k| k.open_time);
Ok(klines)
}
async fn ping(&self) -> ExchangeResult<()> {
let today = format_today();
let body = serde_json::json!({
"basDd": today
});
let _ = self.post_openapi(KrxEndpoint::KospiDailyTrading, body).await?;
Ok(())
}
async fn get_exchange_info(&self, account_type: AccountType) -> ExchangeResult<Vec<SymbolInfo>> {
let today = format_today();
let body = serde_json::json!({
"basDd": today
});
let response = self.post_openapi(KrxEndpoint::KospiBaseInfo, body).await?;
let symbols = KrxParser::parse_symbols(&response)?;
let infos = symbols.into_iter().map(|code| SymbolInfo {
symbol: code.clone(),
base_asset: code,
quote_asset: "KRW".to_string(),
status: "TRADING".to_string(),
price_precision: 0,
quantity_precision: 0,
min_quantity: Some(1.0),
max_quantity: None,
tick_size: None,
step_size: Some(1.0),
min_notional: None,
account_type,
}).collect();
Ok(infos)
}
}
#[async_trait]
impl Trading for KrxConnector {
async fn place_order(&self, _req: OrderRequest) -> ExchangeResult<PlaceOrderResponse> {
Err(ExchangeError::UnsupportedOperation(
"KRX is a data provider - trading not supported".to_string()
))
}
async fn cancel_order(&self, _req: CancelRequest) -> ExchangeResult<Order> {
Err(ExchangeError::UnsupportedOperation(
"KRX is a data provider - trading not supported".to_string()
))
}
async fn get_order(
&self,
_symbol: &str,
_order_id: &str,
_account_type: AccountType,
) -> ExchangeResult<Order> {
Err(ExchangeError::UnsupportedOperation(
"KRX is a data provider - trading not supported".to_string()
))
}
async fn get_open_orders(
&self,
_symbol: Option<&str>,
_account_type: AccountType,
) -> ExchangeResult<Vec<Order>> {
Err(ExchangeError::UnsupportedOperation(
"KRX is a data provider - trading not supported".to_string()
))
}
async fn get_order_history(
&self,
_filter: OrderHistoryFilter,
_account_type: AccountType,
) -> ExchangeResult<Vec<Order>> {
Err(ExchangeError::UnsupportedOperation(
"KRX is a data provider - trading not supported".to_string()
))
}
}
#[async_trait]
impl Account for KrxConnector {
async fn get_balance(&self, _query: BalanceQuery) -> ExchangeResult<Vec<Balance>> {
Err(ExchangeError::UnsupportedOperation(
"KRX is a data provider - account operations not supported".to_string(),
))
}
async fn get_account_info(&self, _account_type: AccountType) -> ExchangeResult<AccountInfo> {
Err(ExchangeError::UnsupportedOperation(
"KRX is a data provider - account operations not supported".to_string(),
))
}
async fn get_fees(&self, _symbol: Option<&str>) -> ExchangeResult<FeeInfo> {
Err(ExchangeError::UnsupportedOperation(
"KRX is a data provider - account operations not supported".to_string()
))
}
}
#[async_trait]
impl Positions for KrxConnector {
async fn get_positions(&self, _query: PositionQuery) -> ExchangeResult<Vec<Position>> {
Err(ExchangeError::UnsupportedOperation(
"KRX is a data provider - position tracking not supported".to_string()
))
}
async fn get_funding_rate(
&self,
_symbol: &str,
_account_type: AccountType,
) -> ExchangeResult<FundingRate> {
Err(ExchangeError::UnsupportedOperation(
"KRX is a data provider - position tracking not supported".to_string()
))
}
async fn modify_position(&self, _req: PositionModification) -> ExchangeResult<()> {
Err(ExchangeError::UnsupportedOperation(
"KRX is a data provider - position tracking not supported".to_string()
))
}
}
impl KrxConnector {
pub async fn get_stock_info(&self, ticker: &str) -> ExchangeResult<serde_json::Value> {
let mut params = HashMap::new();
params.insert("likeSrtnCd".to_string(), ticker.to_string());
let response = self.get_portal(params).await?;
let items = KrxParser::parse_stock_info(&response)?;
if let Some(first) = items.first() {
Ok(first.clone())
} else {
Err(ExchangeError::NotFound(format!("Stock '{}' not found", ticker)))
}
}
pub async fn get_base_info(
&self,
date: &str,
market: MarketId,
) -> ExchangeResult<serde_json::Value> {
let endpoint = match market {
MarketId::Kospi => KrxEndpoint::KospiBaseInfo,
MarketId::Kosdaq => KrxEndpoint::KosdaqBaseInfo,
MarketId::Konex => KrxEndpoint::KonexBaseInfo,
MarketId::All => KrxEndpoint::KospiBaseInfo,
};
let body = serde_json::json!({
"basDd": date
});
self.post_openapi(endpoint, body).await
}
pub async fn get_index_data(&self, date: &str) -> ExchangeResult<serde_json::Value> {
let body = serde_json::json!({
"basDd": date
});
self.post_openapi(KrxEndpoint::IndexDailyTrading, body).await
}
}