Skip to content

Commit 585d49a

Browse files
committed
feat: bybit klines
1 parent 5a008c2 commit 585d49a

File tree

10 files changed

+168
-48
lines changed

10 files changed

+168
-48
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ chrono = "^0.4.39"
1818
color-eyre = "^0.6.3"
1919
derive-new = "^0"
2020
futures-util = "^0.3.31"
21-
reqwest = { version = "^0.12.9", features = ["blocking", "json"] }
22-
serde = { version = "^1.0.216", features = ["derive"] }
21+
reqwest = { version = "^0.12.12", features = ["blocking", "json"] }
22+
serde = { version = "^1.0.217", features = ["derive"] }
2323
serde_plain = "^1.0.2" #TEST
24-
serde_json = "^1.0.133"
25-
serde_with = "^3.11.0"
26-
thiserror = "^2.0.7"
24+
serde_json = "^1.0.134"
25+
serde_with = "^3.12.0"
26+
thiserror = "^2.0.9"
2727
tokio = { version = "^1.42.0", features = ["full"] }
28-
tokio-tungstenite = { version = "^0.26.0", features = ["native-tls"] }
28+
tokio-tungstenite = { version = "^0.26.1", features = ["native-tls"] }
2929
tracing = "^0.1.41"
3030
url = "^2.5.4"
3131
hmac = "^0.12.1"

examples/binance/market.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,6 @@ async fn main() {
1616
let price = b.futures_price(("BTC", "USDT").into()).await.unwrap();
1717
dbg!(&klines, price);
1818

19-
let trades: serde_json::Value = b.get("/fapi/v1/aggTrades", &[("symbol", "BTCUSDT"), ("limit", "2")], [BinanceOption::Default]).await.unwrap();
20-
dbg!(&trades);
21-
2219
if let (Ok(key), Ok(secret)) = (env::var("BINANCE_TIGER_READ_KEY"), env::var("BINANCE_TIGER_READ_SECRET")) {
2320
b.update_default_option(BinanceOption::Key(key));
2421
b.update_default_option(BinanceOption::Secret(secret));

examples/bybit/market.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ async fn main() {
1010

1111
let mut bb = Bybit::default();
1212

13+
//let ticker: serde_json::Value =
14+
//bb.get("/v5/market/tickers", &[("category", "spot"), ("symbol", "BTCUSDT")], [BybitOption::Default])
15+
// .await
16+
// .expect("failed to get ticker");
17+
//println!("Ticker:\n{ticker}");
18+
19+
let klines = bb.futures_klines(("BTC", "USDT").into(), "1m".into(), 2, None, None).await.unwrap();
20+
dbg!(&klines);
21+
1322
if let (Ok(key), Ok(secret)) = (env::var("BYBIT_TIGER_READ_KEY"), env::var("BYBIT_TIGER_READ_SECRET")) {
1423
bb.update_default_option(BybitOption::Key(key));
1524
bb.update_default_option(BybitOption::Secret(secret));

v_exchanges/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ serde_plain.workspace = true
3131
serde_with.workspace = true
3232
tokio.workspace = true
3333
tracing.workspace = true
34+
thiserror.workspace = true
3435
v_exchanges_adapters = { version = "^0.1.1", path = "../v_exchanges_adapters/", features = ["full"] }
3536
v_utils = { workspace = true }
3637
chrono = "0.4.39"

v_exchanges/src/binance/futures/market.rs

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use chrono::{DateTime, TimeZone, Utc};
22
//HACK: Methods should be implemented on the central interface struct, following <https://github.com/wisespace-io/binance-rs>.
3-
use color_eyre::eyre::Result;
3+
use color_eyre::eyre::{self, Error, Result};
4+
use color_eyre::eyre::{Report, eyre};
45
use serde::{Deserialize, Serialize};
56
use serde_json::{Value, json};
67
use serde_with::{DisplayFromStr, serde_as};
@@ -21,10 +22,35 @@ pub async fn klines(client: &v_exchanges_adapters::Client, pair: Pair, tf: Timef
2122
"startTime": start_time,
2223
"endTime": end_time,
2324
}));
24-
2525
let kline_responses: Vec<KlineResponse> = client.get("/fapi/v1/klines", &params, [BinanceOption::HttpUrl(BinanceHttpUrl::FuturesUsdM)]).await.unwrap();
26-
let klines: Vec<Kline> = kline_responses.into_iter().map(Kline::from).collect();
2726

27+
let r_len = kline_responses.len();
28+
let mut klines = Vec::with_capacity(r_len);
29+
for (i, k) in kline_responses.into_iter().enumerate() {
30+
//HACK: have to check against [now](Utc::now) instead, because binance returns some dumb shit instead of actual close. Here structured this way in case they fix it in the future.
31+
let close_time = Utc::now().timestamp_millis();
32+
match close_time > k.open_time + (0.99 * tf.duration().num_milliseconds() as f64) as i64 {
33+
true => {
34+
let ohlc = Ohlc {
35+
open: k.open,
36+
high: k.high,
37+
low: k.low,
38+
close: k.close,
39+
};
40+
klines.push(Kline {
41+
open_time: DateTime::from_timestamp_millis(k.open_time).unwrap(),
42+
ohlc,
43+
volume_quote: k.quote_asset_volume,
44+
trades: Some(k.number_of_trades),
45+
taker_buy_volume_quote: Some(k.taker_buy_quote_asset_volume),
46+
});
47+
}
48+
false => match i == r_len - 1 {
49+
true => tracing::trace!("Skipped last kline in binance request, as it's incomplete (expected behavior)"),
50+
false => tracing::warn!("Skipped kline in binance request, as it's incomplete"),
51+
},
52+
}
53+
}
2854
Ok(Klines { v: klines, tf, oi: Vec::new() })
2955
}
3056

@@ -46,7 +72,8 @@ pub struct KlineResponse {
4672
pub low: f64,
4773
#[serde_as(as = "DisplayFromStr")]
4874
pub volume: f64,
49-
pub close_time: i64,
75+
/// As of today (2025/01/03), means NOTHING, as they will still send what it _SHOULD_ be even if the kline is not yet finished. (fuck you, binance)
76+
__close_time: i64,
5077
#[serde_as(as = "DisplayFromStr")]
5178
pub quote_asset_volume: f64,
5279
pub number_of_trades: usize,
@@ -57,25 +84,6 @@ pub struct KlineResponse {
5784

5885
__ignore: Option<Value>,
5986
}
60-
impl From<KlineResponse> for Kline {
61-
fn from(k: KlineResponse) -> Self {
62-
let ohlc = Ohlc {
63-
open: k.open,
64-
high: k.high,
65-
low: k.low,
66-
close: k.close,
67-
};
68-
Kline {
69-
open_time: DateTime::from_timestamp_millis(k.open_time).unwrap(),
70-
ohlc,
71-
volume_quote: k.quote_asset_volume,
72-
//TODO!!!!!!: before adding check that it is not less than start_time + tf
73-
trades: Some(k.number_of_trades),
74-
taker_buy_volume_quote: Some(k.taker_buy_quote_asset_volume),
75-
close_time: Some(Utc.timestamp_millis_opt(k.close_time).unwrap()),
76-
}
77-
}
78-
}
7987
//,}}}
8088

8189
// price {{{

v_exchanges/src/bybit/account.rs

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
use std::str::FromStr;
2-
use v_utils::macros::ScreamIt;
32

43
use color_eyre::eyre::Result;
54
use serde::{Deserialize, Serialize};
65
use serde_json::Value;
6+
use serde_with::{DisplayFromStr, serde_as};
77
use v_exchanges_adapters::bybit::{BybitHttpAuth, BybitOption};
8-
use serde_with::{serde_as, DisplayFromStr};
8+
use v_utils::{macros::ScreamIt, trades::Asset};
99

1010
use crate::core::AssetBalance;
11-
use v_utils::trades::Asset;
1211

1312
pub async fn asset_balance(client: &v_exchanges_adapters::Client, asset: Asset) -> Result<AssetBalance> {
1413
let balances = balances(client).await?;
@@ -28,18 +27,15 @@ pub async fn balances(client: &v_exchanges_adapters::Client) -> Result<Vec<Asset
2827

2928
let mut balances = Vec::new();
3029
for r in &account_info.coin {
31-
balances.push(
32-
AssetBalance {
33-
asset: (&*r.coin).into(),
34-
balance: r.wallet_balance,
35-
timestamp: account_response.time,
36-
}
37-
);
30+
balances.push(AssetBalance {
31+
asset: (&*r.coin).into(),
32+
balance: r.wallet_balance,
33+
timestamp: account_response.time,
34+
});
3835
}
3936
Ok(balances)
4037
}
4138

42-
4339
#[derive(Debug, Clone, ScreamIt, Copy)]
4440
pub enum AccountType {
4541
Spot,
@@ -91,7 +87,7 @@ pub struct AccountInfo {
9187
pub struct CoinInfo {
9288
pub accrued_interest: String,
9389
/// deprecated
94-
pub available_to_borrow: Option<Value>, //? can I start it with __, will serde understand?
90+
__available_to_borrow: Option<Value>, //? can I start it with __, will serde understand?
9591
pub available_to_withdraw: String,
9692
pub bonus: String,
9793
pub borrow_amount: String,

v_exchanges/src/bybit/market.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
use std::fmt;
2+
3+
use chrono::{DateTime, TimeZone, Utc};
4+
use color_eyre::eyre::Result;
5+
use serde::{Deserialize, Serialize};
6+
use serde_json::{Value, json};
7+
use serde_with::{DisplayFromStr, serde_as};
8+
use thiserror::Error;
9+
use v_exchanges_adapters::bybit::{BybitHttpUrl, BybitOption};
10+
use v_utils::{
11+
trades::{Kline, Ohlc, Pair, Timeframe},
12+
utils::filter_nulls,
13+
};
14+
15+
use crate::core::Klines;
16+
17+
//MOVE: centralized error module
18+
#[derive(Debug)]
19+
struct LimitOutOfRangeError {
20+
allowed: std::ops::RangeInclusive<u32>,
21+
provided: u32,
22+
}
23+
impl fmt::Display for LimitOutOfRangeError {
24+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25+
write!(f, "Limit out of range. Allowed: {:?}, provided: {}", self.allowed, self.provided)
26+
}
27+
}
28+
impl std::error::Error for LimitOutOfRangeError {}
29+
30+
pub async fn klines(client: &v_exchanges_adapters::Client, pair: Pair, tf: Timeframe, limit: u32, start_time: Option<u64>, end_time: Option<u64>) -> Result<Klines> {
31+
let range = 1..=1000;
32+
if !range.contains(&limit) {
33+
return Err(LimitOutOfRangeError { allowed: range, provided: limit }.into());
34+
}
35+
36+
let mut params = filter_nulls(json!({
37+
"category": "linear", // can be ["linear", "inverse", "spot"] afaiu, could drive some generics with this later, but for now hardcode
38+
"symbol": pair.to_string(),
39+
"interval": tf.format_bybit()?,
40+
"limit": limit,
41+
"startTime": start_time,
42+
"endTime": end_time,
43+
}));
44+
let kline_response: KlineResponse = client.get("/v5/market/kline", &params, [BybitOption::Default]).await.unwrap();
45+
46+
let mut klines = Vec::new();
47+
for k in kline_response.result.list {
48+
if kline_response.time > k.0 + tf.duration().num_milliseconds() {
49+
klines.push(Kline {
50+
open_time: Utc.timestamp_millis(k.0),
51+
ohlc: Ohlc {
52+
open: k.1,
53+
close: k.2,
54+
high: k.3,
55+
low: k.4,
56+
},
57+
volume_quote: k.5,
58+
trades: None,
59+
taker_buy_volume_quote: None,
60+
});
61+
}
62+
}
63+
Ok(Klines { v: klines, tf, oi: Vec::new() })
64+
}
65+
66+
#[derive(Debug, Serialize, Deserialize)]
67+
#[serde(rename_all = "camelCase")]
68+
pub struct KlineResponse {
69+
pub result: ResponseResult,
70+
pub ret_code: i32,
71+
pub ret_ext_info: std::collections::HashMap<String, serde_json::Value>,
72+
pub ret_msg: String,
73+
pub time: i64,
74+
}
75+
76+
#[derive(Debug, Serialize, Deserialize)]
77+
#[serde(rename_all = "camelCase")]
78+
pub struct ResponseResult {
79+
pub category: String,
80+
pub list: Vec<KlineData>,
81+
pub symbol: String,
82+
}
83+
84+
#[serde_as]
85+
#[derive(Debug, Serialize, Deserialize)]
86+
pub struct KlineData(
87+
#[serde_as(as = "DisplayFromStr")] pub i64,
88+
#[serde_as(as = "DisplayFromStr")] pub f64,
89+
#[serde_as(as = "DisplayFromStr")] pub f64,
90+
#[serde_as(as = "DisplayFromStr")] pub f64,
91+
#[serde_as(as = "DisplayFromStr")] pub f64,
92+
#[serde_as(as = "DisplayFromStr")] pub f64,
93+
#[serde_as(as = "DisplayFromStr")] pub f64,
94+
);

v_exchanges/src/bybit/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
pub mod account;
1+
mod account;
2+
mod market;
23

34
use color_eyre::eyre::Result;
45
use derive_more::derive::{Deref, DerefMut};
@@ -13,8 +14,7 @@ pub struct Bybit(pub Client);
1314
//? currently client ends up importing this from crate::binance, but could it be possible to lift the [Client] reexport up, and still have the ability to call all exchange methods right on it?
1415
impl Exchange for Bybit {
1516
async fn futures_klines(&self, symbol: Pair, tf: Timeframe, limit: u32, start_time: Option<u64>, end_time: Option<u64>) -> Result<Klines> {
16-
//futures::market::klines(&self.0, symbol, tf, limit, start_time, end_time).await
17-
todo!();
17+
market::klines(&self.0, symbol, tf, limit, start_time, end_time).await
1818
}
1919

2020
async fn futures_price(&self, symbol: Pair) -> Result<f64> {

v_exchanges/src/core.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
use chrono::{DateTime, Utc};
2-
use color_eyre::eyre::Result;
2+
use color_eyre::eyre::{Error, Result};
33
use tokio::sync::mpsc;
44
use v_exchanges_adapters::traits::HandlerOptions;
55
use v_utils::trades::{Asset, Kline, Pair, Timeframe};
66

7+
//TODO!!!!!!!!!!!!!: klines switch to defining the range via an Enum over either limit either start and end times
8+
79
pub trait Exchange {
810
//? should I have Self::Pair too? Like to catch the non-existent ones immediately? Although this would increase the error surface on new listings.
911
fn futures_klines(&self, symbol: Pair, tf: Timeframe, limit: u32, start_time: Option<u64>, end_time: Option<u64>) -> impl std::future::Future<Output = Result<Klines>> + Send;
@@ -38,6 +40,18 @@ pub struct Klines {
3840
/// Doesn't have to be synchronized with klines; each track has its own timestamps.
3941
pub oi: Vec<Oi>,
4042
}
43+
44+
//MOVE: v_utils (along with [Klines])
45+
//? not sure what to do about oi here
46+
/// [Kline]s series that is _guaranteed to not have any gaps in kline data_.
47+
#[derive(Clone, Debug, Default)]
48+
struct FullKlines(Klines);
49+
impl TryFrom<Klines> for FullKlines {
50+
type Error = color_eyre::eyre::Report;
51+
fn try_from(value: Klines) -> Result<Self> {
52+
todo!();
53+
}
54+
}
4155
//,}}}
4256

4357
#[derive(Clone, Debug, Default, Copy)]

0 commit comments

Comments
 (0)