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::*;
fn detect_market(code: &str) -> MarketId {
let first_digit = code.chars().next().unwrap_or('0');
match first_digit {
'2' | '3' => MarketId::Kosdaq,
_ => MarketId::Kospi,
}
}
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 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)
}
async fn get_daily_data(
&self,
_symbol: &Symbol,
date: &str,
market: MarketId,
) -> ExchangeResult<serde_json::Value> {
let endpoint = market.daily_trading_endpoint();
let body = serde_json::json!({
"basDd": date
});
self.post_openapi(endpoint, body).await
}
}
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: Symbol, _account_type: AccountType) -> ExchangeResult<Price> {
let klines = self.get_klines(symbol, "1d", Some(1), AccountType::Spot, None).await?;
if let Some(latest) = klines.first() {
Ok(latest.close)
} else {
Err(ExchangeError::NotFound("No price data available".to_string()))
}
}
async fn get_ticker(&self, symbol: Symbol, _account_type: AccountType) -> ExchangeResult<Ticker> {
let klines = self.get_klines(symbol.clone(), "1d", Some(1), AccountType::Spot, None).await?;
if let Some(latest) = klines.first() {
Ok(Ticker {
symbol: symbol.base.clone(),
last_price: latest.close,
bid_price: None,
ask_price: None,
high_24h: Some(latest.high),
low_24h: Some(latest.low),
volume_24h: Some(latest.volume),
quote_volume_24h: latest.quote_volume,
price_change_24h: None,
price_change_percent_24h: None,
timestamp: latest.open_time,
})
} else {
Err(ExchangeError::NotFound("No ticker data available".to_string()))
}
}
async fn get_orderbook(
&self,
_symbol: Symbol,
_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: Symbol,
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 limit = limit.unwrap_or(30) as i64;
use chrono::{Duration, Local, Datelike};
let end = Local::now();
let start = end - Duration::days(limit - 1);
let mut dates = Vec::new();
let mut current = start;
while current <= end {
dates.push(format_date(current.year(), current.month(), current.day()));
current += Duration::days(1);
}
let market = detect_market(&symbol.base);
let mut all_klines = Vec::new();
for date in dates {
match self.get_daily_data(&symbol, &date, market).await {
Ok(response) => {
match KrxParser::parse_klines(&response, &symbol.base) {
Ok(mut klines) => all_klines.append(&mut klines),
Err(_) => continue, }
}
Err(_) => continue, }
}
all_klines.sort_by_key(|k| k.open_time);
Ok(all_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
}
}