Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,4 @@ jobs:
- uses: rustsec/audit-check@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
ignore: RUSTSEC-2023-0071, RUSTSEC-2024-0436
ignore: RUSTSEC-2023-0071, RUSTSEC-2024-0436, RUSTSEC-2026-0001
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ go/
.kiro/
.roo/
.specify/
specs/
.claude/
.gemini/
_bmad/
docs/
Expand Down
21 changes: 6 additions & 15 deletions ccxt-exchanges/src/binance/parser/funding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,8 @@ pub fn parse_funding_rate(data: &Value, market: Option<&Market>) -> Result<FeeFu
estimated_settle_price: None,
funding_rate,
funding_timestamp,
funding_datetime: funding_timestamp.map(|t| {
chrono::DateTime::from_timestamp(t / 1000, 0)
.map(|dt| dt.to_rfc3339())
.unwrap_or_default()
}),
funding_datetime: funding_timestamp
.and_then(|t| chrono::DateTime::from_timestamp(t / 1000, 0).map(|dt| dt.to_rfc3339())),
next_funding_rate: None,
next_funding_timestamp: None,
next_funding_datetime: None,
Expand Down Expand Up @@ -79,17 +76,11 @@ pub fn parse_funding_rate_history(
symbol,
funding_rate,
funding_timestamp: funding_time,
funding_datetime: funding_time.map(|t| {
chrono::DateTime::from_timestamp(t / 1000, 0)
.map(|dt| dt.to_rfc3339())
.unwrap_or_default()
}),
funding_datetime: funding_time
.and_then(|t| chrono::DateTime::from_timestamp(t / 1000, 0).map(|dt| dt.to_rfc3339())),
timestamp: funding_time,
datetime: funding_time.map(|t| {
chrono::DateTime::from_timestamp(t / 1000, 0)
.map(|dt| dt.to_rfc3339())
.unwrap_or_default()
}),
datetime: funding_time
.and_then(|t| chrono::DateTime::from_timestamp(t / 1000, 0).map(|dt| dt.to_rfc3339())),
})
}

Expand Down
46 changes: 37 additions & 9 deletions ccxt-exchanges/src/bitget/exchange_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,17 +134,45 @@ impl Exchange for Bitget {
let ohlcv_data = Bitget::fetch_ohlcv(self, symbol, &timeframe_str, since, limit).await?;

// Convert OHLCV to Ohlcv with proper type conversions
Ok(ohlcv_data
ohlcv_data
.into_iter()
.map(|o| Ohlcv {
timestamp: o.timestamp,
open: Price::from(Decimal::try_from(o.open).unwrap_or_default()),
high: Price::from(Decimal::try_from(o.high).unwrap_or_default()),
low: Price::from(Decimal::try_from(o.low).unwrap_or_default()),
close: Price::from(Decimal::try_from(o.close).unwrap_or_default()),
volume: Amount::from(Decimal::try_from(o.volume).unwrap_or_default()),
.map(|o| -> ccxt_core::Result<Ohlcv> {
Ok(Ohlcv {
timestamp: o.timestamp,
open: Price(Decimal::try_from(o.open).map_err(|e| {
ccxt_core::Error::from(ccxt_core::ParseError::invalid_value(
"OHLCV open",
format!("{e}"),
))
})?),
high: Price(Decimal::try_from(o.high).map_err(|e| {
ccxt_core::Error::from(ccxt_core::ParseError::invalid_value(
"OHLCV high",
format!("{e}"),
))
})?),
low: Price(Decimal::try_from(o.low).map_err(|e| {
ccxt_core::Error::from(ccxt_core::ParseError::invalid_value(
"OHLCV low",
format!("{e}"),
))
})?),
close: Price(Decimal::try_from(o.close).map_err(|e| {
ccxt_core::Error::from(ccxt_core::ParseError::invalid_value(
"OHLCV close",
format!("{e}"),
))
})?),
volume: Amount(Decimal::try_from(o.volume).map_err(|e| {
ccxt_core::Error::from(ccxt_core::ParseError::invalid_value(
"OHLCV volume",
format!("{e}"),
))
})?),
})
})
.collect())
.collect::<ccxt_core::Result<Vec<Ohlcv>>>()
.map_err(|e| e.context("Failed to convert Bitget OHLCV data"))
}

// ==================== Trading (Private API) ====================
Expand Down
15 changes: 12 additions & 3 deletions ccxt-exchanges/src/bitget/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,10 @@ pub fn parse_ticker(data: &Value, market: Option<&Market>) -> Result<Ticker> {
.as_str()
.or_else(|| data["instId"].as_str())
.map(ToString::to_string)
.unwrap_or_default()
.ok_or_else(|| {
ccxt_core::Error::from(ccxt_core::ParseError::missing_field("symbol/instId"))
.context("Failed to parse ticker: missing symbol identifier")
})?
};

// Bitget uses "ts" for timestamp
Expand Down Expand Up @@ -318,7 +321,10 @@ pub fn parse_trade(data: &Value, market: Option<&Market>) -> Result<Trade> {
data["symbol"]
.as_str()
.map(ToString::to_string)
.unwrap_or_default()
.ok_or_else(|| {
ccxt_core::Error::from(ccxt_core::ParseError::missing_field("symbol"))
.context("Failed to parse trade: missing symbol identifier")
})?
};

let id = data["tradeId"]
Expand Down Expand Up @@ -472,7 +478,10 @@ pub fn parse_order(data: &Value, market: Option<&Market>) -> Result<Order> {
.as_str()
.or_else(|| data["instId"].as_str())
.map(ToString::to_string)
.unwrap_or_default()
.ok_or_else(|| {
ccxt_core::Error::from(ccxt_core::ParseError::missing_field("symbol/instId"))
.context("Failed to parse order: missing symbol identifier")
})?
};

let id = data["orderId"]
Expand Down
12 changes: 9 additions & 3 deletions ccxt-exchanges/src/bitget/rest/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,15 @@ impl Bitget {
String::new()
};

let body_string = body
.map(|b| serde_json::to_string(b).unwrap_or_default())
.unwrap_or_default();
let body_string = match body {
Some(b) => serde_json::to_string(b).map_err(|e| {
ccxt_core::Error::from(ccxt_core::ParseError::invalid_format(
"request body",
format!("JSON serialization failed: {}", e),
))
})?,
None => String::new(),
};

let sign_path = format!("{}{}", path, query_string);
let signature = auth.sign(&timestamp, method, &sign_path, &body_string);
Expand Down
19 changes: 16 additions & 3 deletions ccxt-exchanges/src/bybit/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,10 @@ pub fn parse_ticker(data: &Value, market: Option<&Market>) -> Result<Ticker> {
data["symbol"]
.as_str()
.map(ToString::to_string)
.unwrap_or_default()
.ok_or_else(|| {
ccxt_core::Error::from(ccxt_core::ParseError::missing_field("symbol"))
.context("Failed to parse ticker: missing symbol identifier")
})?
};

// Bybit uses different timestamp fields
Expand Down Expand Up @@ -342,10 +345,15 @@ pub fn parse_trade(data: &Value, market: Option<&Market>) -> Result<Trade> {
let symbol = if let Some(m) = market {
m.symbol.clone()
} else {
// Check both "symbol" (REST API) and "s" (WebSocket) fields
data["symbol"]
.as_str()
.or_else(|| data["s"].as_str())
.map(ToString::to_string)
.unwrap_or_default()
.ok_or_else(|| {
ccxt_core::Error::from(ccxt_core::ParseError::missing_field("symbol/s"))
.context("Failed to parse: missing symbol identifier")
})?
};

let id = data["execId"]
Expand Down Expand Up @@ -556,10 +564,15 @@ pub fn parse_order(data: &Value, market: Option<&Market>) -> Result<Order> {
let symbol = if let Some(m) = market {
m.symbol.clone()
} else {
// Check both "symbol" (REST API) and "s" (WebSocket) fields
data["symbol"]
.as_str()
.or_else(|| data["s"].as_str())
.map(ToString::to_string)
.unwrap_or_default()
.ok_or_else(|| {
ccxt_core::Error::from(ccxt_core::ParseError::missing_field("symbol/s"))
.context("Failed to parse: missing symbol identifier")
})?
};

let id = data["orderId"]
Expand Down
12 changes: 9 additions & 3 deletions ccxt-exchanges/src/bybit/rest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,15 @@ impl Bybit {
};

// Build body string for POST requests
let body_string = body
.map(|b| serde_json::to_string(b).unwrap_or_default())
.unwrap_or_default();
let body_string = match body {
Some(b) => serde_json::to_string(b).map_err(|e| {
ccxt_core::Error::from(ccxt_core::ParseError::invalid_format(
"request body",
format!("JSON serialization failed: {}", e),
))
})?,
None => String::new(),
};

// Sign the request - Bybit uses query string for GET, body for POST
let sign_params = if method.to_uppercase() == "GET" {
Expand Down
7 changes: 6 additions & 1 deletion ccxt-exchanges/src/bybit/signed_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,12 @@ impl<'a> BybitSignedRequestBuilder<'a> {
body.to_string()
} else if !self.params.is_empty() {
// Convert params to JSON for POST/DELETE
serde_json::to_string(&self.params).unwrap_or_default()
serde_json::to_string(&self.params).map_err(|e| {
ccxt_core::Error::from(ccxt_core::ParseError::invalid_format(
"request params",
format!("JSON serialization failed: {}", e),
))
})?
} else {
String::new()
};
Expand Down
46 changes: 37 additions & 9 deletions ccxt-exchanges/src/okx/exchange_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,17 +143,45 @@ impl Exchange for Okx {
let ohlcv_data = Okx::fetch_ohlcv(self, symbol, &timeframe_str, since, limit).await?;

// Convert OHLCV to Ohlcv with proper type conversions
Ok(ohlcv_data
ohlcv_data
.into_iter()
.map(|o| Ohlcv {
timestamp: o.timestamp,
open: Price::from(Decimal::try_from(o.open).unwrap_or_default()),
high: Price::from(Decimal::try_from(o.high).unwrap_or_default()),
low: Price::from(Decimal::try_from(o.low).unwrap_or_default()),
close: Price::from(Decimal::try_from(o.close).unwrap_or_default()),
volume: Amount::from(Decimal::try_from(o.volume).unwrap_or_default()),
.map(|o| -> ccxt_core::Result<Ohlcv> {
Ok(Ohlcv {
timestamp: o.timestamp,
open: Price(Decimal::try_from(o.open).map_err(|e| {
ccxt_core::Error::from(ccxt_core::ParseError::invalid_value(
"OHLCV open",
format!("{e}"),
))
})?),
high: Price(Decimal::try_from(o.high).map_err(|e| {
ccxt_core::Error::from(ccxt_core::ParseError::invalid_value(
"OHLCV high",
format!("{e}"),
))
})?),
low: Price(Decimal::try_from(o.low).map_err(|e| {
ccxt_core::Error::from(ccxt_core::ParseError::invalid_value(
"OHLCV low",
format!("{e}"),
))
})?),
close: Price(Decimal::try_from(o.close).map_err(|e| {
ccxt_core::Error::from(ccxt_core::ParseError::invalid_value(
"OHLCV close",
format!("{e}"),
))
})?),
volume: Amount(Decimal::try_from(o.volume).map_err(|e| {
ccxt_core::Error::from(ccxt_core::ParseError::invalid_value(
"OHLCV volume",
format!("{e}"),
))
})?),
})
})
.collect())
.collect::<ccxt_core::Result<Vec<Ohlcv>>>()
.map_err(|e| e.context("Failed to convert OKX OHLCV data"))
}

// ==================== Trading (Private API) ====================
Expand Down