├── .editorconfig ├── .github ├── CODEOWNERS └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── crypto-contract-value ├── Cargo.toml ├── README.md └── src │ ├── exchanges │ ├── binance.rs │ ├── bitfinex.rs │ ├── bitget.rs │ ├── bitmex.rs │ ├── bybit.rs │ ├── deribit.rs │ ├── gate.rs │ ├── huobi.rs │ ├── kraken.rs │ ├── kucoin.rs │ ├── mexc.rs │ ├── mod.rs │ ├── okx.rs │ ├── utils.rs │ └── zbg.rs │ └── lib.rs ├── crypto-message ├── Cargo.toml ├── README.md └── src │ ├── compact │ ├── message.rs │ ├── mod.rs │ └── order.rs │ ├── lib.rs │ ├── order.rs │ └── proto │ ├── README.md │ ├── message.proto │ ├── message.rs │ └── mod.rs ├── crypto-msg-parser.svg ├── crypto-msg-parser ├── Cargo.toml ├── README.md ├── src │ ├── exchanges │ │ ├── binance │ │ │ ├── binance_all.rs │ │ │ ├── binance_option.rs │ │ │ ├── binance_spot.rs │ │ │ └── mod.rs │ │ ├── bitfinex.rs │ │ ├── bitget │ │ │ ├── before20220429 │ │ │ │ ├── bitget_swap.rs │ │ │ │ └── mod.rs │ │ │ ├── bitget_mix.rs │ │ │ └── mod.rs │ │ ├── bithumb.rs │ │ ├── bitmex.rs │ │ ├── bitstamp.rs │ │ ├── bitz.rs │ │ ├── bybit.rs │ │ ├── coinbase_pro.rs │ │ ├── deribit.rs │ │ ├── dydx │ │ │ ├── dydx_swap.rs │ │ │ ├── message.rs │ │ │ └── mod.rs │ │ ├── ftx.rs │ │ ├── gate │ │ │ ├── gate_spot.rs │ │ │ ├── gate_spot_20210916.rs │ │ │ ├── gate_spot_current.rs │ │ │ ├── gate_swap.rs │ │ │ ├── messages.rs │ │ │ └── mod.rs │ │ ├── huobi │ │ │ ├── funding_rate.rs │ │ │ ├── huobi_inverse.rs │ │ │ ├── huobi_linear.rs │ │ │ ├── huobi_spot.rs │ │ │ ├── message.rs │ │ │ └── mod.rs │ │ ├── kraken │ │ │ ├── kraken_futures.rs │ │ │ ├── kraken_spot.rs │ │ │ └── mod.rs │ │ ├── kucoin │ │ │ ├── kucoin_spot.rs │ │ │ ├── kucoin_swap.rs │ │ │ ├── message.rs │ │ │ └── mod.rs │ │ ├── mexc │ │ │ ├── mexc_spot.rs │ │ │ ├── mexc_swap.rs │ │ │ └── mod.rs │ │ ├── mod.rs │ │ ├── okx │ │ │ ├── mod.rs │ │ │ ├── okx_v3.rs │ │ │ └── okx_v5.rs │ │ ├── utils.rs │ │ ├── zb │ │ │ ├── mod.rs │ │ │ ├── zb_spot.rs │ │ │ └── zb_swap.rs │ │ └── zbg │ │ │ ├── mod.rs │ │ │ ├── zbg_spot.rs │ │ │ └── zbg_swap.rs │ └── lib.rs └── tests │ ├── binance.rs │ ├── bitfinex.rs │ ├── bitget.rs │ ├── bithumb.rs │ ├── bitmex.rs │ ├── bitstamp.rs │ ├── bitz.rs │ ├── bybit.rs │ ├── coinbase_pro.rs │ ├── deribit.rs │ ├── dydx.rs │ ├── ftx.rs │ ├── gate.rs │ ├── huobi.rs │ ├── kraken.rs │ ├── kucoin.rs │ ├── mexc.rs │ ├── okx.rs │ ├── okx_v3.rs │ ├── utils.rs │ ├── zb.rs │ └── zbg.rs ├── crypto-pair ├── Cargo.toml ├── README.md ├── src │ ├── exchanges │ │ ├── binance.rs │ │ ├── bitfinex.rs │ │ ├── bitget.rs │ │ ├── bitmex.rs │ │ ├── bitstamp.rs │ │ ├── bybit.rs │ │ ├── deribit.rs │ │ ├── dydx.rs │ │ ├── ftx.rs │ │ ├── gate.rs │ │ ├── huobi.rs │ │ ├── kraken.rs │ │ ├── kucoin.rs │ │ ├── mexc.rs │ │ ├── mod.rs │ │ ├── okx.rs │ │ ├── utils.rs │ │ ├── zb.rs │ │ └── zbg.rs │ └── lib.rs └── tests │ ├── binance.rs │ ├── bitfinex.rs │ ├── bitget.rs │ ├── bithumb.rs │ ├── bitmex.rs │ ├── bitstamp.rs │ ├── bitz.rs │ ├── bybit.rs │ ├── coinbase_pro.rs │ ├── deribit.rs │ ├── dydx.rs │ ├── ftx.rs │ ├── gate.rs │ ├── huobi.rs │ ├── kraken.rs │ ├── kucoin.rs │ ├── mexc.rs │ ├── okx.rs │ ├── utils │ └── mod.rs │ ├── zb.rs │ └── zbg.rs └── rustfmt.toml /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://EditorConfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 4 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | # Markdown Files 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | 16 | # Batch Files 17 | [*.{cmd,bat}] 18 | end_of_line = crlf 19 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @soulmachine 2 | 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | CARGO_TERM_COLOR: always 7 | 8 | # Stop the previous CI tasks (which is deprecated) 9 | # to conserve the runner resource. 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | build: 16 | name: Cargo build 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v2 20 | - uses: actions-rs/toolchain@v1 21 | with: 22 | toolchain: nightly 23 | override: true 24 | - uses: actions-rs/cargo@v1 25 | with: 26 | command: build 27 | args: --release --all-features 28 | 29 | test: 30 | name: Cargo test 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v2 34 | - uses: actions-rs/toolchain@v1 35 | with: 36 | toolchain: nightly 37 | override: true 38 | - uses: actions-rs/cargo@v1 39 | with: 40 | command: test 41 | 42 | fmt: 43 | name: Cargo fmt 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v2 47 | - uses: actions-rs/toolchain@v1 48 | with: 49 | toolchain: nightly 50 | override: true 51 | components: rustfmt 52 | - uses: actions-rs/cargo@v1 53 | with: 54 | command: fmt 55 | args: -- --check 56 | 57 | check: 58 | name: Cargo check 59 | runs-on: ubuntu-latest 60 | steps: 61 | - uses: actions/checkout@v2 62 | - uses: actions-rs/toolchain@v1 63 | with: 64 | toolchain: nightly 65 | override: true 66 | - uses: actions-rs/cargo@v1 67 | with: 68 | command: check 69 | 70 | clippy: 71 | name: Cargo clippy 72 | runs-on: ubuntu-latest 73 | steps: 74 | - uses: actions/checkout@v2 75 | - uses: actions-rs/toolchain@v1 76 | with: 77 | toolchain: nightly 78 | override: true 79 | components: clippy 80 | - uses: actions-rs/cargo@v1 81 | with: 82 | command: clippy 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | # IDEA configurations 13 | /.idea 14 | *.iml 15 | *.project 16 | *.classpath 17 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "crypto-contract-value", 5 | "crypto-message", 6 | "crypto-msg-parser", 7 | "crypto-pair", 8 | ] 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # crypto-msg-parser 2 | 3 | The parser library to parse messages from [crypto-crawler](https://github.com/crypto-crawler/crypto-crawler-rs/tree/main/crypto-crawler). 4 | 5 | [![](https://img.shields.io/github/actions/workflow/status/crypto-crawler/crypto-msg-parser/ci.yml?branch=main)](https://github.com/crypto-crawler/crypto-msg-parser/actions?query=branch%3Amain) 6 | [![](https://img.shields.io/crates/v/crypto-msg-parser.svg)](https://crates.io/crates/crypto-msg-parser) 7 | [![](https://docs.rs/crypto-msg-parser/badge.svg)](https://docs.rs/crypto-msg-parser) [![Discord](https://img.shields.io/discord/1043987684164649020?logo=discord)](https://discord.gg/Vych8DNZU2) 8 | 9 | ## Architecture 10 | 11 | ![](./crypto-msg-parser.svg) 12 | 13 | - [crypto-msg-parser](./crypto-msg-parser) is the parser library to parse messages from `crypto-crawler`. 14 | - [crypto-pair](./crypto-pair) is an offline utility library to parse exchange-specific symbols to unified format. 15 | - [crypto-contract-value](./crypto-pair) is an offline utility library that simply provides the contract values of a trading market. 16 | - Support multiple languages. Some libraries support multiple languages, which is achieved by first providing a FFI binding, then a languge specific wrapper. For example, `crypto-crawler` provides a C-style FFI binding first, and then provides a Python wrapper and a C++ wrapper based on the FFI binding. 17 | - [crypto-message](./crypto-message) contains all output data types of `crypto-msg-parser`. 18 | -------------------------------------------------------------------------------- /crypto-contract-value/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crypto-contract-value" 3 | version = "1.7.25" 4 | authors = ["soulmachine "] 5 | edition = "2021" 6 | description = "Get contract value." 7 | license = "Apache-2.0" 8 | repository = "https://github.com/crypto-crawler/crypto-msg-parser/tree/main/crypto-contract-value" 9 | keywords = ["cryptocurrency", "blockchain", "trading"] 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | crypto-market-type = "1.1.6" 15 | crypto-pair = "2.3.20" 16 | once_cell = "1.19.0" 17 | reqwest = { version = "0.12.1", features = ["blocking", "gzip"] } 18 | serde = { version = "1.0.197", features = ["derive"] } 19 | serde_json = "1.0.114" 20 | -------------------------------------------------------------------------------- /crypto-contract-value/README.md: -------------------------------------------------------------------------------- 1 | # crypto-contract-value 2 | 3 | [![](https://img.shields.io/crates/v/crypto-contract-value.svg)](https://crates.io/crates/crypto-contract-value) 4 | [![](https://docs.rs/crypto-contract-value/badge.svg)](https://docs.rs/crypto-contract-value) 5 | ========== 6 | 7 | The value of an unit of contract diffs in different exchanges, and even in the same exchange it differs in different markets. 8 | 9 | For example: 10 | 11 | - Each Binance perpetual `BTCUSDT` contract is valued at 100 USD, and each alt coin contract is valued at 10 USD. 12 | - Each OKX `BTC-USDT-SWAP` contract is valued at 0.01 BTC. 13 | - Each OKX `BTC-USD-SWAP` contract is valued at 100 USD. 14 | - The contract value of spot markets is always 1. 15 | 16 | Given `quantity`, the number of traded coins/contracts, we can multiply it by `contract_value` to get the total traded coins/USDs. 17 | -------------------------------------------------------------------------------- /crypto-contract-value/src/exchanges/binance.rs: -------------------------------------------------------------------------------- 1 | use crypto_market_type::MarketType; 2 | 3 | pub(crate) fn get_contract_value(market_type: MarketType, pair: &str) -> Option { 4 | match market_type { 5 | MarketType::InverseSwap | MarketType::InverseFuture => { 6 | Some(if pair.starts_with("BTC") { 100.0 } else { 10.0 }) 7 | } 8 | MarketType::LinearSwap | MarketType::LinearFuture => Some(1.0), 9 | MarketType::EuropeanOption => Some(1.0), 10 | _ => None, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /crypto-contract-value/src/exchanges/bitfinex.rs: -------------------------------------------------------------------------------- 1 | use crypto_market_type::MarketType; 2 | 3 | pub(crate) fn get_contract_value(market_type: MarketType, _pair: &str) -> Option { 4 | match market_type { 5 | MarketType::Spot | MarketType::LinearSwap => Some(1.0), 6 | _ => None, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /crypto-contract-value/src/exchanges/bitget.rs: -------------------------------------------------------------------------------- 1 | use crypto_market_type::MarketType; 2 | use once_cell::sync::Lazy; 3 | use serde::{Deserialize, Serialize}; 4 | use std::collections::{BTreeMap, HashMap}; 5 | 6 | use super::utils::http_get; 7 | 8 | static LINEAR_SWAP_CONTRACT_VALUES: Lazy> = Lazy::new(|| { 9 | // offline data, in case the network is down 10 | let mut m: HashMap = vec![ 11 | ("AAVE/USDT", 0.1_f64), 12 | ("ADA/USDT", 100_f64), 13 | ("ALGO/USDT", 10_f64), 14 | ("ATOM/USDT", 1_f64), 15 | ("BCH/USDT", 0.01_f64), 16 | ("BTC/USDT", 0.001_f64), 17 | ("COMP/USDT", 0.01_f64), 18 | ("DOGE/USDT", 10_f64), 19 | ("DOT/USDT", 1_f64), 20 | ("EOS/USDT", 1_f64), 21 | ("ETC/USDT", 1_f64), 22 | ("ETH/USDT", 0.1_f64), 23 | ("FIL/USDT", 0.1_f64), 24 | ("ICP/USDT", 0.1_f64), 25 | ("LINK/USDT", 1_f64), 26 | ("LTC/USDT", 0.1_f64), 27 | ("SUSHI/USDT", 1_f64), 28 | ("TRX/USDT", 100_f64), 29 | ("UNI/USDT", 1_f64), 30 | ("XLM/USDT", 10_f64), 31 | ("XRP/USDT", 10_f64), 32 | ("XTZ/USDT", 1_f64), 33 | ("YFI/USDT", 0.0001_f64), 34 | ("ZEC/USDT", 0.1_f64), 35 | ] 36 | .into_iter() 37 | .map(|x| (x.0.to_string(), x.1)) 38 | .collect(); 39 | 40 | let from_online = fetch_contract_val(); 41 | for (pair, contract_value) in from_online { 42 | m.insert(pair, contract_value); 43 | } 44 | 45 | m 46 | }); 47 | 48 | fn fetch_contract_val() -> BTreeMap { 49 | // See https://bitgetlimited.github.io/apidoc/en/swap/#contract-information 50 | #[derive(Serialize, Deserialize)] 51 | #[allow(non_snake_case)] 52 | struct SwapMarket { 53 | symbol: String, 54 | contract_val: String, 55 | forwardContractFlag: bool, 56 | } 57 | 58 | let mut mapping: BTreeMap = BTreeMap::new(); 59 | 60 | if let Ok(txt) = http_get("https://capi.bitget.com/api/swap/v3/market/contracts") { 61 | if let Ok(swap_markets) = serde_json::from_str::>(&txt) { 62 | for swap_market in swap_markets.iter().filter(|x| x.forwardContractFlag) { 63 | mapping.insert( 64 | crypto_pair::normalize_pair(&swap_market.symbol, "bitget").unwrap(), 65 | swap_market.contract_val.parse::().unwrap(), 66 | ); 67 | } 68 | } 69 | } 70 | 71 | mapping 72 | } 73 | 74 | pub(crate) fn get_contract_value(market_type: MarketType, pair: &str) -> Option { 75 | match market_type { 76 | MarketType::InverseSwap => Some(1.0), 77 | MarketType::LinearSwap => Some(LINEAR_SWAP_CONTRACT_VALUES[pair]), 78 | _ => None, 79 | } 80 | } 81 | 82 | #[cfg(test)] 83 | mod tests { 84 | use super::fetch_contract_val; 85 | 86 | #[test] 87 | fn linear_swap() { 88 | let mapping = fetch_contract_val(); 89 | for (pair, contract_value) in &mapping { 90 | println!("(\"{pair}\", {contract_value}_f64),"); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /crypto-contract-value/src/exchanges/bitmex.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{BTreeMap, HashMap}; 2 | 3 | use super::utils::http_get; 4 | use crypto_market_type::MarketType; 5 | use once_cell::sync::Lazy; 6 | use serde::{Deserialize, Serialize}; 7 | use serde_json::Value; 8 | 9 | // key = market_type + pair 10 | static CONTRACT_VALUES: Lazy> = Lazy::new(|| { 11 | // offline data, in case the network is down 12 | let mut m: HashMap = vec![ 13 | ("inverse_future.BTC/USD", 1.0), 14 | ("inverse_future.ETH/USD", 1.0), 15 | ("inverse_swap.BTC/EUR", 1.0), 16 | ("inverse_swap.BTC/USD", 1.0), 17 | ("inverse_swap.ETH/USD", 1.0), 18 | ("inverse_swap.XBTETH/BTC", 0.01), 19 | ("linear_future.ADA/BTC", 0.01), 20 | ("linear_future.BTC/USDT", 0.000001), 21 | ("linear_future.ETH/BTC", 0.00001), 22 | ("linear_future.ETH/USDT", 0.00001), 23 | ("linear_future.ETHPOW/USDT", 0.00001), 24 | ("linear_future.XRP/BTC", 0.01), 25 | ("linear_swap.ADA/USDT", 0.01), 26 | ("linear_swap.APE/USDT", 0.001), 27 | ("linear_swap.APT/USDT", 0.001), 28 | ("linear_swap.ARB/USDT", 0.001), 29 | ("linear_swap.AVAX/USDT", 0.0001), 30 | ("linear_swap.BCH/USDT", 0.00001), 31 | ("linear_swap.BIGTIME/USDT", 1.0), 32 | ("linear_swap.BLUR/USDT", 0.001), 33 | ("linear_swap.BMEX/USDT", 0.001), 34 | ("linear_swap.BNB/USDT", 0.0001), 35 | ("linear_swap.BTC/USDT", 0.000001), 36 | ("linear_swap.CRO/USDT", 0.01), 37 | ("linear_swap.CYBER/USDT", 1.0), 38 | ("linear_swap.DEFIMEXT/USDT", 0.0001), 39 | ("linear_swap.DOGE/USDT", 0.01), 40 | ("linear_swap.DOT/USDT", 0.001), 41 | ("linear_swap.EOS/USDT", 0.001), 42 | ("linear_swap.ETH/USDT", 0.00001), 43 | ("linear_swap.FIL/USDT", 1.0), 44 | ("linear_swap.FTM/USDT", 0.01), 45 | ("linear_swap.FTT/USDT", 0.0001), 46 | ("linear_swap.GAL/USDT", 0.001), 47 | ("linear_swap.GMT/USDT", 0.001), 48 | ("linear_swap.KLAY/USDT", 0.01), 49 | ("linear_swap.LINK/USDT", 0.001), 50 | ("linear_swap.LTC/USDT", 0.0001), 51 | ("linear_swap.LUNA/USDT", 0.001), 52 | ("linear_swap.MANA/USDT", 0.001), 53 | ("linear_swap.MATIC/USDT", 0.01), 54 | ("linear_swap.MEME/USDT", 1.0), 55 | ("linear_swap.NEAR/USDT", 0.001), 56 | ("linear_swap.OP/USDT", 0.001), 57 | ("linear_swap.ORBS/USDT", 1.0), 58 | ("linear_swap.PEPE/USDT", 1.0), 59 | ("linear_swap.PYTH/USDT", 1.0), 60 | ("linear_swap.SAND/USDT", 0.001), 61 | ("linear_swap.SEI/USDT", 1.0), 62 | ("linear_swap.SHIB/USDT", 1.0), 63 | ("linear_swap.SOL/USDT", 0.0001), 64 | ("linear_swap.SUI/USDT", 0.001), 65 | ("linear_swap.TIA/USDT", 1.0), 66 | ("linear_swap.TRX/USDT", 0.1), 67 | ("linear_swap.XRP/USDT", 0.01), 68 | ("quanto_swap.EUR/USDT", 1.0), 69 | ("quanto_swap.NZD/USDT", 1.0), 70 | ] 71 | .into_iter() 72 | .map(|x| (x.0.to_string(), x.1)) 73 | .collect(); 74 | 75 | let from_online = fetch_contract_values(); 76 | for (pair, contract_value) in from_online { 77 | m.insert(pair, contract_value); 78 | } 79 | 80 | m 81 | }); 82 | 83 | #[derive(Clone, Serialize, Deserialize)] 84 | #[allow(non_snake_case)] 85 | struct Instrument { 86 | symbol: String, 87 | state: String, 88 | typ: String, 89 | quoteCurrency: String, 90 | multiplier: f64, 91 | isQuanto: bool, 92 | isInverse: bool, 93 | hasLiquidity: bool, 94 | openInterest: i64, 95 | volume: i64, 96 | volume24h: i64, 97 | turnover: i64, 98 | turnover24h: i64, 99 | underlyingToSettleMultiplier: Option, 100 | underlyingToPositionMultiplier: Option, 101 | quoteToSettleMultiplier: Option, 102 | #[serde(flatten)] 103 | extra: HashMap, 104 | } 105 | 106 | fn fetch_contract_values() -> BTreeMap { 107 | let mut mapping: BTreeMap = BTreeMap::new(); 108 | 109 | if let Ok(text) = http_get("https://www.bitmex.com/api/v1/instrument/active") { 110 | let instruments: Vec = serde_json::from_str::>(&text) 111 | .unwrap() 112 | .into_iter() 113 | .filter(|x| x.state == "Open" && x.hasLiquidity && x.volume24h > 0 && x.turnover24h > 0) 114 | .collect(); 115 | 116 | for instrument in instruments 117 | .iter() 118 | .filter(|instrument| !instrument.isQuanto && instrument.typ != "IFXXXP") 119 | { 120 | let market_type = crypto_pair::get_market_type(&instrument.symbol, "bitmex", None); 121 | let pair = crypto_pair::normalize_pair(&instrument.symbol, "bitmex").unwrap(); 122 | mapping.insert( 123 | market_type.to_string() + "." + pair.as_str(), 124 | if let Some(x) = instrument.underlyingToSettleMultiplier { 125 | instrument.multiplier / x 126 | } else { 127 | instrument.multiplier / instrument.quoteToSettleMultiplier.unwrap() 128 | }, 129 | ); 130 | } 131 | } 132 | 133 | mapping 134 | } 135 | 136 | pub(crate) fn get_contract_value(market_type: MarketType, pair: &str) -> Option { 137 | if market_type == MarketType::Unknown { 138 | panic!("Must be a specific market type"); 139 | } 140 | let key = market_type.to_string() + "." + pair; 141 | if CONTRACT_VALUES.contains_key(key.as_str()) { Some(CONTRACT_VALUES[&key]) } else { Some(1.0) } 142 | } 143 | 144 | #[cfg(test)] 145 | mod tests { 146 | use super::fetch_contract_values; 147 | 148 | #[ignore] 149 | #[test] 150 | fn test_fetch_contract_values() { 151 | let mut mapping = fetch_contract_values(); 152 | for (key, value) in super::CONTRACT_VALUES.iter() { 153 | if !mapping.contains_key(key) { 154 | mapping.insert(key.to_string(), *value); 155 | } 156 | } 157 | for (pair, contract_value) in &mapping { 158 | println!("(\"{pair}\", {contract_value}),"); 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /crypto-contract-value/src/exchanges/bybit.rs: -------------------------------------------------------------------------------- 1 | use crypto_market_type::MarketType; 2 | 3 | pub(crate) fn get_contract_value(market_type: MarketType, _pair: &str) -> Option { 4 | match market_type { 5 | // Each inverse contract value is 1 USD, see: 6 | // https://www.bybit.com/data/basic/inverse/contract-detail?symbol=BTCUSD 7 | // https://www.bybit.com/data/basic/future-inverse/contract-detail?symbol=BTCUSD0625 8 | MarketType::InverseSwap | MarketType::InverseFuture => Some(1.0), 9 | // Each linear contract value is 1 coin, see: 10 | // https://www.bybit.com/data/basic/linear/contract-detail?symbol=BTCUSDT 11 | MarketType::LinearSwap => Some(1.0), 12 | _ => None, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /crypto-contract-value/src/exchanges/deribit.rs: -------------------------------------------------------------------------------- 1 | pub use crypto_market_type::MarketType; 2 | 3 | pub(crate) fn get_contract_value(market_type: MarketType, pair: &str) -> Option { 4 | match market_type { 5 | MarketType::InverseSwap | MarketType::InverseFuture => { 6 | if pair.starts_with("BTC") { 7 | Some(10.0) 8 | } else { 9 | Some(1.0) 10 | } 11 | } 12 | // Each option contract value is 1 coin 13 | MarketType::EuropeanOption => Some(1.0), 14 | _ => None, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /crypto-contract-value/src/exchanges/kraken.rs: -------------------------------------------------------------------------------- 1 | use crypto_market_type::MarketType; 2 | 3 | pub(crate) fn get_contract_value(market_type: MarketType, _pair: &str) -> Option { 4 | match market_type { 5 | MarketType::InverseSwap | MarketType::InverseFuture => Some(1.0), 6 | _ => None, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /crypto-contract-value/src/exchanges/mod.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | pub(super) mod binance; 4 | pub(super) mod bitfinex; 5 | pub(super) mod bitget; 6 | pub(super) mod bitmex; 7 | pub(super) mod bybit; 8 | pub(super) mod deribit; 9 | pub(super) mod gate; 10 | pub(super) mod huobi; 11 | pub(super) mod kraken; 12 | pub(super) mod kucoin; 13 | pub(super) mod mexc; 14 | pub(super) mod okx; 15 | pub(super) mod zbg; 16 | -------------------------------------------------------------------------------- /crypto-contract-value/src/exchanges/utils.rs: -------------------------------------------------------------------------------- 1 | use reqwest::{header, Result}; 2 | 3 | pub(super) fn http_get(url: &str) -> Result { 4 | let mut headers = header::HeaderMap::new(); 5 | headers.insert(header::CONTENT_TYPE, header::HeaderValue::from_static("application/json")); 6 | 7 | let client = reqwest::blocking::Client::builder() 8 | .default_headers(headers) 9 | .user_agent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36") 10 | .gzip(true) 11 | .build()?; 12 | let response = client.get(url).send()?; 13 | 14 | match response.error_for_status() { 15 | Ok(resp) => Ok(resp.text()?), 16 | Err(error) => Err(error), 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /crypto-contract-value/src/exchanges/zbg.rs: -------------------------------------------------------------------------------- 1 | use crypto_market_type::MarketType; 2 | use once_cell::sync::Lazy; 3 | use serde::{Deserialize, Serialize}; 4 | use std::collections::{BTreeMap, HashMap}; 5 | 6 | use super::utils::http_get; 7 | 8 | static SWAP_CONTRACT_VALUES: Lazy> = Lazy::new(|| { 9 | // offline data, in case the network is down 10 | let mut m: HashMap = vec![ 11 | ("AXS/USDT", 0.1_f64), 12 | ("BCH/USDT", 0.1_f64), 13 | ("BSV/USDT", 0.1_f64), 14 | ("BTC/USD", 1_f64), 15 | ("BTC/USDT", 0.01_f64), 16 | ("BTC/ZUSD", 0.01_f64), 17 | ("DOGE/USDT", 100_f64), 18 | ("DOT/USDT", 1_f64), 19 | ("EOS/USDT", 1_f64), 20 | ("ETC/USDT", 1_f64), 21 | ("ETH/USD", 1_f64), 22 | ("ETH/USDT", 0.1_f64), 23 | ("FIL/USDT", 0.1_f64), 24 | ("ICP/USDT", 0.1_f64), 25 | ("LINK/USDT", 1_f64), 26 | ("LTC/USDT", 0.1_f64), 27 | ("RHI/ZUSD", 0.01_f64), 28 | ("SUSHI/USDT", 1_f64), 29 | ("UNI/USDT", 0.1_f64), 30 | ("XRP/USDT", 10_f64), 31 | ] 32 | .into_iter() 33 | .map(|x| (x.0.to_string(), x.1)) 34 | .collect(); 35 | 36 | let from_online = fetch_contract_val(); 37 | for (pair, contract_value) in from_online { 38 | m.insert(pair, contract_value); 39 | } 40 | 41 | m 42 | }); 43 | 44 | #[derive(Serialize, Deserialize)] 45 | #[allow(non_snake_case)] 46 | struct SwapMarket { 47 | symbol: String, 48 | contractUnit: String, 49 | } 50 | 51 | // See https://zbgapi.github.io/docs/future/v1/en/#public-get-contracts 52 | fn fetch_swap_markets_raw() -> Vec { 53 | #[derive(Serialize, Deserialize)] 54 | struct ResMsg { 55 | message: String, 56 | method: Option, 57 | code: String, 58 | } 59 | #[derive(Serialize, Deserialize)] 60 | #[allow(non_snake_case)] 61 | struct Response { 62 | datas: Vec, 63 | resMsg: ResMsg, 64 | } 65 | if let Ok(txt) = http_get("https://www.zbg.com/exchange/api/v1/future/common/contracts") { 66 | if let Ok(resp) = serde_json::from_str::(&txt) { 67 | if resp.resMsg.code != "1" { Vec::new() } else { resp.datas } 68 | } else { 69 | Vec::new() 70 | } 71 | } else { 72 | Vec::new() 73 | } 74 | } 75 | 76 | fn fetch_contract_val() -> BTreeMap { 77 | let mut mapping: BTreeMap = BTreeMap::new(); 78 | let markets = fetch_swap_markets_raw(); 79 | for market in markets { 80 | let contract_value = market.contractUnit.parse::().unwrap(); 81 | assert!(contract_value > 0.0); 82 | mapping.insert(crypto_pair::normalize_pair(&market.symbol, "zbg").unwrap(), contract_value); 83 | } 84 | mapping 85 | } 86 | 87 | pub(crate) fn get_contract_value(market_type: MarketType, pair: &str) -> Option { 88 | match market_type { 89 | MarketType::InverseSwap | MarketType::LinearSwap => Some(SWAP_CONTRACT_VALUES[pair]), 90 | _ => None, 91 | } 92 | } 93 | 94 | #[cfg(test)] 95 | mod tests { 96 | use super::fetch_contract_val; 97 | 98 | #[ignore] 99 | #[test] 100 | fn print_contract_values() { 101 | let mut mapping = fetch_contract_val(); 102 | for (key, value) in super::SWAP_CONTRACT_VALUES.iter() { 103 | if !mapping.contains_key(key) { 104 | mapping.insert(key.to_string(), *value); 105 | } 106 | } 107 | for (pair, contract_value) in &mapping { 108 | println!("(\"{pair}\", {contract_value}_f64),"); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /crypto-contract-value/src/lib.rs: -------------------------------------------------------------------------------- 1 | use crypto_market_type::MarketType; 2 | 3 | mod exchanges; 4 | 5 | pub fn get_contract_value(exchange: &str, market_type: MarketType, pair: &str) -> Option { 6 | if market_type == MarketType::Spot { 7 | return Some(1.0); 8 | } 9 | 10 | match exchange { 11 | "binance" => exchanges::binance::get_contract_value(market_type, pair), 12 | "bitfinex" => exchanges::bitfinex::get_contract_value(market_type, pair), 13 | "bitget" => exchanges::bitget::get_contract_value(market_type, pair), 14 | "bitmex" => exchanges::bitmex::get_contract_value(market_type, pair), 15 | "bybit" => exchanges::bybit::get_contract_value(market_type, pair), 16 | "deribit" => exchanges::deribit::get_contract_value(market_type, pair), 17 | "dydx" => Some(1.0), 18 | "ftx" => Some(1.0), 19 | "gate" => exchanges::gate::get_contract_value(market_type, pair), 20 | "huobi" => exchanges::huobi::get_contract_value(market_type, pair), 21 | "kraken" => exchanges::kraken::get_contract_value(market_type, pair), 22 | "kucoin" => exchanges::kucoin::get_contract_value(market_type, pair), 23 | "mxc" | "mexc" => exchanges::mexc::get_contract_value(market_type, pair), 24 | "okex" | "okx" => exchanges::okx::get_contract_value(market_type, pair), 25 | "zb" => Some(1.0), 26 | "zbg" => exchanges::zbg::get_contract_value(market_type, pair), 27 | _ => panic!("Unknown exchange {exchange}"), 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /crypto-message/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crypto-message" 3 | version = "1.1.21" 4 | edition = "2021" 5 | description = "Unified data structures for all cryptocurrency exchanges." 6 | license = "Apache-2.0" 7 | repository = "https://github.com/crypto-crawler/crypto-msg-parser/tree/main/crypto-message" 8 | keywords = ["cryptocurrency", "blockchain", "trading"] 9 | 10 | 11 | [dependencies] 12 | ahash = "0.8.11" 13 | crypto-market-type = "1.1.6" 14 | crypto-msg-type = "1.0.12" 15 | protobuf = "3.5.0" 16 | serde = { version = "1.0.203", features = ["derive"] } 17 | serde_json = "1.0.120" 18 | strum = "0.26.3" 19 | strum_macros = "0.26.4" 20 | 21 | [features] 22 | f32 = [] 23 | -------------------------------------------------------------------------------- /crypto-message/README.md: -------------------------------------------------------------------------------- 1 | # crypto-message 2 | 3 | [![](https://img.shields.io/crates/v/crypto-message.svg)](https://crates.io/crates/crypto-message) 4 | [![](https://docs.rs/crypto-message/badge.svg)](https://docs.rs/crypto-message) 5 | ========== 6 | 7 | Unified data structures for all cryptocurrency exchanges. 8 | 9 | This library contains all output data types of [`crypto-msg-parser`](https://crates.io/crates/crypto-msg-parser). 10 | 11 | The `crypto_message::proto` module contains protobuf messages corresponding to message types in `lib.rs`. 12 | 13 | The `crypto_message::compact` module contains compact messages corresponding to message types in `lib.rs`. 14 | 15 | **Differences**: 16 | 17 | * Message types in `lib.rs` are output data types of `crypto-msg-parser`, and they are designed for JSON and CSV. 18 | * Message types in `crypto_message::proto` are protobuf messages, which are designed for disk storage. 19 | * message types in `crypto_message::compact` are suitable for RPC. 20 | 21 | Messages types in `lib.rs` has string fields such as `exchange`, `symbol`, which causes a lot of memory allocation and copying, so these types are NOT suitable for high-performance processing. 22 | 23 | Message types in `crypto_message::proto` are compact, (1) metadata fields such as `exchange`, `symbol` and `pair` are removed to save disk space, because these fields exist in filenames already, and (2) all float numbers are 32-bit to save more disk space. 24 | 25 | Message types in `crypto_message::compact` are equivalent to message types in `lib.rs`, with `exchange` changed to `enum`, `symbol` and `pair` changed to `u64` hash values. 26 | -------------------------------------------------------------------------------- /crypto-message/src/compact/mod.rs: -------------------------------------------------------------------------------- 1 | mod message; 2 | mod order; 3 | 4 | pub use message::{calculate_hash, BboMsg, Exchange, Message, OrderBookMsg, TickerMsg, TradeMsg}; 5 | pub use order::{Float, Order, QuantityChoice}; 6 | -------------------------------------------------------------------------------- /crypto-message/src/compact/order.rs: -------------------------------------------------------------------------------- 1 | use serde::{ 2 | de::{Deserializer, SeqAccess, Visitor}, 3 | ser::{SerializeSeq, Serializer}, 4 | Deserialize, Serialize, 5 | }; 6 | use strum_macros::{Display, EnumString}; 7 | 8 | /// Choose which field to use as the quantity. 9 | #[derive(Copy, Clone, Debug, Display, EnumString)] 10 | #[strum(serialize_all = "snake_case")] 11 | pub enum QuantityChoice { 12 | /// quantity_base 13 | Base, 14 | /// quantity_quote 15 | Quote, 16 | /// quantity_contract 17 | Contract, 18 | } 19 | 20 | #[cfg(feature = "f32")] 21 | pub type Float = f32; 22 | #[cfg(not(feature = "f32"))] 23 | pub type Float = f64; 24 | 25 | /// An order in the orderbook asks or bids array. 26 | #[derive(Copy, Clone, Debug)] 27 | pub struct Order { 28 | /// price 29 | pub price: Float, 30 | // quantity, comes from one of quantity_base, quantity_quote and quantity_contract. 31 | pub quantity: Float, 32 | } 33 | 34 | impl PartialEq for Order { 35 | fn eq(&self, other: &Self) -> bool { 36 | self.price == other.price && self.quantity == other.quantity 37 | } 38 | } 39 | 40 | impl Eq for Order {} 41 | 42 | impl Serialize for Order { 43 | fn serialize(&self, serializer: S) -> Result 44 | where 45 | S: Serializer, 46 | { 47 | let len: usize = 2; 48 | let mut seq = serializer.serialize_seq(Some(len))?; 49 | seq.serialize_element(&self.price)?; 50 | // limit the number of decimals to 9 51 | let quantity = format!("{:.9}", self.quantity).as_str().parse::().unwrap(); 52 | seq.serialize_element(&quantity)?; 53 | 54 | seq.end() 55 | } 56 | } 57 | 58 | struct OrderVisitor; 59 | 60 | impl<'de> Visitor<'de> for OrderVisitor { 61 | type Value = Order; 62 | 63 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 64 | formatter.write_str("a nonempty sequence of numbers") 65 | } 66 | 67 | fn visit_seq(self, mut visitor: V) -> Result 68 | where 69 | V: SeqAccess<'de>, 70 | { 71 | let mut vec = Vec::::new(); 72 | 73 | while let Some(elem) = visitor.next_element()? { 74 | vec.push(elem); 75 | } 76 | 77 | let order = Order { price: vec[0] as Float, quantity: vec[1] as Float }; 78 | 79 | Ok(order) 80 | } 81 | } 82 | 83 | impl<'de> Deserialize<'de> for Order { 84 | fn deserialize(deserializer: D) -> Result 85 | where 86 | D: Deserializer<'de>, 87 | { 88 | deserializer.deserialize_seq(OrderVisitor) 89 | } 90 | } 91 | 92 | #[cfg(test)] 93 | mod tests { 94 | use crate::compact::Order; 95 | 96 | #[test] 97 | fn order_serialize() { 98 | let order = Order { price: 59999.8, quantity: 1.7000000001 }; 99 | let text = serde_json::to_string(&order).unwrap(); 100 | assert_eq!(text.as_str(), "[59999.8,1.7]"); 101 | 102 | let order = Order { price: 59999.8, quantity: 1.7000000006 }; 103 | let text = serde_json::to_string(&order).unwrap(); 104 | assert_eq!(text.as_str(), "[59999.8,1.700000001]"); 105 | } 106 | 107 | #[test] 108 | fn order_deserialize() { 109 | let expected = Order { price: 59999.8, quantity: 1.7 }; 110 | let actual = serde_json::from_str::("[59999.8,1.7,101999.66,1.7]").unwrap(); 111 | assert_eq!(expected.price, actual.price); 112 | assert_eq!(expected.quantity, actual.quantity); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /crypto-message/src/order.rs: -------------------------------------------------------------------------------- 1 | use serde::{ 2 | de::{Deserializer, SeqAccess, Visitor}, 3 | ser::{SerializeSeq, Serializer}, 4 | Deserialize, Serialize, 5 | }; 6 | 7 | /// An order in the orderbook asks or bids array. 8 | #[derive(Copy, Clone, Debug)] 9 | pub struct Order { 10 | /// price 11 | pub price: f64, 12 | // Number of base coins, 0 means the price level can be removed. 13 | pub quantity_base: f64, 14 | // Number of quote coins(mostly USDT) 15 | pub quantity_quote: f64, 16 | /// Number of contracts, always None for Spot 17 | pub quantity_contract: Option, 18 | } 19 | 20 | impl PartialEq for Order { 21 | fn eq(&self, other: &Self) -> bool { 22 | self.price == other.price 23 | && self.quantity_base == other.quantity_base 24 | && self.quantity_quote == other.quantity_quote 25 | && self.quantity_contract == other.quantity_contract 26 | } 27 | } 28 | 29 | impl Eq for Order {} 30 | 31 | impl Serialize for Order { 32 | fn serialize(&self, serializer: S) -> Result 33 | where 34 | S: Serializer, 35 | { 36 | let len: usize = if self.quantity_contract.is_some() { 4 } else { 3 }; 37 | let mut seq = serializer.serialize_seq(Some(len))?; 38 | seq.serialize_element(&self.price)?; 39 | // limit the number of decimals to 9 40 | let quantity_base = format!("{:.9}", self.quantity_base).as_str().parse::().unwrap(); 41 | let quantity_quote = format!("{:.9}", self.quantity_quote).as_str().parse::().unwrap(); 42 | seq.serialize_element(&quantity_base)?; 43 | seq.serialize_element(&quantity_quote)?; 44 | if let Some(qc) = self.quantity_contract { 45 | seq.serialize_element(&qc)?; 46 | } 47 | 48 | seq.end() 49 | } 50 | } 51 | 52 | struct OrderVisitor; 53 | 54 | impl<'de> Visitor<'de> for OrderVisitor { 55 | type Value = Order; 56 | 57 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 58 | formatter.write_str("a nonempty sequence of numbers") 59 | } 60 | 61 | fn visit_seq(self, mut visitor: V) -> Result 62 | where 63 | V: SeqAccess<'de>, 64 | { 65 | let mut vec = Vec::::new(); 66 | 67 | while let Some(elem) = visitor.next_element()? { 68 | vec.push(elem); 69 | } 70 | 71 | let order = Order { 72 | price: vec[0], 73 | quantity_base: vec[1], 74 | quantity_quote: vec[2], 75 | quantity_contract: if vec.len() == 4 { Some(vec[3]) } else { None }, 76 | }; 77 | 78 | Ok(order) 79 | } 80 | } 81 | 82 | impl<'de> Deserialize<'de> for Order { 83 | fn deserialize(deserializer: D) -> Result 84 | where 85 | D: Deserializer<'de>, 86 | { 87 | deserializer.deserialize_seq(OrderVisitor) 88 | } 89 | } 90 | 91 | #[cfg(test)] 92 | mod tests { 93 | use crate::order::Order; 94 | 95 | #[test] 96 | fn order_serialize() { 97 | let order = Order { 98 | price: 59999.8, 99 | quantity_base: 1.7000000001, 100 | quantity_quote: 59999.8 * 1.7, 101 | quantity_contract: Some(1.7), 102 | }; 103 | let text = serde_json::to_string(&order).unwrap(); 104 | assert_eq!(text.as_str(), "[59999.8,1.7,101999.66,1.7]"); 105 | 106 | let order = Order { 107 | price: 59999.8, 108 | quantity_base: 1.7000000006, 109 | quantity_quote: 59999.8 * 1.7, 110 | quantity_contract: Some(1.7), 111 | }; 112 | let text = serde_json::to_string(&order).unwrap(); 113 | assert_eq!(text.as_str(), "[59999.8,1.700000001,101999.66,1.7]"); 114 | } 115 | 116 | #[test] 117 | fn order_deserialize() { 118 | let expected = Order { 119 | price: 59999.8, 120 | quantity_base: 1.7, 121 | quantity_quote: 59999.8 * 1.7, 122 | quantity_contract: Some(1.7), 123 | }; 124 | let actual = serde_json::from_str::("[59999.8,1.7,101999.66,1.7]").unwrap(); 125 | assert_eq!(expected.price, actual.price); 126 | assert_eq!(expected.quantity_base, actual.quantity_base); 127 | assert_eq!(expected.quantity_quote, actual.quantity_quote); 128 | assert_eq!(expected.quantity_contract, actual.quantity_contract); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /crypto-message/src/proto/README.md: -------------------------------------------------------------------------------- 1 | # protobuf schema 2 | 3 | The schema file is `message.proto`. 4 | 5 | ## Build 6 | 7 | First, install the `protoc`, i.e., the protobuf compiler: 8 | 9 | ```bash 10 | # Install protoc on macOS 11 | xcode-select --install && brew install protobuf gcc 12 | # Install protc on Ubuntu 13 | sudo apt install protobuf-compiler 14 | ``` 15 | 16 | Second, compile `message.proto` to language specific files: 17 | 18 | ```bash 19 | protoc -I=. message.proto --cpp_out=./cpp 20 | protoc -I=. message.proto --python_out=./python 21 | protoc -I=. message.proto --rust_out=./rust # Need to cargo install protobuf-codegen 22 | ``` 23 | 24 | ## Libraries 25 | 26 | - [Python delimited-protobuf](https://pypi.org/project/delimited-protobuf/) 27 | - [Rust delimited-protobuf](https://crates.io/crates/delimited-protobuf) 28 | -------------------------------------------------------------------------------- /crypto-message/src/proto/message.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package crypto_crawler; 3 | 4 | import "google/protobuf/timestamp.proto"; 5 | 6 | 7 | // Tick-by-tick trade message. 8 | message Trade { 9 | google.protobuf.Timestamp timestamp = 1; 10 | // Which side is taker? True, seller is taker; False, buyer is taker 11 | bool side = 2; 12 | float price = 3; 13 | // Number of base coins, 0 means delete 14 | float quantity_base = 4; 15 | // Number of quote coins, 0 means delete 16 | float quantity_quote = 5; 17 | // Number of contracts, empty for spot markets 18 | optional float quantity_contract = 6; 19 | } 20 | 21 | // Level2 orderbook. 22 | message Orderbook { 23 | message Order { 24 | float price = 1; 25 | // Number of base coins, 0 means delete 26 | float quantity_base = 2; 27 | // Number of quote coins, 0 means delete 28 | float quantity_quote = 3; 29 | // Number of contracts, empty for spot markets 30 | optional float quantity_contract = 4; 31 | } 32 | google.protobuf.Timestamp timestamp = 1; 33 | // snapshot or updates 34 | bool snapshot = 2; 35 | // sorted in ascending order by price if snapshot=true, otherwise not sorted 36 | repeated Order asks = 3; 37 | // sorted in descending order by price if snapshot=true, otherwise not sorted 38 | repeated Order bids = 4; 39 | } 40 | 41 | // Best bid and offer. 42 | message Bbo { 43 | google.protobuf.Timestamp timestamp = 1; 44 | float bid_price = 2; 45 | float bid_quantity_base = 3; 46 | float bid_quantity_quote = 4; 47 | optional float bid_quantity_contract = 5; 48 | float ask_price = 6; 49 | float ask_quantity_base = 7; 50 | float ask_quantity_quote = 8; 51 | optional float ask_quantity_contract = 9; 52 | } 53 | 54 | // 24hr rolling window ticker. 55 | message Ticker { 56 | google.protobuf.Timestamp timestamp = 1; 57 | float open = 2; 58 | float high = 3; 59 | float low = 4; 60 | float close = 5; 61 | float volume = 6; 62 | float quote_volume = 7; 63 | optional float last_quantity = 8; 64 | optional float best_bid_price = 9; 65 | optional float best_bid_quantity = 10; 66 | optional float best_ask_price = 11; 67 | optional float best_ask_quantity = 12; 68 | // availale in Futures and Swap markets 69 | optional float open_interest = 13; 70 | // availale in Futures and Swap markets 71 | optional float open_interest_quote = 14; 72 | } 73 | -------------------------------------------------------------------------------- /crypto-message/src/proto/mod.rs: -------------------------------------------------------------------------------- 1 | mod message; 2 | 3 | pub use message::{orderbook::Order, Bbo, Orderbook, Ticker, Trade}; 4 | -------------------------------------------------------------------------------- /crypto-msg-parser/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crypto-msg-parser" 3 | version = "2.9.2" 4 | authors = ["soulmachine "] 5 | edition = "2021" 6 | description = "Parse websocket messages from cryptocurreny exchanges" 7 | license = "Apache-2.0" 8 | repository = "https://github.com/crypto-crawler/crypto-msg-parser/tree/main/crypto-msg-parser" 9 | keywords = ["cryptocurrency", "blockchain", "trading"] 10 | 11 | [dependencies] 12 | chrono = "0.4.38" 13 | crypto-contract-value = "1.7.25" 14 | crypto-market-type = "1.1.6" 15 | crypto-msg-type = "1.0.12" 16 | crypto-message = "1.1.21" 17 | crypto-pair = "2.3.20" 18 | if_chain = "1.0.2" 19 | once_cell = "1.19.0" 20 | reqwest = { version = "0.12.5", features = ["blocking", "gzip"] } 21 | simple-error = "0.3.1" 22 | serde = { version = "1.0.203", features = ["derive"] } 23 | serde_json = "1.0.120" 24 | strum = "0.26.3" 25 | strum_macros = "0.26.4" 26 | -------------------------------------------------------------------------------- /crypto-msg-parser/README.md: -------------------------------------------------------------------------------- 1 | # crypto-msg-parser 2 | 3 | [![](https://img.shields.io/crates/v/crypto-msg-parser.svg)](https://crates.io/crates/crypto-msg-parser) 4 | [![](https://docs.rs/crypto-msg-parser/badge.svg)](https://docs.rs/crypto-msg-parser) 5 | 6 | The parser library to parse messages from [crypto-crawler](https://github.com/crypto-crawler/crypto-crawler-rs/tree/main/crypto-crawler). 7 | -------------------------------------------------------------------------------- /crypto-msg-parser/src/exchanges/binance/binance_option.rs: -------------------------------------------------------------------------------- 1 | use crypto_market_type::MarketType; 2 | use crypto_msg_type::MessageType; 3 | 4 | use crypto_message::{TradeMsg, TradeSide}; 5 | 6 | use serde::{Deserialize, Serialize}; 7 | use serde_json::Value; 8 | use simple_error::SimpleError; 9 | use std::collections::HashMap; 10 | 11 | const EXCHANGE_NAME: &str = "binance"; 12 | 13 | #[derive(Serialize, Deserialize)] 14 | #[allow(non_snake_case)] 15 | struct OptionTradeMsg { 16 | t: String, // Trade ID 17 | p: String, // Price 18 | q: String, // Quantity 19 | b: String, // Bid order ID 20 | a: String, // Ask order ID 21 | T: i64, // Trade time 22 | s: String, // Side 23 | S: String, // Symbol 24 | #[serde(flatten)] 25 | extra: HashMap, 26 | } 27 | 28 | #[derive(Serialize, Deserialize)] 29 | #[allow(non_snake_case)] 30 | struct OptionTradeAllMsg { 31 | e: String, // Event type 32 | E: i64, // Event time 33 | s: String, // Symbol 34 | t: Vec, 35 | } 36 | 37 | #[derive(Serialize, Deserialize)] 38 | struct WebsocketMsg { 39 | stream: String, 40 | data: T, 41 | } 42 | 43 | pub(crate) fn parse_trade(msg: &str) -> Result, SimpleError> { 44 | let obj = serde_json::from_str::>(msg) 45 | .map_err(|_e| SimpleError::new(format!("{msg} is not a JSON object")))?; 46 | let data = obj 47 | .get("data") 48 | .ok_or_else(|| SimpleError::new(format!("There is no data field in {msg}")))?; 49 | let event_type = data["e"].as_str().ok_or_else(|| { 50 | SimpleError::new(format!("There is no e field in the data field of {msg}")) 51 | })?; 52 | 53 | assert_eq!(event_type, "trade_all"); 54 | 55 | let all_trades: OptionTradeAllMsg = serde_json::from_value(data.clone()).map_err(|_e| { 56 | SimpleError::new(format!("Failed to deserialize {data} to OptionTradeAllMsg")) 57 | })?; 58 | let trades: Vec = all_trades 59 | .t 60 | .into_iter() 61 | .map(|trade| { 62 | let pair = crypto_pair::normalize_pair(&trade.S, EXCHANGE_NAME).unwrap(); 63 | let price = trade.p.parse::().unwrap(); 64 | let quantity = trade.q.parse::().unwrap(); 65 | TradeMsg { 66 | exchange: EXCHANGE_NAME.to_string(), 67 | market_type: MarketType::EuropeanOption, 68 | symbol: trade.S.clone(), 69 | pair, 70 | msg_type: MessageType::Trade, 71 | timestamp: trade.T, 72 | price, 73 | quantity_base: quantity, 74 | quantity_quote: price * quantity, 75 | quantity_contract: Some(quantity), 76 | side: if trade.s == "1" { 77 | // TODO: find out the meaning of the field s 78 | TradeSide::Sell 79 | } else { 80 | TradeSide::Buy 81 | }, 82 | trade_id: trade.a.to_string(), 83 | json: serde_json::to_string(&trade).unwrap(), 84 | } 85 | }) 86 | .collect(); 87 | Ok(trades) 88 | } 89 | -------------------------------------------------------------------------------- /crypto-msg-parser/src/exchanges/binance/binance_spot.rs: -------------------------------------------------------------------------------- 1 | use crypto_market_type::MarketType; 2 | use crypto_msg_type::MessageType; 3 | 4 | use crate::{Order, OrderBookMsg}; 5 | 6 | use super::EXCHANGE_NAME; 7 | use serde::{Deserialize, Serialize}; 8 | use serde_json::Value; 9 | use simple_error::SimpleError; 10 | use std::collections::HashMap; 11 | 12 | /// price, quantity 13 | pub type RawOrder = [String; 2]; 14 | 15 | // See https://binance-docs.github.io/apidocs/spot/en/#partial-book-depth-streams 16 | #[derive(Serialize, Deserialize)] 17 | #[allow(non_snake_case)] 18 | struct RawL2TopKMsg { 19 | lastUpdateId: u64, // Last update ID 20 | asks: Vec, 21 | bids: Vec, 22 | #[serde(flatten)] 23 | extra: HashMap, 24 | } 25 | 26 | #[derive(Serialize, Deserialize)] 27 | struct WebsocketMsg { 28 | stream: String, 29 | data: T, 30 | } 31 | 32 | // See https://binance-docs.github.io/apidocs/spot/en/#order-book 33 | #[derive(Serialize, Deserialize)] 34 | #[allow(non_snake_case)] 35 | struct RawL2SnapshotMsg { 36 | lastUpdateId: u64, // Last update ID 37 | bids: Vec, 38 | asks: Vec, 39 | #[serde(flatten)] 40 | extra: HashMap, 41 | } 42 | 43 | // The @depth20 payload of spot has quite different format from contracts. 44 | pub(super) fn parse_l2_topk( 45 | msg: &str, 46 | received_at: Option, 47 | ) -> Result, SimpleError> { 48 | let ws_msg = serde_json::from_str::>(msg).map_err(|_e| { 49 | SimpleError::new(format!("Failed to deserialize {msg} to WebsocketMsg")) 50 | })?; 51 | debug_assert!(!ws_msg.stream.starts_with('!')); 52 | let symbol = ws_msg.stream.as_str().split('@').next().unwrap().to_uppercase(); 53 | let pair = crypto_pair::normalize_pair(&symbol, EXCHANGE_NAME) 54 | .ok_or_else(|| SimpleError::new(format!("Failed to normalize {} from {}", &symbol, msg)))?; 55 | let timestamp = received_at.expect("Binance spot L2TopK doesn't have timestamp"); 56 | 57 | let parse_order = |raw_order: &RawOrder| -> Order { 58 | let price = raw_order[0].parse::().unwrap(); 59 | let quantity_base = raw_order[1].parse::().unwrap(); 60 | Order { 61 | price, 62 | quantity_base, 63 | quantity_quote: price * quantity_base, 64 | quantity_contract: None, 65 | } 66 | }; 67 | 68 | let orderbook = OrderBookMsg { 69 | exchange: EXCHANGE_NAME.to_string(), 70 | market_type: MarketType::Spot, 71 | symbol, 72 | pair, 73 | msg_type: MessageType::L2TopK, 74 | timestamp, 75 | seq_id: Some(ws_msg.data.lastUpdateId), 76 | prev_seq_id: None, 77 | asks: ws_msg.data.asks.iter().map(parse_order).collect::>(), 78 | bids: ws_msg.data.bids.iter().map(parse_order).collect::>(), 79 | snapshot: true, 80 | json: msg.to_string(), 81 | }; 82 | Ok(vec![orderbook]) 83 | } 84 | 85 | // binance l2 snapshot data is quite large 86 | pub(super) fn parse_l2_snapshot( 87 | msg: &str, 88 | symbol: Option<&str>, 89 | received_at: Option, 90 | ) -> Result, SimpleError> { 91 | let ws_msg = serde_json::from_str::(msg).map_err(|_e| { 92 | SimpleError::new(format!("Failed to deserialize {msg} to RawL2SnapshotMsg")) 93 | })?; 94 | 95 | let pair = crypto_pair::normalize_pair(symbol.unwrap(), EXCHANGE_NAME).ok_or_else(|| { 96 | SimpleError::new(format!("Failed to normalize {} from {}", symbol.unwrap(), msg)) 97 | })?; 98 | let timestamp = received_at.expect("Binance spot L2 Snapshot doesn't have timestamp"); 99 | 100 | let parse_order = |raw_order: &RawOrder| -> Order { 101 | let price = raw_order[0].parse::().unwrap(); 102 | let quantity_base = raw_order[1].parse::().unwrap(); 103 | Order { 104 | price, 105 | quantity_base, 106 | quantity_quote: price * quantity_base, 107 | quantity_contract: None, 108 | } 109 | }; 110 | 111 | let orderbook = OrderBookMsg { 112 | exchange: EXCHANGE_NAME.to_string(), 113 | market_type: MarketType::Spot, 114 | symbol: symbol.unwrap().to_string(), 115 | pair, 116 | msg_type: MessageType::L2Snapshot, 117 | timestamp, 118 | seq_id: Some(ws_msg.lastUpdateId), 119 | prev_seq_id: None, 120 | asks: ws_msg.asks.iter().map(parse_order).collect::>(), 121 | bids: ws_msg.bids.iter().map(parse_order).collect::>(), 122 | snapshot: true, 123 | json: msg.to_string(), 124 | }; 125 | Ok(vec![orderbook]) 126 | } 127 | -------------------------------------------------------------------------------- /crypto-msg-parser/src/exchanges/bitget/before20220429/mod.rs: -------------------------------------------------------------------------------- 1 | mod bitget_swap; 2 | 3 | use crypto_market_type::MarketType; 4 | use crypto_msg_type::MessageType; 5 | 6 | use crate::{CandlestickMsg, FundingRateMsg, OrderBookMsg, TradeMsg}; 7 | 8 | use simple_error::SimpleError; 9 | 10 | pub(super) fn extract_symbol(_market_type: MarketType, msg: &str) -> Result { 11 | bitget_swap::extract_symbol(msg) 12 | } 13 | 14 | pub(super) fn extract_timestamp( 15 | _market_type: MarketType, 16 | msg: &str, 17 | ) -> Result, SimpleError> { 18 | bitget_swap::extract_timestamp(msg) 19 | } 20 | 21 | pub(super) fn get_msg_type(msg: &str) -> MessageType { 22 | bitget_swap::get_msg_type(msg) 23 | } 24 | 25 | pub(super) fn parse_trade( 26 | market_type: MarketType, 27 | msg: &str, 28 | ) -> Result, SimpleError> { 29 | if market_type == MarketType::Spot { 30 | Err(SimpleError::new("Not implemented")) 31 | } else { 32 | bitget_swap::parse_trade(market_type, msg) 33 | } 34 | } 35 | 36 | pub(super) fn parse_l2( 37 | market_type: MarketType, 38 | msg: &str, 39 | ) -> Result, SimpleError> { 40 | if market_type == MarketType::Spot { 41 | Err(SimpleError::new("Not implemented")) 42 | } else { 43 | bitget_swap::parse_l2(market_type, msg) 44 | } 45 | } 46 | 47 | pub(super) fn parse_funding_rate( 48 | market_type: MarketType, 49 | msg: &str, 50 | ) -> Result, SimpleError> { 51 | bitget_swap::parse_funding_rate(market_type, msg) 52 | } 53 | 54 | pub(super) fn parse_candlestick( 55 | market_type: MarketType, 56 | msg: &str, 57 | ) -> Result, SimpleError> { 58 | bitget_swap::parse_candlestick(market_type, msg) 59 | } 60 | -------------------------------------------------------------------------------- /crypto-msg-parser/src/exchanges/bitstamp.rs: -------------------------------------------------------------------------------- 1 | use crypto_market_type::MarketType; 2 | use crypto_msg_type::MessageType; 3 | 4 | use crypto_message::{Order, OrderBookMsg, TradeMsg, TradeSide}; 5 | 6 | use serde::{Deserialize, Serialize}; 7 | use serde_json::Value; 8 | use simple_error::SimpleError; 9 | use std::collections::HashMap; 10 | 11 | const EXCHANGE_NAME: &str = "bitstamp"; 12 | 13 | // see "Live ticker" at https://www.bitstamp.net/websocket/v2/ 14 | #[derive(Serialize, Deserialize)] 15 | struct SpotTradeMsg { 16 | microtimestamp: String, // Trade microtimestamp 17 | amount: f64, // Trade amount 18 | buy_order_id: i64, // Trade buy order ID 19 | sell_order_id: i64, // Trade sell order ID. 20 | amount_str: String, // Trade amount represented in string format 21 | price_str: String, // Trade price represented in string format 22 | timestamp: String, // Trade timestamp 23 | price: f64, // Trade price 24 | #[serde(rename = "type")] 25 | type_: i64, // Trade type (0 - buy; 1 - sell) 26 | id: i64, // Trade unique ID 27 | #[serde(flatten)] 28 | extra: HashMap, 29 | } 30 | 31 | // see "Live full order book" at https://www.bitstamp.net/websocket/v2/ 32 | #[derive(Serialize, Deserialize)] 33 | struct SpotOrderbookMsg { 34 | timestamp: String, // Trade timestamp 35 | microtimestamp: String, // Trade microtimestamp 36 | bids: Vec<[String; 2]>, 37 | asks: Vec<[String; 2]>, 38 | #[serde(flatten)] 39 | extra: HashMap, 40 | } 41 | 42 | #[derive(Serialize, Deserialize)] 43 | struct WebsocketMsg { 44 | channel: String, 45 | event: String, 46 | data: T, 47 | } 48 | 49 | pub(crate) fn extract_symbol(_market_type: MarketType, msg: &str) -> Result { 50 | let json_obj = serde_json::from_str::>(msg) 51 | .map_err(|_e| SimpleError::new(format!("Failed to parse the JSON string {msg}")))?; 52 | if let Some(channel) = json_obj.get("channel") { 53 | let symbol = channel.as_str().unwrap().split('_').last().unwrap(); 54 | Ok(symbol.to_string()) 55 | } else if json_obj.contains_key("asks") && json_obj.contains_key("bids") { 56 | // l2_snapshot has no symbol 57 | Ok("NONE".to_string()) 58 | } else { 59 | Err(SimpleError::new(format!("Failed to extract symbol from {msg}"))) 60 | } 61 | } 62 | 63 | pub(crate) fn extract_timestamp( 64 | _market_type: MarketType, 65 | msg: &str, 66 | ) -> Result, SimpleError> { 67 | let json_obj = serde_json::from_str::>(msg) 68 | .map_err(|_e| SimpleError::new(format!("Failed to parse the JSON string {msg}")))?; 69 | if let Some(data) = json_obj.get("data") { 70 | Ok(Some(data["microtimestamp"].as_str().unwrap().parse::().unwrap() / 1000)) 71 | } else if let Some(microtimestamp) = json_obj.get("microtimestamp") { 72 | Ok(Some(microtimestamp.as_str().unwrap().parse::().unwrap() / 1000)) 73 | } else { 74 | Err(SimpleError::new(format!("No microtimestamp field in {msg}"))) 75 | } 76 | } 77 | 78 | pub(crate) fn parse_trade( 79 | market_type: MarketType, 80 | msg: &str, 81 | ) -> Result, SimpleError> { 82 | let ws_msg = serde_json::from_str::>(msg).map_err(|_e| { 83 | SimpleError::new(format!("Failed to deserialize {msg} to WebsocketMsg")) 84 | })?; 85 | let symbol = ws_msg.channel.split('_').last().unwrap(); 86 | let pair = crypto_pair::normalize_pair(symbol, EXCHANGE_NAME) 87 | .ok_or_else(|| SimpleError::new(format!("Failed to normalize {symbol} from {msg}")))?; 88 | let raw_trade = ws_msg.data; 89 | 90 | let trade = TradeMsg { 91 | exchange: EXCHANGE_NAME.to_string(), 92 | market_type, 93 | symbol: symbol.to_string(), 94 | pair, 95 | msg_type: MessageType::Trade, 96 | timestamp: raw_trade.microtimestamp.parse::().unwrap() / 1000, 97 | price: raw_trade.price, 98 | quantity_base: raw_trade.amount, 99 | quantity_quote: raw_trade.price * raw_trade.amount, 100 | quantity_contract: None, 101 | side: if raw_trade.type_ == 1 { TradeSide::Sell } else { TradeSide::Buy }, 102 | trade_id: raw_trade.id.to_string(), 103 | json: msg.to_string(), 104 | }; 105 | 106 | Ok(vec![trade]) 107 | } 108 | 109 | pub(crate) fn parse_l2( 110 | market_type: MarketType, 111 | msg: &str, 112 | ) -> Result, SimpleError> { 113 | let ws_msg = serde_json::from_str::>(msg).map_err(|_e| { 114 | SimpleError::new(format!("Failed to deserialize {msg} to WebsocketMsg")) 115 | })?; 116 | let symbol = ws_msg.channel.split('_').last().unwrap(); 117 | let pair = crypto_pair::normalize_pair(symbol, EXCHANGE_NAME) 118 | .ok_or_else(|| SimpleError::new(format!("Failed to normalize {symbol} from {msg}")))?; 119 | let msg_type = if ws_msg.channel.starts_with("diff_order_book_") { 120 | MessageType::L2Event 121 | } else { 122 | MessageType::L2TopK 123 | }; 124 | let raw_orderbook = ws_msg.data; 125 | 126 | let parse_order = |raw_order: &[String; 2]| -> Order { 127 | let price = raw_order[0].parse::().unwrap(); 128 | let quantity_base = raw_order[1].parse::().unwrap(); 129 | 130 | Order { 131 | price, 132 | quantity_base, 133 | quantity_quote: price * quantity_base, 134 | quantity_contract: None, 135 | } 136 | }; 137 | 138 | let orderbook = OrderBookMsg { 139 | exchange: EXCHANGE_NAME.to_string(), 140 | market_type, 141 | symbol: symbol.to_string(), 142 | pair, 143 | msg_type, 144 | timestamp: raw_orderbook.microtimestamp.parse::().unwrap() / 1000, 145 | seq_id: None, 146 | prev_seq_id: None, 147 | asks: raw_orderbook.asks.iter().map(parse_order).collect(), 148 | bids: raw_orderbook.bids.iter().map(parse_order).collect(), 149 | snapshot: ws_msg.channel.starts_with("order_book_"), 150 | json: msg.to_string(), 151 | }; 152 | 153 | Ok(vec![orderbook]) 154 | } 155 | 156 | pub(crate) fn parse_l2_topk( 157 | market_type: MarketType, 158 | msg: &str, 159 | ) -> Result, SimpleError> { 160 | parse_l2(market_type, msg) 161 | } 162 | -------------------------------------------------------------------------------- /crypto-msg-parser/src/exchanges/bitz.rs: -------------------------------------------------------------------------------- 1 | use crypto_market_type::MarketType; 2 | use crypto_msg_type::MessageType; 3 | 4 | use crypto_message::{Order, OrderBookMsg, TradeMsg, TradeSide}; 5 | 6 | use serde::{Deserialize, Serialize}; 7 | use serde_json::Value; 8 | use simple_error::SimpleError; 9 | use std::collections::HashMap; 10 | 11 | const EXCHANGE_NAME: &str = "bitz"; 12 | 13 | // see https://apidocv2.bitz.plus/#order 14 | #[derive(Serialize, Deserialize)] 15 | #[allow(non_snake_case)] 16 | struct SpotTradeMsg { 17 | id: String, 18 | t: String, 19 | T: i64, 20 | p: String, 21 | n: String, 22 | s: String, // Sell, Buy 23 | #[serde(flatten)] 24 | extra: HashMap, 25 | } 26 | 27 | // see https://apidocv2.bitz.plus/#depth 28 | #[derive(Serialize, Deserialize)] 29 | #[allow(non_snake_case)] 30 | struct SpotOrderbookMsg { 31 | asks: Option>, 32 | bids: Option>, 33 | #[serde(flatten)] 34 | extra: HashMap, 35 | } 36 | 37 | #[derive(Serialize, Deserialize)] 38 | #[allow(non_snake_case)] 39 | struct Params { 40 | symbol: String, 41 | #[serde(flatten)] 42 | extra: HashMap, 43 | } 44 | 45 | #[derive(Serialize, Deserialize)] 46 | struct WebsocketMsg { 47 | params: Params, 48 | action: String, 49 | data: T, 50 | time: i64, 51 | } 52 | 53 | pub(crate) fn extract_symbol(_market_type: MarketType, msg: &str) -> Result { 54 | let ws_msg = serde_json::from_str::>(msg).map_err(|_e| { 55 | SimpleError::new(format!("Failed to deserialize {msg} to WebsocketMsg")) 56 | })?; 57 | let symbol = ws_msg.params.symbol.as_str(); 58 | Ok(symbol.to_string()) 59 | } 60 | 61 | pub(crate) fn extract_timestamp( 62 | _market_type: MarketType, 63 | msg: &str, 64 | ) -> Result, SimpleError> { 65 | let ws_msg = serde_json::from_str::>(msg).map_err(|_e| { 66 | SimpleError::new(format!("Failed to deserialize {msg} to WebsocketMsg")) 67 | })?; 68 | Ok(Some(ws_msg.time)) 69 | } 70 | 71 | pub(crate) fn parse_trade( 72 | market_type: MarketType, 73 | msg: &str, 74 | ) -> Result, SimpleError> { 75 | let ws_msg = serde_json::from_str::>>(msg).map_err(|_e| { 76 | SimpleError::new(format!("Failed to deserialize {msg} to WebsocketMsg")) 77 | })?; 78 | let symbol = ws_msg.params.symbol.as_str(); 79 | let pair = crypto_pair::normalize_pair(symbol, EXCHANGE_NAME) 80 | .ok_or_else(|| SimpleError::new(format!("Failed to normalize {symbol} from {msg}")))?; 81 | 82 | let mut trades: Vec = ws_msg 83 | .data 84 | .into_iter() 85 | .map(|raw_trade| { 86 | let price = raw_trade.p.parse::().unwrap(); 87 | let quantity = raw_trade.n.parse::().unwrap(); 88 | let timestamp = if raw_trade.id.is_empty() { 89 | raw_trade.T * 1000 90 | } else { 91 | raw_trade.id.parse::().unwrap() 92 | }; 93 | TradeMsg { 94 | exchange: EXCHANGE_NAME.to_string(), 95 | market_type, 96 | symbol: symbol.to_string(), 97 | pair: pair.clone(), 98 | msg_type: MessageType::Trade, 99 | timestamp, 100 | price, 101 | quantity_base: quantity, 102 | quantity_quote: price * quantity, 103 | quantity_contract: None, 104 | side: if raw_trade.s == "sell" { TradeSide::Sell } else { TradeSide::Buy }, 105 | trade_id: timestamp.to_string(), 106 | json: serde_json::to_string(&raw_trade).unwrap(), 107 | } 108 | }) 109 | .collect(); 110 | if trades.len() == 1 { 111 | trades[0].json = msg.to_string(); 112 | } 113 | Ok(trades) 114 | } 115 | 116 | pub(crate) fn parse_l2( 117 | market_type: MarketType, 118 | msg: &str, 119 | ) -> Result, SimpleError> { 120 | let ws_msg = serde_json::from_str::>(msg).map_err(|_e| { 121 | SimpleError::new(format!("Failed to deserialize {msg} to WebsocketMsg")) 122 | })?; 123 | debug_assert_eq!(ws_msg.action, "Pushdata.depth"); 124 | let symbol = ws_msg.params.symbol.as_str(); 125 | let pair = crypto_pair::normalize_pair(symbol, EXCHANGE_NAME) 126 | .ok_or_else(|| SimpleError::new(format!("Failed to normalize {symbol} from {msg}")))?; 127 | 128 | let parse_order = |raw_order: &[Value; 3]| -> Order { 129 | let price = raw_order[0].as_str().unwrap().parse::().unwrap(); 130 | let (quantity_base, quantity_quote) = if raw_order[1].is_i64() { 131 | (0.0, 0.0) 132 | } else { 133 | let base = raw_order[1].as_str().unwrap().parse::().unwrap(); 134 | let quote = raw_order[2].as_str().unwrap().parse::().unwrap(); 135 | (base, quote) 136 | }; 137 | 138 | Order { price, quantity_base, quantity_quote, quantity_contract: None } 139 | }; 140 | 141 | let orderbook = OrderBookMsg { 142 | exchange: EXCHANGE_NAME.to_string(), 143 | market_type, 144 | symbol: symbol.to_string(), 145 | pair, 146 | msg_type: MessageType::L2Event, 147 | timestamp: ws_msg.time, 148 | seq_id: None, 149 | prev_seq_id: None, 150 | asks: if let Some(asks) = ws_msg.data.asks { 151 | asks.iter().map(parse_order).collect() 152 | } else { 153 | Vec::new() 154 | }, 155 | bids: if let Some(bids) = ws_msg.data.bids { 156 | bids.iter().map(parse_order).collect() 157 | } else { 158 | Vec::new() 159 | }, 160 | snapshot: false, 161 | json: msg.to_string(), 162 | }; 163 | 164 | Ok(vec![orderbook]) 165 | } 166 | -------------------------------------------------------------------------------- /crypto-msg-parser/src/exchanges/dydx/message.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::Value; 3 | use std::collections::HashMap; 4 | 5 | #[derive(Serialize, Deserialize)] 6 | pub(super) struct WebsocketMsg { 7 | #[serde(rename = "type")] 8 | pub type_: String, 9 | pub connection_id: String, 10 | pub message_id: i64, 11 | pub id: String, 12 | pub channel: String, 13 | pub contents: T, 14 | #[serde(flatten)] 15 | pub extra: HashMap, 16 | } 17 | 18 | #[derive(Serialize, Deserialize)] 19 | pub(super) struct L2SnapshotOrder { 20 | size: String, 21 | price: String, 22 | } 23 | 24 | #[derive(Serialize, Deserialize)] 25 | pub(super) struct L2SnapshotRawMsg { 26 | asks: Vec, 27 | bids: Vec, 28 | } 29 | -------------------------------------------------------------------------------- /crypto-msg-parser/src/exchanges/dydx/mod.rs: -------------------------------------------------------------------------------- 1 | mod dydx_swap; 2 | mod message; 3 | 4 | use crate::{OrderBookMsg, TradeMsg}; 5 | 6 | use crypto_market_type::MarketType; 7 | use serde_json::Value; 8 | use simple_error::SimpleError; 9 | 10 | use self::message::{L2SnapshotRawMsg, WebsocketMsg}; 11 | 12 | pub(crate) fn extract_symbol(msg: &str) -> Result { 13 | if let Ok(ws_msg) = serde_json::from_str::>(msg) { 14 | Ok(ws_msg.id) 15 | } else if serde_json::from_str::(msg).is_ok() { 16 | Ok("NONE".to_string()) 17 | } else if msg.starts_with(r#"{"markets":"#) { 18 | // https://api.dydx.exchange/v3/markets 19 | Ok("ALL".to_string()) 20 | } else { 21 | Err(SimpleError::new(format!("Unsupported message format {msg}"))) 22 | } 23 | } 24 | 25 | pub(crate) fn extract_timestamp( 26 | market_type: MarketType, 27 | msg: &str, 28 | ) -> Result, SimpleError> { 29 | match market_type { 30 | MarketType::LinearSwap => dydx_swap::extract_timestamp(msg), 31 | _ => Err(SimpleError::new(format!("Unknown dYdX market type {market_type}"))), 32 | } 33 | } 34 | 35 | pub(crate) fn parse_trade( 36 | market_type: MarketType, 37 | msg: &str, 38 | ) -> Result, SimpleError> { 39 | match market_type { 40 | MarketType::LinearSwap => dydx_swap::parse_trade(market_type, msg), 41 | _ => Err(SimpleError::new(format!("Unknown dYdX market type {market_type}"))), 42 | } 43 | } 44 | 45 | pub(crate) fn parse_l2( 46 | market_type: MarketType, 47 | msg: &str, 48 | timestamp: i64, 49 | ) -> Result, SimpleError> { 50 | match market_type { 51 | MarketType::LinearSwap => dydx_swap::parse_l2(market_type, msg, timestamp), 52 | _ => Err(SimpleError::new(format!("Unknown dYdX market type {market_type}"))), 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /crypto-msg-parser/src/exchanges/gate/gate_spot.rs: -------------------------------------------------------------------------------- 1 | use crypto_message::{BboMsg, CandlestickMsg, OrderBookMsg, TradeMsg}; 2 | use serde_json::Value; 3 | use simple_error::SimpleError; 4 | use std::collections::HashMap; 5 | 6 | use super::{gate_spot_20210916, gate_spot_current}; 7 | 8 | pub(super) fn extract_symbol(msg: &str) -> Result { 9 | let json_obj = serde_json::from_str::>(msg).map_err(|_e| { 10 | SimpleError::new(format!("Failed to deserialize {msg} to HashMap")) 11 | })?; 12 | if json_obj.contains_key("params") { 13 | gate_spot_20210916::extract_symbol(msg) 14 | } else { 15 | gate_spot_current::extract_symbol(msg) 16 | } 17 | } 18 | 19 | pub(super) fn extract_timestamp(msg: &str) -> Result, SimpleError> { 20 | let json_obj = serde_json::from_str::>(msg).map_err(|_e| { 21 | SimpleError::new(format!("Failed to deserialize {msg} to HashMap")) 22 | })?; 23 | if json_obj.contains_key("params") { 24 | #[allow(deprecated)] 25 | gate_spot_20210916::extract_timestamp(msg) 26 | } else { 27 | gate_spot_current::extract_timestamp(msg) 28 | } 29 | } 30 | 31 | pub(super) fn parse_trade(msg: &str) -> Result, SimpleError> { 32 | let json_obj = serde_json::from_str::>(msg).map_err(|_e| { 33 | SimpleError::new(format!("Failed to deserialize {msg} to HashMap")) 34 | })?; 35 | if json_obj.contains_key("params") { 36 | #[allow(deprecated)] 37 | gate_spot_20210916::parse_trade(msg) 38 | } else if json_obj.contains_key("result") { 39 | gate_spot_current::parse_trade(msg) 40 | } else { 41 | Err(SimpleError::new(format!("Unknown message format: {msg}"))) 42 | } 43 | } 44 | 45 | pub(super) fn parse_l2(msg: &str, timestamp: i64) -> Result, SimpleError> { 46 | let json_obj = serde_json::from_str::>(msg).map_err(|_e| { 47 | SimpleError::new(format!("Failed to deserialize {msg} to HashMap")) 48 | })?; 49 | if json_obj.contains_key("params") { 50 | #[allow(deprecated)] 51 | gate_spot_20210916::parse_l2(msg, timestamp) 52 | } else if json_obj.contains_key("result") { 53 | gate_spot_current::parse_l2(msg) 54 | } else { 55 | Err(SimpleError::new(format!("Unknown message format: {msg}"))) 56 | } 57 | } 58 | 59 | pub(super) fn parse_l2_topk(msg: &str) -> Result, SimpleError> { 60 | let json_obj = serde_json::from_str::>(msg).map_err(|_e| { 61 | SimpleError::new(format!("Failed to deserialize {msg} to HashMap")) 62 | })?; 63 | if json_obj.contains_key("params") { 64 | #[allow(deprecated)] 65 | gate_spot_20210916::parse_l2_topk(msg) 66 | } else if json_obj.contains_key("result") { 67 | gate_spot_current::parse_l2_topk(msg) 68 | } else { 69 | Err(SimpleError::new(format!("Unknown message format: {msg}"))) 70 | } 71 | } 72 | 73 | pub(super) fn parse_bbo(msg: &str) -> Result, SimpleError> { 74 | let json_obj = serde_json::from_str::>(msg).map_err(|_e| { 75 | SimpleError::new(format!("Failed to deserialize {msg} to HashMap")) 76 | })?; 77 | if json_obj.contains_key("params") { 78 | #[allow(deprecated)] 79 | gate_spot_20210916::parse_bbo(msg) 80 | } else if json_obj.contains_key("result") { 81 | gate_spot_current::parse_bbo(msg) 82 | } else { 83 | Err(SimpleError::new(format!("Unknown message format: {msg}"))) 84 | } 85 | } 86 | 87 | pub(super) fn parse_candlestick(msg: &str) -> Result, SimpleError> { 88 | let json_obj = 89 | serde_json::from_str::>(msg).map_err(SimpleError::from)?; 90 | if json_obj.contains_key("params") { 91 | #[allow(deprecated)] 92 | gate_spot_20210916::parse_candlestick(msg) 93 | } else if json_obj.contains_key("result") { 94 | gate_spot_current::parse_candlestick(msg) 95 | } else { 96 | Err(SimpleError::new(format!("Unknown message format: {msg}"))) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /crypto-msg-parser/src/exchanges/gate/messages.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::Value; 3 | use std::collections::HashMap; 4 | 5 | // https://www.gateio.pro/docs/apiv4/ws/en/#server-response 6 | // https://www.gateio.pro/docs/futures/ws/en/#response 7 | // https://www.gateio.pro/docs/delivery/ws/en/#response 8 | #[derive(Serialize, Deserialize)] 9 | pub(super) struct WebsocketMsg { 10 | pub time: i64, 11 | pub channel: String, 12 | pub event: String, 13 | pub error: Option, 14 | pub result: T, 15 | #[serde(flatten)] 16 | pub extra: HashMap, 17 | } 18 | -------------------------------------------------------------------------------- /crypto-msg-parser/src/exchanges/gate/mod.rs: -------------------------------------------------------------------------------- 1 | mod gate_spot; 2 | mod gate_spot_20210916; 3 | mod gate_spot_current; 4 | mod gate_swap; 5 | mod messages; 6 | 7 | use crypto_market_type::MarketType; 8 | use crypto_message::{BboMsg, CandlestickMsg}; 9 | 10 | use crate::{OrderBookMsg, TradeMsg}; 11 | 12 | use simple_error::SimpleError; 13 | 14 | pub(crate) fn extract_symbol(market_type: MarketType, msg: &str) -> Result { 15 | if market_type == MarketType::Spot { 16 | gate_spot::extract_symbol(msg) 17 | } else { 18 | gate_swap::extract_symbol(market_type, msg) 19 | } 20 | } 21 | 22 | pub(crate) fn extract_timestamp( 23 | market_type: MarketType, 24 | msg: &str, 25 | ) -> Result, SimpleError> { 26 | if market_type == MarketType::Spot { 27 | gate_spot::extract_timestamp(msg) 28 | } else { 29 | gate_swap::extract_timestamp(msg) 30 | } 31 | } 32 | 33 | pub(crate) fn parse_trade( 34 | market_type: MarketType, 35 | msg: &str, 36 | ) -> Result, SimpleError> { 37 | if market_type == MarketType::Spot { 38 | gate_spot::parse_trade(msg) 39 | } else { 40 | gate_swap::parse_trade(market_type, msg) 41 | } 42 | } 43 | 44 | pub(crate) fn parse_l2( 45 | market_type: MarketType, 46 | msg: &str, 47 | timestamp: Option, 48 | ) -> Result, SimpleError> { 49 | match market_type { 50 | MarketType::Spot => gate_spot::parse_l2( 51 | msg, 52 | timestamp.expect("Gate spot orderbook messages don't have timestamp"), 53 | ), 54 | MarketType::InverseFuture | MarketType::LinearFuture => { 55 | gate_swap::parse_l2_topk(market_type, msg) 56 | } 57 | MarketType::InverseSwap | MarketType::LinearSwap => gate_swap::parse_l2(market_type, msg), 58 | _ => Err(SimpleError::new(format!("Unsupported market type: {market_type:?}"))), 59 | } 60 | } 61 | 62 | pub(crate) fn parse_l2_topk( 63 | market_type: MarketType, 64 | msg: &str, 65 | ) -> Result, SimpleError> { 66 | if market_type == MarketType::Spot { 67 | gate_spot::parse_l2_topk(msg) 68 | } else { 69 | gate_swap::parse_l2_topk(market_type, msg) 70 | } 71 | } 72 | 73 | pub(crate) fn parse_bbo(market_type: MarketType, msg: &str) -> Result, SimpleError> { 74 | if market_type == MarketType::Spot { 75 | gate_spot::parse_bbo(msg) 76 | } else { 77 | gate_swap::parse_bbo(market_type, msg) 78 | } 79 | } 80 | 81 | pub(crate) fn parse_candlestick( 82 | market_type: MarketType, 83 | msg: &str, 84 | ) -> Result, SimpleError> { 85 | if market_type == MarketType::Spot { 86 | gate_spot::parse_candlestick(msg) 87 | } else { 88 | gate_swap::parse_candlestick(market_type, msg) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /crypto-msg-parser/src/exchanges/huobi/funding_rate.rs: -------------------------------------------------------------------------------- 1 | use crypto_market_type::MarketType; 2 | use crypto_msg_type::MessageType; 3 | 4 | use crate::FundingRateMsg; 5 | 6 | use serde::{Deserialize, Serialize}; 7 | use serde_json::Value; 8 | use simple_error::SimpleError; 9 | use std::collections::HashMap; 10 | 11 | #[derive(Serialize, Deserialize)] 12 | #[allow(non_snake_case)] 13 | struct RawFundingRateMsg { 14 | symbol: String, 15 | contract_code: String, 16 | fee_asset: String, 17 | funding_time: String, 18 | funding_rate: String, 19 | estimated_rate: String, 20 | settlement_time: String, 21 | #[serde(flatten)] 22 | extra: HashMap, 23 | } 24 | 25 | #[derive(Serialize, Deserialize)] 26 | pub(super) struct WebsocketMsg { 27 | op: String, 28 | pub topic: String, 29 | ts: i64, 30 | data: Vec, 31 | } 32 | 33 | pub(super) fn parse_funding_rate( 34 | market_type: MarketType, 35 | msg: &str, 36 | ) -> Result, SimpleError> { 37 | let ws_msg = serde_json::from_str::(msg) 38 | .map_err(|_e| SimpleError::new(format!("Failed to deserialize {msg} to WebsocketMsg")))?; 39 | let mut funding_rates: Vec = ws_msg 40 | .data 41 | .into_iter() 42 | .map(|raw_msg| FundingRateMsg { 43 | exchange: "huobi".to_string(), 44 | market_type, 45 | symbol: raw_msg.contract_code.clone(), 46 | pair: crypto_pair::normalize_pair(&raw_msg.contract_code, "huobi").unwrap(), 47 | msg_type: MessageType::FundingRate, 48 | timestamp: ws_msg.ts, 49 | funding_rate: raw_msg.funding_rate.parse::().unwrap(), 50 | funding_time: raw_msg.settlement_time.parse::().unwrap(), 51 | estimated_rate: Some(raw_msg.estimated_rate.parse::().unwrap()), 52 | json: serde_json::to_string(&raw_msg).unwrap(), 53 | }) 54 | .collect(); 55 | if funding_rates.len() == 1 { 56 | funding_rates[0].json = msg.to_string(); 57 | } 58 | Ok(funding_rates) 59 | } 60 | -------------------------------------------------------------------------------- /crypto-msg-parser/src/exchanges/huobi/huobi_linear.rs: -------------------------------------------------------------------------------- 1 | use crypto_market_type::MarketType; 2 | use crypto_msg_type::MessageType; 3 | 4 | use crypto_message::{CandlestickMsg, TradeMsg, TradeSide}; 5 | 6 | use serde::{Deserialize, Serialize}; 7 | use serde_json::Value; 8 | use simple_error::SimpleError; 9 | use std::collections::HashMap; 10 | 11 | use crate::exchanges::utils::calc_quantity_and_volume; 12 | 13 | use super::message::WebsocketMsg; 14 | 15 | const EXCHANGE_NAME: &str = "huobi"; 16 | 17 | // https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-subscribe-trade-detail-data 18 | // https://huobiapi.github.io/docs/option/v1/en/#subscribe-trade-detail-data 19 | #[derive(Serialize, Deserialize)] 20 | #[allow(non_snake_case)] 21 | struct LinearTradeMsg { 22 | id: i64, 23 | ts: i64, 24 | amount: f64, 25 | quantity: f64, 26 | trade_turnover: Option, 27 | price: f64, 28 | direction: String, // sell, buy 29 | #[serde(flatten)] 30 | extra: HashMap, 31 | } 32 | 33 | #[derive(Serialize, Deserialize)] 34 | struct TradeTick { 35 | id: i64, 36 | ts: i64, 37 | data: Vec, 38 | } 39 | 40 | // https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-subscribe-kline-data 41 | #[derive(Serialize, Deserialize)] 42 | #[allow(non_snake_case)] 43 | struct RawCandlestickMsg { 44 | id: i64, 45 | mrid: i64, 46 | open: f64, 47 | close: f64, 48 | low: f64, 49 | high: f64, 50 | amount: f64, 51 | vol: f64, 52 | trade_turnover: f64, 53 | count: u64, 54 | #[serde(flatten)] 55 | extra: HashMap, 56 | } 57 | 58 | pub(crate) fn parse_trade( 59 | market_type: MarketType, 60 | msg: &str, 61 | ) -> Result, SimpleError> { 62 | let ws_msg = serde_json::from_str::>(msg).map_err(SimpleError::from)?; 63 | 64 | let symbol = ws_msg.ch.split('.').nth(1).unwrap(); 65 | let pair = crypto_pair::normalize_pair(symbol, EXCHANGE_NAME) 66 | .ok_or_else(|| SimpleError::new(format!("Failed to normalize {symbol} from {msg}")))?; 67 | 68 | let mut trades: Vec = ws_msg 69 | .tick 70 | .data 71 | .into_iter() 72 | .map(|raw_trade| { 73 | let (_, quantity_quote, _) = calc_quantity_and_volume( 74 | EXCHANGE_NAME, 75 | market_type, 76 | &pair, 77 | raw_trade.price, 78 | raw_trade.amount, 79 | ); 80 | TradeMsg { 81 | exchange: EXCHANGE_NAME.to_string(), 82 | market_type, 83 | symbol: symbol.to_string(), 84 | pair: pair.to_string(), 85 | msg_type: MessageType::Trade, 86 | timestamp: raw_trade.ts, 87 | price: raw_trade.price, 88 | quantity_base: raw_trade.quantity, 89 | quantity_quote: if let Some(x) = raw_trade.trade_turnover { 90 | x 91 | } else { 92 | quantity_quote 93 | }, 94 | quantity_contract: Some(raw_trade.amount), 95 | side: if raw_trade.direction == "sell" { TradeSide::Sell } else { TradeSide::Buy }, 96 | trade_id: raw_trade.id.to_string(), 97 | json: serde_json::to_string(&raw_trade).unwrap(), 98 | } 99 | }) 100 | .collect(); 101 | 102 | if trades.len() == 1 { 103 | trades[0].json = msg.to_string(); 104 | } 105 | Ok(trades) 106 | } 107 | 108 | pub(super) fn parse_candlestick( 109 | market_type: MarketType, 110 | msg: &str, 111 | ) -> Result, SimpleError> { 112 | let ws_msg = 113 | serde_json::from_str::>(msg).map_err(SimpleError::from)?; 114 | debug_assert!(ws_msg.ch.contains(".kline.")); 115 | 116 | let (symbol, period) = { 117 | let arr: Vec<&str> = ws_msg.ch.split('.').collect(); 118 | let symbol = arr[1]; 119 | let period = arr[3]; 120 | (symbol, period) 121 | }; 122 | let pair = crypto_pair::normalize_pair(symbol, EXCHANGE_NAME).unwrap(); 123 | 124 | let kline_msg = CandlestickMsg { 125 | exchange: EXCHANGE_NAME.to_string(), 126 | market_type, 127 | msg_type: MessageType::Candlestick, 128 | symbol: symbol.to_string(), 129 | pair, 130 | timestamp: ws_msg.ts, 131 | begin_time: ws_msg.tick.id, 132 | open: ws_msg.tick.open, 133 | high: ws_msg.tick.high, 134 | low: ws_msg.tick.low, 135 | close: ws_msg.tick.close, 136 | volume: ws_msg.tick.amount, 137 | quote_volume: Some(ws_msg.tick.trade_turnover), 138 | period: period.to_string(), 139 | json: msg.to_string(), 140 | }; 141 | 142 | Ok(vec![kline_msg]) 143 | } 144 | -------------------------------------------------------------------------------- /crypto-msg-parser/src/exchanges/huobi/message.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize, Deserialize)] 4 | pub(super) struct WebsocketMsg { 5 | pub ch: String, 6 | pub ts: i64, 7 | pub tick: T, 8 | } 9 | -------------------------------------------------------------------------------- /crypto-msg-parser/src/exchanges/huobi/mod.rs: -------------------------------------------------------------------------------- 1 | mod funding_rate; 2 | mod huobi_inverse; 3 | mod huobi_linear; 4 | mod huobi_spot; 5 | mod message; 6 | 7 | use std::collections::HashMap; 8 | 9 | use crypto_market_type::MarketType; 10 | use crypto_message::{BboMsg, CandlestickMsg}; 11 | use crypto_msg_type::MessageType; 12 | 13 | use crate::{FundingRateMsg, OrderBookMsg, TradeMsg}; 14 | 15 | use serde_json::Value; 16 | use simple_error::SimpleError; 17 | 18 | use message::WebsocketMsg; 19 | 20 | pub(crate) fn extract_symbol(msg: &str) -> Result { 21 | let json_obj = serde_json::from_str::>(msg).unwrap(); 22 | if json_obj.contains_key("data") 23 | && json_obj["data"].is_array() 24 | && json_obj["data"].as_array().unwrap().len() > 1 25 | { 26 | // open interest from RESTful API 27 | return Ok("ALL".to_string()); 28 | } 29 | 30 | let channel = if json_obj.contains_key("ch") { 31 | json_obj["ch"].as_str().unwrap() 32 | } else if json_obj.contains_key("topic") { 33 | json_obj["topic"].as_str().unwrap() 34 | } else { 35 | return Err(SimpleError::new(format!("No channel or topic found in {msg}"))); 36 | }; 37 | if channel == "public.*.funding_rate" { 38 | Ok("ALL".to_string()) 39 | } else { 40 | let symbol = channel.split('.').nth(1).unwrap(); 41 | Ok(symbol.to_string()) 42 | } 43 | } 44 | 45 | pub(crate) fn extract_timestamp(msg: &str) -> Result, SimpleError> { 46 | let json_obj = serde_json::from_str::>(msg).unwrap(); 47 | json_obj.get("ts").map_or(Err(SimpleError::new(msg)), |ts| Ok(Some(ts.as_i64().unwrap()))) 48 | } 49 | 50 | pub(crate) fn get_msg_type(msg: &str) -> MessageType { 51 | if let Ok(ws_msg) = serde_json::from_str::>(msg) { 52 | let channel = ws_msg.ch; 53 | if channel.ends_with("trade.detail") { 54 | MessageType::Trade 55 | } else if channel.ends_with("depth.size_20.high_freq") 56 | || channel.ends_with("depth.size_150.high_freq") 57 | || channel.ends_with("mbp.20") 58 | { 59 | MessageType::L2Event 60 | } else if channel.contains(".depth.step") { 61 | MessageType::L2TopK 62 | } else if channel.ends_with("bbo") { 63 | MessageType::BBO 64 | } else if channel.ends_with("detail") { 65 | MessageType::Ticker 66 | } else if channel.contains(".kline.") { 67 | MessageType::Candlestick 68 | } else if channel.ends_with(".funding_rate") { 69 | MessageType::FundingRate 70 | } else { 71 | MessageType::Other 72 | } 73 | } else if let Ok(ws_msg) = serde_json::from_str::(msg) { 74 | if ws_msg.topic.ends_with(".funding_rate") { 75 | MessageType::FundingRate 76 | } else { 77 | MessageType::Other 78 | } 79 | } else { 80 | MessageType::Other 81 | } 82 | } 83 | 84 | pub(crate) fn parse_trade( 85 | market_type: MarketType, 86 | msg: &str, 87 | ) -> Result, SimpleError> { 88 | match market_type { 89 | MarketType::Spot => huobi_spot::parse_trade(msg), 90 | MarketType::InverseFuture | MarketType::InverseSwap => { 91 | huobi_inverse::parse_trade(market_type, msg) 92 | } 93 | MarketType::LinearFuture | MarketType::LinearSwap | MarketType::EuropeanOption => { 94 | huobi_linear::parse_trade(market_type, msg) 95 | } 96 | _ => Err(SimpleError::new(format!("Unknown huobi market type {market_type}"))), 97 | } 98 | } 99 | 100 | pub(crate) fn parse_funding_rate( 101 | market_type: MarketType, 102 | msg: &str, 103 | ) -> Result, SimpleError> { 104 | if market_type == MarketType::InverseSwap || market_type == MarketType::LinearSwap { 105 | funding_rate::parse_funding_rate(market_type, msg) 106 | } else { 107 | Err(SimpleError::new(format!("Huobi {market_type} does NOT have funding rates"))) 108 | } 109 | } 110 | 111 | pub(crate) fn parse_l2( 112 | market_type: MarketType, 113 | msg: &str, 114 | ) -> Result, SimpleError> { 115 | match market_type { 116 | MarketType::Spot => huobi_spot::parse_l2(msg), 117 | MarketType::InverseFuture | MarketType::InverseSwap => { 118 | huobi_inverse::parse_l2(market_type, msg) 119 | } 120 | MarketType::LinearFuture | MarketType::LinearSwap | MarketType::EuropeanOption => { 121 | huobi_inverse::parse_l2(market_type, msg) 122 | } 123 | _ => Err(SimpleError::new(format!("Unknown huobi market type {market_type}"))), 124 | } 125 | } 126 | 127 | pub(crate) fn parse_l2_topk( 128 | market_type: MarketType, 129 | msg: &str, 130 | ) -> Result, SimpleError> { 131 | parse_l2(market_type, msg) 132 | } 133 | 134 | pub(crate) fn parse_bbo(market_type: MarketType, msg: &str) -> Result, SimpleError> { 135 | match market_type { 136 | MarketType::Spot => huobi_spot::parse_bbo(msg), 137 | MarketType::InverseFuture | MarketType::InverseSwap => { 138 | huobi_inverse::parse_bbo(market_type, msg) 139 | } 140 | MarketType::LinearFuture | MarketType::LinearSwap | MarketType::EuropeanOption => { 141 | huobi_inverse::parse_bbo(market_type, msg) 142 | } 143 | _ => Err(SimpleError::new(format!("Unknown huobi market type {market_type}"))), 144 | } 145 | } 146 | 147 | pub(crate) fn parse_candlestick( 148 | market_type: MarketType, 149 | msg: &str, 150 | ) -> Result, SimpleError> { 151 | match market_type { 152 | MarketType::Spot => huobi_spot::parse_candlestick(market_type, msg), 153 | MarketType::InverseSwap | MarketType::InverseFuture => { 154 | huobi_inverse::parse_candlestick(market_type, msg) 155 | } 156 | MarketType::LinearSwap => huobi_linear::parse_candlestick(market_type, msg), 157 | _ => Err(SimpleError::new(format!("Unknown huobi market type {market_type}"))), 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /crypto-msg-parser/src/exchanges/kraken/mod.rs: -------------------------------------------------------------------------------- 1 | mod kraken_futures; 2 | mod kraken_spot; 3 | 4 | use std::collections::HashMap; 5 | 6 | use crypto_market_type::MarketType; 7 | use crypto_msg_type::MessageType; 8 | 9 | use crate::{BboMsg, CandlestickMsg, OrderBookMsg, TradeMsg}; 10 | 11 | use serde_json::Value; 12 | use simple_error::SimpleError; 13 | 14 | pub(crate) fn extract_symbol(market_type: MarketType, msg: &str) -> Result { 15 | match market_type { 16 | MarketType::Spot => kraken_spot::extract_symbol(msg), 17 | MarketType::InverseFuture | MarketType::InverseSwap => kraken_futures::extract_symbol(msg), 18 | _ => panic!("Kraken unknown market_type: {market_type}"), 19 | } 20 | } 21 | 22 | pub(crate) fn extract_timestamp( 23 | market_type: MarketType, 24 | msg: &str, 25 | ) -> Result, SimpleError> { 26 | match market_type { 27 | MarketType::Spot => kraken_spot::extract_timestamp(msg), 28 | MarketType::InverseFuture | MarketType::InverseSwap => { 29 | kraken_futures::extract_timestamp(msg) 30 | } 31 | _ => panic!("Kraken unknown market_type: {market_type}"), 32 | } 33 | } 34 | 35 | pub(crate) fn get_msg_type(msg: &str) -> MessageType { 36 | if let Ok(arr) = serde_json::from_str::>(msg) { 37 | // spot 38 | let channel = arr[arr.len() - 2].as_str().unwrap(); 39 | if channel == "ticker" { 40 | MessageType::Ticker 41 | } else if channel == "trade" { 42 | MessageType::Trade 43 | } else if channel == "spread" { 44 | MessageType::BBO 45 | } else if channel.starts_with("book-") { 46 | MessageType::L2Event 47 | } else if channel.starts_with("ohlc-") { 48 | MessageType::Candlestick 49 | } else { 50 | MessageType::Other 51 | } 52 | } else if let Ok(obj) = serde_json::from_str::>(msg) { 53 | // futures 54 | if let Some(feed) = obj.get("feed") { 55 | match feed.as_str().unwrap() { 56 | "trade" | "trade_snapshot" => MessageType::Trade, 57 | "ticker" => MessageType::Ticker, 58 | "book" | "book_snapshot" => MessageType::L2Event, 59 | _ => MessageType::Other, 60 | } 61 | } else { 62 | MessageType::Other 63 | } 64 | } else { 65 | MessageType::Other 66 | } 67 | } 68 | 69 | pub(crate) fn parse_trade( 70 | market_type: MarketType, 71 | msg: &str, 72 | ) -> Result, SimpleError> { 73 | match market_type { 74 | MarketType::Spot => kraken_spot::parse_trade(msg), 75 | MarketType::InverseFuture | MarketType::InverseSwap => kraken_futures::parse_trade(msg), 76 | _ => panic!("Kraken unknown market_type: {market_type}"), 77 | } 78 | } 79 | 80 | pub(crate) fn parse_l2( 81 | market_type: MarketType, 82 | msg: &str, 83 | ) -> Result, SimpleError> { 84 | match market_type { 85 | MarketType::Spot => kraken_spot::parse_l2(msg), 86 | MarketType::InverseFuture | MarketType::InverseSwap => kraken_futures::parse_l2(msg), 87 | _ => panic!("Kraken unknown market_type: {market_type}"), 88 | } 89 | } 90 | 91 | pub(crate) fn parse_bbo( 92 | market_type: MarketType, 93 | msg: &str, 94 | received_at: Option, 95 | ) -> Result, SimpleError> { 96 | match market_type { 97 | MarketType::Spot => kraken_spot::parse_bbo(msg, received_at), 98 | _ => Err(SimpleError::new("Not implemented")), 99 | } 100 | } 101 | 102 | pub(crate) fn parse_candlestick( 103 | market_type: MarketType, 104 | msg: &str, 105 | ) -> Result, SimpleError> { 106 | match market_type { 107 | MarketType::Spot => kraken_spot::parse_candlestick(msg), 108 | _ => Err(SimpleError::new("Not implemented")), 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /crypto-msg-parser/src/exchanges/kucoin/message.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize, Deserialize)] 4 | pub(super) struct WebsocketMsg { 5 | pub subject: String, 6 | pub topic: String, 7 | #[serde(rename = "type")] 8 | pub type_: String, 9 | pub data: T, 10 | } 11 | 12 | #[derive(Serialize, Deserialize)] 13 | pub(super) struct RestfulMsg { 14 | pub code: String, 15 | pub data: T, 16 | } 17 | -------------------------------------------------------------------------------- /crypto-msg-parser/src/exchanges/mexc/mod.rs: -------------------------------------------------------------------------------- 1 | mod mexc_spot; 2 | mod mexc_swap; 3 | 4 | use std::collections::HashMap; 5 | 6 | use crypto_market_type::MarketType; 7 | 8 | use crate::{CandlestickMsg, OrderBookMsg, TradeMsg}; 9 | 10 | use serde_json::Value; 11 | use simple_error::SimpleError; 12 | 13 | pub(super) const EXCHANGE_NAME: &str = "mexc"; 14 | 15 | pub(crate) fn extract_symbol(msg: &str) -> Result { 16 | if let Ok(arr) = serde_json::from_str::>(msg) { 17 | Ok(arr[1]["symbol"].as_str().unwrap().to_string()) 18 | } else if let Ok(json_obj) = serde_json::from_str::>(msg) { 19 | if json_obj.contains_key("code") && json_obj.contains_key("data") { 20 | // RESTful 21 | let code = json_obj["code"].as_i64().unwrap(); 22 | if code != 0 && code != 200 { 23 | return Err(SimpleError::new(format!("Error HTTP response {msg}"))); 24 | } 25 | let data = json_obj["data"].as_object().unwrap(); 26 | if let Some(symbol) = data.get("symbol") { 27 | Ok(symbol.as_str().unwrap().to_string()) 28 | } else { 29 | Ok("NONE".to_string()) 30 | } 31 | } else if let Some(symbol) = json_obj.get("symbol") { 32 | Ok(symbol.as_str().unwrap().to_string()) 33 | } else { 34 | Err(SimpleError::new(format!("Unknown message format {msg}"))) 35 | } 36 | } else { 37 | Err(SimpleError::new(format!("Unknown message format {msg}"))) 38 | } 39 | } 40 | 41 | pub(crate) fn extract_timestamp(msg: &str) -> Result, SimpleError> { 42 | if let Ok(arr) = serde_json::from_str::>(msg) { 43 | let channel = arr[0].as_str().unwrap(); 44 | match channel { 45 | "push.symbol" => { 46 | let data = arr[1]["data"].as_object().unwrap(); 47 | if let Some(deals) = data.get("deals") { 48 | let raw_trades = deals.as_array().unwrap(); 49 | let timestamp = 50 | raw_trades.iter().map(|raw_trade| raw_trade["t"].as_i64().unwrap()).max(); 51 | 52 | if timestamp.is_none() { 53 | Err(SimpleError::new(format!("deals is empty in {msg}"))) 54 | } else { 55 | Ok(timestamp) 56 | } 57 | } else { 58 | Ok(None) 59 | } 60 | } 61 | "push.kline" => { 62 | let data = arr[1]["data"].as_object().unwrap(); 63 | let t = data["t"].as_i64().unwrap(); 64 | Ok(Some(t * 1000)) 65 | } 66 | _ => Err(SimpleError::new(format!("Unknown channel {channel} in {msg}"))), 67 | } 68 | } else if let Ok(json_obj) = serde_json::from_str::>(msg) { 69 | if json_obj.contains_key("code") && json_obj.contains_key("data") { 70 | // RESTful 71 | let code = json_obj["code"].as_i64().unwrap(); 72 | if code != 0 && code != 200 { 73 | return Err(SimpleError::new(format!("Error HTTP response {msg}"))); 74 | } 75 | let data = json_obj["data"].as_object().unwrap(); 76 | Ok(data.get("timestamp").map(|x| x.as_i64().unwrap())) 77 | } else { 78 | // websocket 79 | let channel = json_obj["channel"].as_str().unwrap(); 80 | if let Some(x) = json_obj.get("ts") { 81 | Ok(Some(x.as_i64().unwrap())) 82 | } else if channel == "push.kline" { 83 | let data = json_obj["data"].as_object().unwrap(); 84 | if let Some(tdt) = data.get("tdt") { 85 | Ok(Some(tdt.as_i64().unwrap())) 86 | } else { 87 | Ok(Some(data["t"].as_i64().unwrap() * 1000)) 88 | } 89 | } else if let Some(deals) = json_obj["data"].get("deals") { 90 | let timestamp = deals 91 | .as_array() 92 | .unwrap() 93 | .iter() 94 | .map(|raw_trade| raw_trade["t"].as_i64().unwrap()) 95 | .max(); 96 | if timestamp.is_none() { 97 | Err(SimpleError::new(format!("deals is empty in {msg}"))) 98 | } else { 99 | Ok(timestamp) 100 | } 101 | } else { 102 | Ok(None) 103 | } 104 | } 105 | } else { 106 | Err(SimpleError::new(format!("Failed to extract symbol from {msg}"))) 107 | } 108 | } 109 | 110 | pub(crate) fn parse_trade( 111 | market_type: MarketType, 112 | msg: &str, 113 | ) -> Result, SimpleError> { 114 | if market_type == MarketType::Spot { 115 | mexc_spot::parse_trade(msg) 116 | } else { 117 | mexc_swap::parse_trade(market_type, msg) 118 | } 119 | } 120 | 121 | pub(crate) fn parse_l2( 122 | market_type: MarketType, 123 | msg: &str, 124 | timestamp: Option, 125 | ) -> Result, SimpleError> { 126 | if market_type == MarketType::Spot { 127 | mexc_spot::parse_l2( 128 | msg, 129 | timestamp.expect("MEXC Spot orderbook messages don't have timestamp"), 130 | ) 131 | } else { 132 | mexc_swap::parse_l2(market_type, msg) 133 | } 134 | } 135 | 136 | pub(crate) fn parse_l2_topk( 137 | market_type: MarketType, 138 | msg: &str, 139 | timestamp: Option, 140 | ) -> Result, SimpleError> { 141 | if market_type == MarketType::Spot { 142 | mexc_spot::parse_l2_topk( 143 | msg, 144 | timestamp.expect("MEXC Spot L2TopK messages don't have timestamp"), 145 | ) 146 | } else { 147 | mexc_swap::parse_l2(market_type, msg) 148 | } 149 | } 150 | 151 | pub(crate) fn parse_candlestick( 152 | market_type: MarketType, 153 | msg: &str, 154 | received_at: Option, 155 | ) -> Result, SimpleError> { 156 | if market_type == MarketType::Spot { 157 | mexc_spot::parse_candlestick(msg, received_at) 158 | } else { 159 | mexc_swap::parse_candlestick(market_type, msg) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /crypto-msg-parser/src/exchanges/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod utils; 2 | 3 | pub(super) mod binance; 4 | pub(super) mod bitfinex; 5 | pub(super) mod bitget; 6 | pub(super) mod bithumb; 7 | pub mod bitmex; 8 | pub(super) mod bitstamp; 9 | pub(super) mod bitz; 10 | pub(super) mod bybit; 11 | pub(super) mod coinbase_pro; 12 | pub(super) mod deribit; 13 | pub(super) mod dydx; 14 | pub(super) mod ftx; 15 | pub(super) mod gate; 16 | pub(super) mod huobi; 17 | pub(super) mod kraken; 18 | pub(super) mod kucoin; 19 | pub(super) mod mexc; 20 | pub(super) mod okx; 21 | pub(super) mod zb; 22 | pub(super) mod zbg; 23 | -------------------------------------------------------------------------------- /crypto-msg-parser/src/exchanges/okx/mod.rs: -------------------------------------------------------------------------------- 1 | mod okx_v3; 2 | mod okx_v5; 3 | 4 | use crypto_message::{BboMsg, CandlestickMsg}; 5 | use simple_error::SimpleError; 6 | use std::collections::HashMap; 7 | 8 | use crate::{FundingRateMsg, OrderBookMsg, TradeMsg}; 9 | use crypto_market_type::MarketType; 10 | use crypto_msg_type::MessageType; 11 | use serde_json::Value; 12 | 13 | pub(super) const EXCHANGE_NAME: &str = "okx"; 14 | 15 | pub(crate) fn extract_symbol(_market_type: MarketType, msg: &str) -> Result { 16 | let obj = serde_json::from_str::>(msg).map_err(SimpleError::from)?; 17 | if obj.contains_key("arg") && obj.contains_key("data") { 18 | okx_v5::extract_symbol(msg) 19 | } else if obj.contains_key("table") && obj.contains_key("data") { 20 | okx_v3::extract_symbol(msg) 21 | } else if obj.contains_key("code") && obj.contains_key("msg") && obj.contains_key("data") { 22 | // restful v5 23 | okx_v5::extract_symbol(msg) 24 | } else { 25 | // restful v3 26 | okx_v3::extract_symbol(msg) 27 | } 28 | } 29 | 30 | pub(crate) fn extract_timestamp( 31 | _market_type: MarketType, 32 | msg: &str, 33 | ) -> Result, SimpleError> { 34 | let obj = serde_json::from_str::>(msg).map_err(SimpleError::from)?; 35 | if obj.contains_key("arg") && obj.contains_key("data") { 36 | // websocket v5 37 | okx_v5::extract_timestamp(msg) 38 | } else if obj.contains_key("table") && obj.contains_key("data") { 39 | // websocket v3 40 | okx_v3::extract_timestamp(msg) 41 | } else if obj.contains_key("code") && obj.contains_key("msg") && obj.contains_key("data") { 42 | // restful v5 43 | okx_v5::extract_timestamp(msg) 44 | } else { 45 | // restful v3 46 | okx_v3::extract_timestamp(msg) 47 | } 48 | } 49 | 50 | pub(crate) fn get_msg_type(msg: &str) -> MessageType { 51 | let obj = 52 | serde_json::from_str::>(msg).map_err(SimpleError::from).unwrap(); 53 | if obj.contains_key("arg") && obj.contains_key("data") { 54 | // websocket v5 55 | okx_v5::get_msg_type(msg) 56 | } else if obj.contains_key("table") && obj.contains_key("data") { 57 | // websocket v3 58 | okx_v3::get_msg_type(msg) 59 | } else if obj.contains_key("code") && obj.contains_key("msg") && obj.contains_key("data") { 60 | // restful v5 61 | okx_v5::get_msg_type(msg) 62 | } else { 63 | // restful v3 64 | okx_v3::get_msg_type(msg) 65 | } 66 | } 67 | 68 | pub(crate) fn parse_trade( 69 | market_type: MarketType, 70 | msg: &str, 71 | ) -> Result, SimpleError> { 72 | let obj = 73 | serde_json::from_str::>(msg).map_err(SimpleError::from).unwrap(); 74 | if obj.contains_key("arg") && obj.contains_key("data") { 75 | okx_v5::parse_trade(market_type, msg) 76 | } else if obj.contains_key("table") && obj.contains_key("data") { 77 | okx_v3::parse_trade(market_type, msg) 78 | } else { 79 | panic!("Unknown msg format {msg}") 80 | } 81 | } 82 | 83 | pub(crate) fn parse_l2( 84 | market_type: MarketType, 85 | msg: &str, 86 | ) -> Result, SimpleError> { 87 | let obj = 88 | serde_json::from_str::>(msg).map_err(SimpleError::from).unwrap(); 89 | if obj.contains_key("arg") && obj.contains_key("data") { 90 | okx_v5::parse_l2(market_type, msg) 91 | } else if obj.contains_key("table") && obj.contains_key("data") { 92 | okx_v3::parse_l2(market_type, msg) 93 | } else { 94 | panic!("Unknown msg format {msg}") 95 | } 96 | } 97 | 98 | pub(crate) fn parse_l2_topk( 99 | market_type: MarketType, 100 | msg: &str, 101 | ) -> Result, SimpleError> { 102 | parse_l2(market_type, msg) 103 | } 104 | 105 | pub(crate) fn parse_funding_rate( 106 | market_type: MarketType, 107 | msg: &str, 108 | received_at: i64, 109 | ) -> Result, SimpleError> { 110 | let obj = serde_json::from_str::>(msg).map_err(SimpleError::from)?; 111 | if obj.contains_key("arg") && obj.contains_key("data") { 112 | okx_v5::parse_funding_rate(market_type, msg, received_at) 113 | } else if obj.contains_key("table") && obj.contains_key("data") { 114 | okx_v3::parse_funding_rate(market_type, msg, received_at) 115 | } else { 116 | panic!("Unknown msg format {msg}") 117 | } 118 | } 119 | 120 | pub(crate) fn parse_bbo(market_type: MarketType, msg: &str) -> Result, SimpleError> { 121 | let obj = serde_json::from_str::>(msg).map_err(SimpleError::from)?; 122 | if obj.contains_key("arg") && obj.contains_key("data") { 123 | okx_v5::parse_bbo(market_type, msg) 124 | } else if obj.contains_key("table") && obj.contains_key("data") { 125 | okx_v3::parse_bbo(market_type, msg) 126 | } else { 127 | panic!("Unknown msg format {msg}") 128 | } 129 | } 130 | 131 | pub(crate) fn parse_candlestick( 132 | market_type: MarketType, 133 | msg: &str, 134 | received_at: i64, 135 | ) -> Result, SimpleError> { 136 | let obj = serde_json::from_str::>(msg).map_err(SimpleError::from)?; 137 | if obj.contains_key("arg") && obj.contains_key("data") { 138 | okx_v5::parse_candlestick(market_type, msg, received_at) 139 | } else if obj.contains_key("table") && obj.contains_key("data") { 140 | okx_v3::parse_candlestick(market_type, msg) 141 | } else { 142 | panic!("Unknown msg format {msg}") 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /crypto-msg-parser/src/exchanges/utils.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use crypto_market_type::MarketType; 4 | use reqwest::{header, Result}; 5 | use serde::{Deserialize, Deserializer}; 6 | use serde_json::Value; 7 | 8 | pub(super) fn http_get(url: &str) -> Result { 9 | let mut headers = header::HeaderMap::new(); 10 | headers.insert(header::CONTENT_TYPE, header::HeaderValue::from_static("application/json")); 11 | 12 | let client = reqwest::blocking::Client::builder() 13 | .default_headers(headers) 14 | .timeout(Duration::from_secs(10)) 15 | .user_agent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36") 16 | .gzip(true) 17 | .build()?; 18 | let response = client.get(url).send()?; 19 | 20 | match response.error_for_status() { 21 | Ok(resp) => Ok(resp.text()?), 22 | Err(error) => Err(error), 23 | } 24 | } 25 | 26 | const PRECISION: f64 = 1000000000.0; // 9 decimals 27 | 28 | pub fn round(f: f64) -> f64 { 29 | (f * PRECISION).round() / PRECISION 30 | } 31 | 32 | // returns (quantity_base, quantity_quote, quantity_contract) 33 | pub(super) fn calc_quantity_and_volume( 34 | exchange: &str, 35 | market_type: MarketType, 36 | pair: &str, 37 | price: f64, 38 | quantity: f64, 39 | ) -> (f64, f64, Option) { 40 | let contract_value = 41 | crypto_contract_value::get_contract_value(exchange, market_type, pair).unwrap(); 42 | match market_type { 43 | MarketType::Spot => (quantity, round(quantity * price), None), 44 | MarketType::InverseSwap | MarketType::InverseFuture => { 45 | let quantity_quote = quantity * contract_value; 46 | (quantity_quote / price, quantity_quote, Some(quantity)) 47 | } 48 | MarketType::LinearSwap | MarketType::LinearFuture | MarketType::Move | MarketType::BVOL => { 49 | let quantity_base = quantity * contract_value; 50 | (round(quantity_base), round(quantity_base * price), Some(quantity)) 51 | } 52 | MarketType::EuropeanOption => { 53 | let quantity_base = quantity * contract_value; 54 | (quantity_base, quantity_base * price, Some(quantity)) 55 | } 56 | _ => panic!("Unknown market_type {market_type}"), 57 | } 58 | } 59 | 60 | const MAX_UNIX_TIMESTAMP: i64 = 10_i64.pow(10) - 1; 61 | const MAX_UNIX_TIMESTAMP_MS: i64 = 10_i64.pow(13) - 1; 62 | 63 | // Convert a UNIX timestamp to a timestamp in milliseconds if needed. 64 | const fn convert_unix_timestamp_if_needed(ts: i64) -> i64 { 65 | if ts <= MAX_UNIX_TIMESTAMP { 66 | ts * 1000 67 | } else if ts <= MAX_UNIX_TIMESTAMP_MS { 68 | ts 69 | } else { 70 | ts / 1000 71 | } 72 | } 73 | 74 | // Convert a JSON value to a timestamp in milliseconds. 75 | pub(super) fn convert_timestamp(v: &Value) -> Option { 76 | if v.is_i64() { 77 | let ts = v.as_i64().unwrap(); 78 | Some(convert_unix_timestamp_if_needed(ts)) 79 | } else if v.is_string() { 80 | let s = v.as_str().unwrap(); 81 | let ts = s.parse::().unwrap_or_else(|_| panic!("{}", v.to_string())); 82 | Some(convert_unix_timestamp_if_needed(ts)) 83 | } else { 84 | None 85 | } 86 | } 87 | 88 | // copied from https://github.com/serde-rs/serde/issues/1098 89 | pub(super) fn deserialize_null_default<'de, D, T>( 90 | deserializer: D, 91 | ) -> std::result::Result 92 | where 93 | T: Default + Deserialize<'de>, 94 | D: Deserializer<'de>, 95 | { 96 | let opt = Option::deserialize(deserializer)?; 97 | Ok(opt.unwrap_or_default()) 98 | } 99 | -------------------------------------------------------------------------------- /crypto-msg-parser/src/exchanges/zb/mod.rs: -------------------------------------------------------------------------------- 1 | mod zb_spot; 2 | mod zb_swap; 3 | 4 | use std::collections::HashMap; 5 | 6 | use crypto_market_type::MarketType; 7 | use crypto_message::CandlestickMsg; 8 | use serde_json::Value; 9 | 10 | use crate::{OrderBookMsg, TradeMsg}; 11 | 12 | use simple_error::SimpleError; 13 | 14 | const EXCHANGE_NAME: &str = "zb"; 15 | 16 | pub(crate) fn extract_symbol(_market_type: MarketType, msg: &str) -> Result { 17 | let obj = serde_json::from_str::>(msg) 18 | .map_err(|_e| SimpleError::new(format!("Failed to parse the JSON string {msg}")))?; 19 | if let Some(channel) = obj.get("channel") { 20 | // websocket 21 | let channel = channel.as_str().unwrap(); 22 | if channel.contains('.') { 23 | let symbol = channel.split('.').next().unwrap(); 24 | Ok(symbol.to_string()) 25 | } else if channel.contains('_') { 26 | let symbol = channel.split('_').next().unwrap(); 27 | Ok(symbol.to_string()) 28 | } else { 29 | Err(SimpleError::new(format!("Failed to extract symbol from {msg}"))) 30 | } 31 | } else if obj.contains_key("asks") && obj.contains_key("bids") { 32 | // e.g., https://api.zbex.site/data/v1/depth?market=btc_usdt&size=50 33 | Ok("NONE".to_string()) 34 | } else if obj.contains_key("code") && obj.contains_key("desc") && obj.contains_key("data") { 35 | // ZB linear_swap RESTful 36 | let data = obj["data"].as_object().unwrap(); 37 | if let Some(symbol) = data.get("symbol") { 38 | Ok(symbol.as_str().unwrap().to_string()) 39 | } else { 40 | Ok("NONE".to_string()) 41 | } 42 | } else { 43 | Err(SimpleError::new(format!("Unknown message format {msg}"))) 44 | } 45 | } 46 | 47 | pub(crate) fn extract_timestamp( 48 | market_type: MarketType, 49 | msg: &str, 50 | ) -> Result, SimpleError> { 51 | if market_type == MarketType::Spot { 52 | zb_spot::extract_timestamp(msg) 53 | } else { 54 | zb_swap::extract_timestamp(market_type, msg) 55 | } 56 | } 57 | 58 | pub(crate) fn parse_trade( 59 | market_type: MarketType, 60 | msg: &str, 61 | ) -> Result, SimpleError> { 62 | if market_type == MarketType::Spot { 63 | zb_spot::parse_trade(msg) 64 | } else { 65 | zb_swap::parse_trade(market_type, msg) 66 | } 67 | } 68 | 69 | pub(crate) fn parse_l2( 70 | market_type: MarketType, 71 | msg: &str, 72 | ) -> Result, SimpleError> { 73 | if market_type == MarketType::Spot { 74 | zb_spot::parse_l2(msg) 75 | } else { 76 | zb_swap::parse_l2(market_type, msg) 77 | } 78 | } 79 | 80 | pub(crate) fn parse_l2_topk( 81 | market_type: MarketType, 82 | msg: &str, 83 | ) -> Result, SimpleError> { 84 | if market_type == MarketType::Spot { 85 | zb_spot::parse_l2_topk(msg) 86 | } else { 87 | zb_swap::parse_l2(market_type, msg) 88 | } 89 | } 90 | 91 | pub(crate) fn parse_candlestick( 92 | market_type: MarketType, 93 | msg: &str, 94 | ) -> Result, SimpleError> { 95 | if market_type == MarketType::Spot { 96 | zb_spot::parse_candlestick(msg) 97 | } else { 98 | zb_swap::parse_candlestick(market_type, msg) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /crypto-msg-parser/src/exchanges/zbg/mod.rs: -------------------------------------------------------------------------------- 1 | mod zbg_spot; 2 | mod zbg_swap; 3 | 4 | use crypto_market_type::MarketType; 5 | 6 | use crate::{CandlestickMsg, OrderBookMsg, TradeMsg}; 7 | 8 | use simple_error::SimpleError; 9 | 10 | const EXCHANGE_NAME: &str = "zbg"; 11 | 12 | pub(crate) fn extract_symbol(market_type: MarketType, msg: &str) -> Result { 13 | if market_type == MarketType::Spot { 14 | zbg_spot::extract_symbol(msg) 15 | } else { 16 | zbg_swap::extract_symbol(market_type, msg) 17 | } 18 | } 19 | 20 | pub(crate) fn extract_timestamp( 21 | market_type: MarketType, 22 | msg: &str, 23 | ) -> Result, SimpleError> { 24 | if market_type == MarketType::Spot { 25 | zbg_spot::extract_timestamp(msg) 26 | } else { 27 | zbg_swap::extract_timestamp(market_type, msg) 28 | } 29 | } 30 | 31 | pub(crate) fn parse_trade( 32 | market_type: MarketType, 33 | msg: &str, 34 | ) -> Result, SimpleError> { 35 | if market_type == MarketType::Spot { 36 | zbg_spot::parse_trade(msg) 37 | } else { 38 | zbg_swap::parse_trade(market_type, msg) 39 | } 40 | } 41 | 42 | pub(crate) fn parse_l2( 43 | market_type: MarketType, 44 | msg: &str, 45 | ) -> Result, SimpleError> { 46 | if market_type == MarketType::Spot { 47 | zbg_spot::parse_l2(msg) 48 | } else { 49 | zbg_swap::parse_l2(market_type, msg) 50 | } 51 | } 52 | 53 | pub(crate) fn parse_candlestick( 54 | market_type: MarketType, 55 | msg: &str, 56 | ) -> Result, SimpleError> { 57 | if market_type == MarketType::Spot { 58 | zbg_spot::parse_candlestick(msg) 59 | } else { 60 | zbg_swap::parse_candlestick(market_type, msg) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /crypto-msg-parser/tests/bitz.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | use crypto_market_type::MarketType; 4 | use crypto_message::TradeSide; 5 | use crypto_msg_parser::{extract_symbol, extract_timestamp, parse_l2, parse_trade}; 6 | use crypto_msg_type::MessageType; 7 | 8 | #[test] 9 | #[ignore = "bitz.com has shutdown since October 2021"] 10 | fn trade() { 11 | let raw_msg = r#"{"msgId":0,"params":{"symbol":"btc_usdt"},"action":"Pushdata.order","data":[{"id":"1616486110508","t":"15:55:10","T":1616486110,"p":"53874.97","n":"0.1310","s":"sell"},{"id":"1616486110006","t":"15:55:10","T":1616486110,"p":"53875.82","n":"0.1144","s":"buy"}],"time":1616486110921,"source":"sub-api"}"#; 12 | let trades = &parse_trade("bitz", MarketType::Spot, raw_msg).unwrap(); 13 | 14 | assert_eq!(trades.len(), 2); 15 | 16 | for trade in trades.iter() { 17 | crate::utils::check_trade_fields( 18 | "bitz", 19 | MarketType::Spot, 20 | "BTC/USDT".to_string(), 21 | extract_symbol("bitz", MarketType::Spot, raw_msg).unwrap(), 22 | trade, 23 | raw_msg, 24 | ); 25 | } 26 | assert_eq!( 27 | 1616486110921, 28 | extract_timestamp("bitz", MarketType::Spot, raw_msg).unwrap().unwrap() 29 | ); 30 | 31 | assert_eq!(trades[0].side, TradeSide::Sell); 32 | assert_eq!(trades[0].quantity_base, 0.1310); 33 | 34 | assert_eq!(trades[1].side, TradeSide::Buy); 35 | assert_eq!(trades[1].quantity_base, 0.1144); 36 | } 37 | 38 | #[test] 39 | #[ignore = "bitz.com has shutdown since October 2021"] 40 | fn l2_orderbook_update() { 41 | let raw_msg = r#"{"msgId":0,"params":{"symbol":"btc_usdt"},"action":"Pushdata.depth","data":{"asks":[["37520.67","0.8396","31502.3545"]],"bids":[["37328.48","0.0050","186.6424"],["37322.18","0.2462","9188.7207"]],"depthSerialNumber":329},"time":1622527417489,"source":"sub-api"}"#; 42 | let orderbook = &parse_l2("bitz", MarketType::Spot, raw_msg, None).unwrap()[0]; 43 | 44 | assert_eq!(orderbook.asks.len(), 1); 45 | assert_eq!(orderbook.bids.len(), 2); 46 | assert!(!orderbook.snapshot); 47 | 48 | crate::utils::check_orderbook_fields( 49 | "bitz", 50 | MarketType::Spot, 51 | MessageType::L2Event, 52 | "BTC/USDT".to_string(), 53 | extract_symbol("bitz", MarketType::Spot, raw_msg).unwrap(), 54 | orderbook, 55 | raw_msg, 56 | ); 57 | assert_eq!( 58 | 1622527417489, 59 | extract_timestamp("bitz", MarketType::Spot, raw_msg).unwrap().unwrap() 60 | ); 61 | 62 | assert_eq!(orderbook.timestamp, 1622527417489); 63 | 64 | assert_eq!(orderbook.asks[0].price, 37520.67); 65 | assert_eq!(orderbook.asks[0].quantity_base, 0.8396); 66 | assert_eq!(orderbook.asks[0].quantity_quote, 31502.3545); 67 | 68 | assert_eq!(orderbook.bids[0].price, 37328.48); 69 | assert_eq!(orderbook.bids[0].quantity_base, 0.0050); 70 | assert_eq!(orderbook.bids[0].quantity_quote, 186.6424); 71 | 72 | assert_eq!(orderbook.bids[1].price, 37322.18); 73 | assert_eq!(orderbook.bids[1].quantity_base, 0.2462); 74 | assert_eq!(orderbook.bids[1].quantity_quote, 9188.7207); 75 | } 76 | -------------------------------------------------------------------------------- /crypto-msg-parser/tests/coinbase_pro.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | use chrono::prelude::*; 4 | use crypto_market_type::MarketType; 5 | use crypto_message::TradeSide; 6 | use crypto_msg_parser::{extract_symbol, extract_timestamp, parse_l2, parse_trade}; 7 | 8 | use crypto_msg_type::MessageType; 9 | 10 | const EXCHANGE_NAME: &str = "coinbase_pro"; 11 | 12 | #[test] 13 | fn trade() { 14 | let raw_msg = r#"{"type":"last_match","trade_id":147587438,"maker_order_id":"3dbaddb1-3dcf-4511-b81c-89450a56deb4","taker_order_id":"421f3aaa-dfdd-4192-805a-bb73462ea6db","side":"sell","size":"0.00031874","price":"57786.82","product_id":"BTC-USD","sequence":22962703070,"time":"2021-03-21T03:47:27.112041Z"}"#; 15 | let trade = &parse_trade(EXCHANGE_NAME, MarketType::Spot, raw_msg).unwrap()[0]; 16 | 17 | crate::utils::check_trade_fields( 18 | EXCHANGE_NAME, 19 | MarketType::Spot, 20 | "BTC/USD".to_string(), 21 | extract_symbol(EXCHANGE_NAME, MarketType::Spot, raw_msg).unwrap(), 22 | trade, 23 | raw_msg, 24 | ); 25 | assert_eq!( 26 | 1616298447112, 27 | extract_timestamp(EXCHANGE_NAME, MarketType::Spot, raw_msg).unwrap().unwrap() 28 | ); 29 | 30 | assert_eq!(trade.quantity_base, 0.00031874); 31 | assert_eq!(trade.side, TradeSide::Sell); 32 | } 33 | 34 | #[test] 35 | fn l2_orderbook_snapshot() { 36 | let raw_msg = r#"{"type":"snapshot","product_id":"BTC-USD","asks":[["37212.77","0.05724592"],["37215.39","0.00900000"],["37215.69","0.09654865"]],"bids":[["37209.96","0.04016376"],["37209.32","0.00192256"],["37209.16","0.01130000"]]}"#; 37 | let received_at = Utc::now().timestamp_millis(); 38 | let orderbook = 39 | &parse_l2(EXCHANGE_NAME, MarketType::Spot, raw_msg, Some(received_at)).unwrap()[0]; 40 | 41 | assert_eq!(orderbook.asks.len(), 3); 42 | assert_eq!(orderbook.bids.len(), 3); 43 | assert!(orderbook.snapshot); 44 | 45 | crate::utils::check_orderbook_fields( 46 | EXCHANGE_NAME, 47 | MarketType::Spot, 48 | MessageType::L2Event, 49 | "BTC/USD".to_string(), 50 | extract_symbol(EXCHANGE_NAME, MarketType::Spot, raw_msg).unwrap(), 51 | orderbook, 52 | raw_msg, 53 | ); 54 | assert_eq!(None, extract_timestamp(EXCHANGE_NAME, MarketType::Spot, raw_msg).unwrap()); 55 | assert_eq!(received_at, orderbook.timestamp); 56 | 57 | assert_eq!(orderbook.bids[0].price, 37209.96); 58 | assert_eq!(orderbook.bids[0].quantity_base, 0.04016376); 59 | assert_eq!(orderbook.bids[0].quantity_quote, 37209.96 * 0.04016376); 60 | 61 | assert_eq!(orderbook.bids[2].price, 37209.16); 62 | assert_eq!(orderbook.bids[2].quantity_base, 0.0113); 63 | assert_eq!(orderbook.bids[2].quantity_quote, 37209.16 * 0.0113); 64 | 65 | assert_eq!(orderbook.asks[0].price, 37212.77); 66 | assert_eq!(orderbook.asks[0].quantity_base, 0.05724592); 67 | assert_eq!(orderbook.asks[0].quantity_quote, 37212.77 * 0.05724592); 68 | 69 | assert_eq!(orderbook.asks[2].price, 37215.69); 70 | assert_eq!(orderbook.asks[2].quantity_base, 0.09654865); 71 | assert_eq!(orderbook.asks[2].quantity_quote, 37215.69 * 0.09654865); 72 | } 73 | 74 | #[test] 75 | fn l2_orderbook_update() { 76 | let raw_msg = r#"{"type":"l2update","product_id":"BTC-USD","changes":[["buy","37378.26","0.02460000"]],"time":"2021-06-02T09:02:09.048568Z"}"#; 77 | let orderbook = &parse_l2(EXCHANGE_NAME, MarketType::Spot, raw_msg, None).unwrap()[0]; 78 | 79 | assert_eq!(orderbook.asks.len(), 0); 80 | assert_eq!(orderbook.bids.len(), 1); 81 | assert!(!orderbook.snapshot); 82 | 83 | crate::utils::check_orderbook_fields( 84 | EXCHANGE_NAME, 85 | MarketType::Spot, 86 | MessageType::L2Event, 87 | "BTC/USD".to_string(), 88 | extract_symbol(EXCHANGE_NAME, MarketType::Spot, raw_msg).unwrap(), 89 | orderbook, 90 | raw_msg, 91 | ); 92 | assert_eq!( 93 | 1622624529048, 94 | extract_timestamp(EXCHANGE_NAME, MarketType::Spot, raw_msg).unwrap().unwrap() 95 | ); 96 | 97 | assert_eq!(orderbook.timestamp, 1622624529048); 98 | 99 | assert_eq!(orderbook.bids[0].price, 37378.26); 100 | assert_eq!(orderbook.bids[0].quantity_base, 0.0246); 101 | assert_eq!(orderbook.bids[0].quantity_quote, 37378.26 * 0.0246); 102 | } 103 | 104 | #[test] 105 | fn l3_event() { 106 | let raw_msg = r#"{"price":"31572.35","order_id":"5816ff12-61fc-4ab0-877a-fdf88544a4ee","remaining_size":"0.23","type":"open","side":"sell","product_id":"BTC-USD","time":"2022-06-01T08:32:21.469151Z","sequence":38292760991}"#; 107 | 108 | assert_eq!( 109 | 1654072341469, 110 | extract_timestamp(EXCHANGE_NAME, MarketType::Spot, raw_msg).unwrap().unwrap() 111 | ); 112 | assert_eq!("BTC-USD", extract_symbol(EXCHANGE_NAME, MarketType::Spot, raw_msg).unwrap()); 113 | } 114 | 115 | #[test] 116 | fn ticker() { 117 | let raw_msg = r#"{"type":"ticker","sequence":38332655422,"product_id":"BTC-USD","price":"29940.91","open_24h":"31677.61","volume_24h":"27783.70216674","low_24h":"29308.01","high_24h":"31888","volume_30d":"778633.19135445","best_bid":"29940.90","best_ask":"29940.91","side":"buy","time":"2022-06-02T09:20:54.127011Z","trade_id":347875517,"last_size":"0.00061522"}"#; 118 | 119 | assert_eq!( 120 | 1654161654127, 121 | extract_timestamp(EXCHANGE_NAME, MarketType::Spot, raw_msg).unwrap().unwrap() 122 | ); 123 | assert_eq!("BTC-USD", extract_symbol(EXCHANGE_NAME, MarketType::Spot, raw_msg).unwrap()); 124 | } 125 | 126 | #[test] 127 | fn l2_snapshot() { 128 | let raw_msg = r#"{"bids": [["0.1135", "35", 1], ["0.1134", "20606.7", 5], ["0.1133", "41561.8", 8], ["0.1132", "51132.8", 4], ["0.1131", "745", 2]], "asks": [["0.1137", "10113.4", 4], ["0.1138", "49781.3", 6], ["0.1139", "34339.9", 6], ["0.114", "34409.1", 4], ["0.1141", "4126.6", 2]], "sequence": 406959136, "auction_mode": false, "auction": null}"#; 129 | 130 | assert_eq!("NONE", extract_symbol(EXCHANGE_NAME, MarketType::Spot, raw_msg).unwrap()); 131 | 132 | assert_eq!(None, extract_timestamp(EXCHANGE_NAME, MarketType::Spot, raw_msg).unwrap()); 133 | } 134 | -------------------------------------------------------------------------------- /crypto-msg-parser/tests/utils.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | use crypto_market_type::MarketType; 3 | use crypto_msg_type::MessageType; 4 | 5 | use crypto_message::{FundingRateMsg, OrderBookMsg, TradeMsg}; 6 | use crypto_msg_parser::{get_msg_type, round}; 7 | 8 | pub fn check_trade_fields( 9 | exchange: &str, 10 | market_type: MarketType, 11 | pair: String, 12 | symbol: String, 13 | trade: &TradeMsg, 14 | raw_msg: &str, 15 | ) { 16 | assert_eq!(trade.exchange, exchange); 17 | assert_eq!(trade.market_type, market_type); 18 | assert_eq!(trade.pair, pair); 19 | assert_eq!(trade.symbol, symbol); 20 | assert_eq!(trade.msg_type, MessageType::Trade); 21 | if ["binance", "bitget", "bitmex", "bybit", "deribit", "ftx", "huobi", "okex"] 22 | .contains(&exchange) 23 | { 24 | assert_eq!(MessageType::Trade, get_msg_type(exchange, raw_msg)); 25 | } 26 | assert!(trade.price > 0.0); 27 | assert!(trade.quantity_base > 0.0); 28 | assert!(trade.quantity_quote > 0.0); 29 | if exchange != "bitmex" { 30 | assert_eq!(round(trade.quantity_quote), round(trade.price * trade.quantity_base)); 31 | } 32 | assert!(!trade.trade_id.is_empty()); 33 | assert_eq!(trade.timestamp.to_string().len(), 13); 34 | } 35 | 36 | pub fn check_orderbook_fields( 37 | exchange: &str, 38 | market_type: MarketType, 39 | msg_type: MessageType, 40 | pair: String, 41 | symbol: String, 42 | orderbook: &OrderBookMsg, 43 | raw_msg: &str, 44 | ) { 45 | assert_eq!(orderbook.exchange, exchange); 46 | assert_eq!(orderbook.market_type, market_type); 47 | assert_eq!(orderbook.msg_type, msg_type); 48 | assert_eq!(orderbook.pair, pair); 49 | assert_eq!(orderbook.symbol, symbol); 50 | if ["binance", "bitget", "bitmex", "bybit", "deribit", "ftx", "huobi", "okex"] 51 | .contains(&exchange) 52 | { 53 | assert_eq!(msg_type, get_msg_type(exchange, raw_msg)); 54 | } 55 | assert_eq!(orderbook.timestamp.to_string().len(), 13); 56 | 57 | for order in orderbook.asks.iter() { 58 | assert!(order.price > 0.0); 59 | assert!(order.quantity_base >= 0.0); 60 | assert!(order.quantity_quote >= 0.0); 61 | 62 | if let Some(quantity_contract) = order.quantity_contract { 63 | assert!(quantity_contract >= 0.0); 64 | } 65 | } 66 | } 67 | 68 | pub fn check_funding_rate_fields( 69 | exchange: &str, 70 | market_type: MarketType, 71 | funding_rate: &FundingRateMsg, 72 | raw_msg: &str, 73 | ) { 74 | assert_eq!(funding_rate.exchange, exchange); 75 | assert_eq!(funding_rate.market_type, market_type); 76 | // assert_eq!(funding_rate.pair, pair); 77 | assert_eq!(funding_rate.msg_type, MessageType::FundingRate); 78 | assert_eq!(MessageType::FundingRate, get_msg_type(exchange, raw_msg)); 79 | assert!(funding_rate.funding_rate > -1.0); 80 | assert!(funding_rate.funding_rate < 1.0); 81 | if exchange == "bitmex" { 82 | assert_eq!(funding_rate.funding_time % (4 * 3600000), 0); 83 | } else if exchange == "bitget" { 84 | assert_eq!(funding_rate.funding_time % 3600000, 0); 85 | } else { 86 | assert_eq!(funding_rate.funding_time % (8 * 3600000), 0); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /crypto-pair/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crypto-pair" 3 | version = "2.3.20" 4 | authors = ["soulmachine "] 5 | edition = "2021" 6 | description = "Parse exchange-specific symbols to unified format" 7 | license = "Apache-2.0" 8 | repository = "https://github.com/crypto-crawler/crypto-msg-parser/tree/main/crypto-pair" 9 | 10 | [dependencies] 11 | crypto-market-type = "1.1.6" 12 | once_cell = "1.19.0" 13 | reqwest = { version = "0.11.25", features = ["blocking", "gzip"] } 14 | serde = { version = "1.0.197", features = ["derive"] } 15 | serde_json = "1.0.114" 16 | -------------------------------------------------------------------------------- /crypto-pair/README.md: -------------------------------------------------------------------------------- 1 | # crypto-pair 2 | 3 | [![](https://img.shields.io/crates/v/crypto-pair.svg)](https://crates.io/crates/crypto-pair) 4 | [![](https://docs.rs/crypto-pair/badge.svg)](https://docs.rs/crypto-pair) 5 | ========== 6 | 7 | Parse exchange-specific symbols to unified format. 8 | 9 | ## Usage 10 | 11 | ```rust 12 | use crypto_pair::normalize_pair; 13 | 14 | fn main() { 15 | assert_eq!(Some("BTC/USD".to_string()), normalize_pair("XBTH21", "BitMEX")); 16 | } 17 | ``` 18 | -------------------------------------------------------------------------------- /crypto-pair/src/exchanges/binance.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{BTreeSet, HashSet}; 2 | 3 | use super::utils::{http_get, normalize_pair_with_quotes}; 4 | 5 | use crypto_market_type::MarketType; 6 | use once_cell::sync::Lazy; 7 | use serde::{Deserialize, Serialize}; 8 | use serde_json::Value; 9 | use std::collections::HashMap; 10 | 11 | static SPOT_QUOTES: Lazy> = Lazy::new(|| { 12 | // offline data, in case the network is down 13 | let mut set: HashSet = vec![ 14 | "ARS", "AUD", "BIDR", "BKRW", "BNB", "BRL", "BTC", "BUSD", "BVND", "DAI", "DOGE", "DOT", 15 | "ETH", "EUR", "GBP", "GYEN", "IDRT", "NGN", "PAX", "PLN", "RON", "RUB", "TRX", "TRY", 16 | "TUSD", "UAH", "USDC", "USDP", "USDS", "USDT", "UST", "VAI", "XRP", "ZAR", 17 | ] 18 | .into_iter() 19 | .map(|x| x.to_string()) 20 | .collect(); 21 | 22 | let from_online = fetch_spot_quotes(); 23 | set.extend(from_online); 24 | 25 | set 26 | }); 27 | 28 | #[derive(Serialize, Deserialize)] 29 | struct BinanceResponse { 30 | symbols: Vec, 31 | } 32 | 33 | #[derive(Serialize, Deserialize)] 34 | #[allow(non_snake_case)] 35 | struct SpotMarket { 36 | quoteAsset: String, 37 | #[serde(flatten)] 38 | extra: HashMap, 39 | } 40 | 41 | // see 42 | fn fetch_spot_quotes() -> BTreeSet { 43 | if let Ok(txt) = http_get("https://api.binance.com/api/v3/exchangeInfo") { 44 | let resp = serde_json::from_str::(&txt).unwrap(); 45 | resp.symbols.into_iter().map(|m| m.quoteAsset).collect::>() 46 | } else { 47 | BTreeSet::new() 48 | } 49 | } 50 | 51 | pub(crate) fn normalize_pair(symbol: &str) -> Option { 52 | if let Some(base) = symbol.strip_suffix("USD_PERP") { 53 | // inverse swap 54 | Some(format!("{base}/USD")) 55 | } else if symbol.ends_with("-P") || symbol.ends_with("-C") { 56 | // option 57 | let pos = symbol.find('-').unwrap(); 58 | let base = &symbol[..pos]; 59 | Some(format!("{base}/USDT")) 60 | } else if symbol.len() > 7 && (symbol[(symbol.len() - 6)..]).parse::().is_ok() { 61 | // linear and inverse future 62 | let remove_date = &symbol[..symbol.len() - 7]; 63 | if remove_date.ends_with("USDT") { 64 | let base = remove_date.strip_suffix("USDT").unwrap(); 65 | Some(format!("{base}/USDT")) 66 | } else if remove_date.ends_with("USD") { 67 | let base = remove_date.strip_suffix("USD").unwrap(); 68 | Some(format!("{base}/USD")) 69 | } else { 70 | panic!("Unsupported symbol {symbol}"); 71 | } 72 | } else { 73 | let quotes = &(*SPOT_QUOTES); 74 | normalize_pair_with_quotes(symbol, quotes) 75 | } 76 | } 77 | 78 | pub(crate) fn get_market_type(symbol: &str, is_spot: Option) -> MarketType { 79 | if symbol.ends_with("USD_PERP") { 80 | MarketType::InverseSwap 81 | } else if symbol.ends_with("-P") || symbol.ends_with("-C") { 82 | MarketType::EuropeanOption 83 | } else if symbol.len() > 7 && (symbol[(symbol.len() - 6)..]).parse::().is_ok() { 84 | // linear and inverse future 85 | let remove_date = &symbol[..symbol.len() - 7]; 86 | if remove_date.ends_with("USDT") { 87 | MarketType::LinearFuture 88 | } else if remove_date.ends_with("USD") { 89 | MarketType::InverseFuture 90 | } else { 91 | MarketType::Unknown 92 | } 93 | } else if let Some(is_spot) = is_spot { 94 | if is_spot { MarketType::Spot } else { MarketType::LinearSwap } 95 | } else { 96 | MarketType::LinearSwap 97 | } 98 | } 99 | 100 | #[cfg(test)] 101 | mod tests { 102 | use super::fetch_spot_quotes; 103 | 104 | #[test] 105 | fn spot_quotes() { 106 | let mut map = fetch_spot_quotes(); 107 | for coin in super::SPOT_QUOTES.iter() { 108 | map.insert(coin.clone()); 109 | } 110 | for quote in map.iter() { 111 | println!("\"{quote}\","); 112 | } 113 | } 114 | 115 | #[test] 116 | fn normalize_pair() { 117 | assert_eq!("BDOT/DOT", super::normalize_pair("BDOTDOT").unwrap()); 118 | assert_eq!("ETH/PLN", super::normalize_pair("ETHPLN").unwrap()); 119 | assert_eq!("USDT/ARS", super::normalize_pair("USDTARS").unwrap()); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /crypto-pair/src/exchanges/bitfinex.rs: -------------------------------------------------------------------------------- 1 | use super::utils::http_get; 2 | use crypto_market_type::MarketType; 3 | use once_cell::sync::Lazy; 4 | use std::collections::{BTreeMap, HashMap}; 5 | 6 | static BITFINEX_MAPPING: Lazy> = Lazy::new(|| { 7 | // offline data, in case the network is down 8 | let mut set: HashMap = vec![ 9 | ("AAA", "TESTAAA"), 10 | ("AIX", "AI"), 11 | ("ALG", "ALGO"), 12 | ("AMP", "AMPL"), 13 | ("AMPF0", "AMPLF0"), 14 | ("ATO", "ATOM"), 15 | ("B21X", "B21"), 16 | ("BBB", "TESTBBB"), 17 | ("BCHABC", "XEC"), 18 | ("BTCF0", "BTC"), 19 | ("CNHT", "CNHt"), 20 | ("DAT", "DATA"), 21 | ("DOG", "MDOGE"), 22 | ("DSH", "DASH"), 23 | ("EDO", "PNT"), 24 | ("ETH2P", "ETH2Pending"), 25 | ("ETH2R", "ETH2Rewards"), 26 | ("ETH2X", "ETH2"), 27 | ("EUS", "EURS"), 28 | ("EUT", "EURt"), 29 | ("EUTF0", "EURt"), 30 | ("FBT", "FB"), 31 | ("GNT", "GLM"), 32 | ("HIX", "HI"), 33 | ("IDX", "ID"), 34 | ("IOT", "IOTA"), 35 | ("LBT", "LBTC"), 36 | ("LES", "LEO-EOS"), 37 | ("LET", "LEO-ERC20"), 38 | ("LNX", "LN-BTC"), 39 | ("MNA", "MANA"), 40 | ("MXNT", "MXNt"), 41 | ("OMN", "OMNI"), 42 | ("PAS", "PASS"), 43 | ("PBTCEOS", "pBTC-EOS"), 44 | ("PBTCETH", "PBTC-ETH"), 45 | ("PETHEOS", "pETH-EOS"), 46 | ("PLTCEOS", "PLTC-EOS"), 47 | ("PLTCETH", "PLTC-ETH"), 48 | ("QSH", "QASH"), 49 | ("QTM", "QTUM"), 50 | ("RBT", "RBTC"), 51 | ("REP", "REP2"), 52 | ("SNG", "SNGLS"), 53 | ("STJ", "STORJ"), 54 | ("SXX", "SX"), 55 | ("TSD", "TUSD"), 56 | ("UDC", "USDC"), 57 | ("UST", "USDt"), 58 | ("USTF0", "USDt"), 59 | ("VSY", "VSYS"), 60 | ("WBT", "WBTC"), 61 | ("XAUT", "XAUt"), 62 | ("XCH", "XCHF"), 63 | ("YGG", "MCS"), 64 | ] 65 | .into_iter() 66 | .map(|x| (x.0.to_string(), x.1.to_string())) 67 | .collect(); 68 | 69 | let from_online = fetch_currency_mapping(); 70 | set.extend(from_online); 71 | 72 | set 73 | }); 74 | 75 | // see 76 | fn fetch_currency_mapping() -> BTreeMap { 77 | let mut mapping = BTreeMap::::new(); 78 | 79 | if let Ok(txt) = http_get("https://api-pub.bitfinex.com/v2/conf/pub:map:currency:sym") { 80 | let arr = serde_json::from_str::>>>(&txt).unwrap(); 81 | assert!(arr.len() == 1); 82 | 83 | for v in arr[0].iter() { 84 | assert!(v.len() == 2); 85 | mapping.insert(v[0].clone(), v[1].clone()); 86 | } 87 | } 88 | 89 | mapping 90 | } 91 | 92 | pub(crate) fn normalize_currency(mut currency: &str) -> String { 93 | assert!(!currency.trim().is_empty(), "The currency must NOT be empty"); 94 | 95 | if currency.ends_with("F0") { 96 | currency = ¤cy[..(currency.len() - 2)]; // Futures only 97 | } 98 | if BITFINEX_MAPPING.contains_key(currency) { 99 | currency = BITFINEX_MAPPING[currency].as_str(); 100 | } 101 | 102 | currency.to_uppercase() 103 | } 104 | 105 | pub(crate) fn normalize_pair(mut symbol: &str) -> Option { 106 | if symbol.starts_with('t') { 107 | symbol = &symbol[1..]; // e.g., tBTCUSD, remove t 108 | }; 109 | 110 | let (base, quote) = if symbol.contains(':') { 111 | let v: Vec<&str> = symbol.split(':').collect(); 112 | (v[0].to_string(), v[1].to_string()) 113 | } else { 114 | (symbol[..(symbol.len() - 3)].to_string(), symbol[(symbol.len() - 3)..].to_string()) 115 | }; 116 | 117 | Some(format!("{}/{}", normalize_currency(&base), normalize_currency("e))) 118 | } 119 | 120 | pub(crate) fn get_market_type(symbol: &str) -> MarketType { 121 | if symbol.ends_with("F0") || symbol.ends_with("f0") { 122 | MarketType::LinearSwap 123 | } else { 124 | MarketType::Spot 125 | } 126 | } 127 | 128 | #[cfg(test)] 129 | mod tests { 130 | use super::fetch_currency_mapping; 131 | 132 | #[test] 133 | fn test_currency_mapping() { 134 | let map = fetch_currency_mapping(); 135 | for (name, new_name) in map { 136 | println!("(\"{name}\", \"{new_name}\"),"); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /crypto-pair/src/exchanges/bitget.rs: -------------------------------------------------------------------------------- 1 | use crypto_market_type::MarketType; 2 | 3 | pub(crate) fn normalize_pair(symbol: &str) -> Option { 4 | if symbol.ends_with("_SPBL") 5 | || symbol.contains("_UMCBL") 6 | || symbol.contains("_CMCBL") 7 | || symbol.contains("_DMCBL") 8 | { 9 | let pos = symbol.find('_').unwrap(); 10 | let pair = &symbol[..pos]; 11 | if symbol == "SBTCSUSDT_SPBL" { 12 | Some("SBTC/SUSDT".to_string()) 13 | } else if symbol.ends_with("PERP_CMCBL") { 14 | Some(format!("{}/USDC", symbol.strip_suffix("PERP_CMCBL").unwrap())) 15 | } else if pair.ends_with("USDT") { 16 | Some(format!("{}/USDT", pair.strip_suffix("USDT").unwrap())) 17 | } else if pair.ends_with("USD") { 18 | Some(format!("{}/USD", pair.strip_suffix("USD").unwrap())) 19 | } else if pair.ends_with("ETH") { 20 | Some(format!("{}/ETH", pair.strip_suffix("ETH").unwrap())) 21 | } else if pair.ends_with("BTC") { 22 | Some(format!("{}/BTC", pair.strip_suffix("BTC").unwrap())) 23 | } else { 24 | panic!("Failed to parse {symbol}"); 25 | } 26 | } else { 27 | #[allow(clippy::collapsible_else_if)] 28 | if symbol.starts_with("cmt_") { 29 | // linear swap 30 | assert!(symbol.ends_with("usdt")); 31 | let base = &symbol[4..symbol.len() - 4]; 32 | Some(format!("{base}/usdt").to_uppercase()) 33 | } else if symbol.contains('_') { 34 | // spot 35 | Some(symbol.replace('_', "/").to_uppercase()) 36 | } else if symbol.ends_with("usd") { 37 | // inverse swap 38 | let base = symbol.strip_suffix("usd").unwrap(); 39 | Some(format!("{base}/usd").to_uppercase()) 40 | } else { 41 | None 42 | } 43 | } 44 | } 45 | 46 | pub(crate) fn get_market_type(symbol: &str) -> MarketType { 47 | if symbol.ends_with("_SPBL") 48 | || symbol.contains("_UMCBL") 49 | || symbol.contains("_CMCBL") 50 | || symbol.contains("_DMCBL") 51 | { 52 | // bitget v3 API 53 | if symbol.ends_with("_SPBL") { 54 | MarketType::Spot 55 | } else if symbol.ends_with("_UMCBL") || symbol.ends_with("_CMCBL") { 56 | MarketType::LinearSwap 57 | } else if symbol.ends_with("_DMCBL") { 58 | MarketType::InverseSwap 59 | } else if symbol.contains("_UMCBL_") | symbol.contains("_CMCBL_") { 60 | MarketType::LinearFuture 61 | } else if symbol.contains("_DMCBL_") { 62 | MarketType::InverseFuture 63 | } else { 64 | MarketType::Unknown 65 | } 66 | } else { 67 | // deprecated bitget v1 API 68 | if symbol.starts_with("cmt_") { 69 | MarketType::LinearSwap 70 | } else if symbol.contains('_') { 71 | MarketType::Spot 72 | } else if symbol.ends_with("usd") { 73 | MarketType::InverseSwap 74 | } else { 75 | MarketType::Unknown 76 | } 77 | } 78 | } 79 | 80 | #[cfg(test)] 81 | mod tests { 82 | use crypto_market_type::MarketType; 83 | 84 | #[test] 85 | fn test_get_market_type() { 86 | assert_eq!(MarketType::InverseFuture, super::get_market_type("BTCUSD_DMCBL_221230")); 87 | assert_eq!(MarketType::LinearSwap, super::get_market_type("BTCPERP_CMCBL")); 88 | } 89 | 90 | #[test] 91 | fn test_normalize_pair() { 92 | assert_eq!("SBTC/SUSDT", super::normalize_pair("SBTCSUSDT_SPBL").unwrap()); 93 | assert_eq!("EOS/USDT", super::normalize_pair("EOSUSDT_SPBL").unwrap()); 94 | assert_eq!("BTC/USD", super::normalize_pair("BTCUSD_DMCBL_221230").unwrap()); 95 | assert_eq!("BTC/USDC", super::normalize_pair("BTCPERP_CMCBL").unwrap()); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /crypto-pair/src/exchanges/bitstamp.rs: -------------------------------------------------------------------------------- 1 | pub(crate) fn normalize_pair(symbol: &str) -> Option { 2 | let (base, quote) = if symbol.ends_with("usdc") || symbol.ends_with("usdt") { 3 | (symbol[0..(symbol.len() - 4)].to_string(), symbol[(symbol.len() - 4)..].to_string()) 4 | } else { 5 | (symbol[..(symbol.len() - 3)].to_string(), symbol[(symbol.len() - 3)..].to_string()) 6 | }; 7 | 8 | Some(format!("{base}/{quote}").to_uppercase()) 9 | } 10 | -------------------------------------------------------------------------------- /crypto-pair/src/exchanges/bybit.rs: -------------------------------------------------------------------------------- 1 | use crypto_market_type::MarketType; 2 | 3 | pub(crate) fn normalize_pair(symbol: &str) -> Option { 4 | let (base, quote) = if symbol.ends_with("USDT") { 5 | // linear swap 6 | let base = symbol.strip_suffix("USDT").unwrap(); 7 | (base, "USDT") 8 | } else if symbol.ends_with("USD") { 9 | // inverse swap 10 | let base = symbol.strip_suffix("USD").unwrap(); 11 | (base, "USD") 12 | } else if symbol[symbol.len() - 2..].parse::().is_ok() { 13 | // inverse future 14 | let base = &symbol[..symbol.len() - 6]; 15 | (base, "USD") 16 | } else { 17 | panic!("Unknown symbol {symbol}"); 18 | }; 19 | Some(format!("{base}/{quote}")) 20 | } 21 | 22 | pub(crate) fn get_market_type(symbol: &str) -> MarketType { 23 | if symbol.ends_with("USDT") { 24 | MarketType::LinearSwap 25 | } else if symbol.ends_with("USD") { 26 | MarketType::InverseSwap 27 | } else if symbol[symbol.len() - 2..].parse::().is_ok() { 28 | MarketType::InverseFuture 29 | } else { 30 | MarketType::Unknown 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /crypto-pair/src/exchanges/deribit.rs: -------------------------------------------------------------------------------- 1 | use crypto_market_type::MarketType; 2 | 3 | pub(crate) fn normalize_pair(symbol: &str) -> Option { 4 | if symbol.ends_with("-PERPETUAL") { 5 | // inverse_swap 6 | let base = symbol.strip_suffix("-PERPETUAL").unwrap(); 7 | Some(format!("{base}/USD")) 8 | } else if symbol.len() > 7 && symbol[(symbol.len() - 2)..].parse::().is_ok() { 9 | // inverse_future 10 | let pos = symbol.find('-').unwrap(); 11 | let base = &symbol[..pos]; 12 | Some(format!("{base}/USD")) 13 | } else if symbol.ends_with("-P") || symbol.ends_with("-C") { 14 | // option 15 | let pos = symbol.find('-').unwrap(); 16 | let base = &symbol[..pos]; 17 | Some(format!("{base}/{base}")) 18 | } else { 19 | None 20 | } 21 | } 22 | 23 | pub(crate) fn get_market_type(symbol: &str) -> MarketType { 24 | if symbol.ends_with("-PERPETUAL") { 25 | MarketType::InverseSwap 26 | } else if symbol.len() > 7 && symbol[(symbol.len() - 2)..].parse::().is_ok() { 27 | MarketType::InverseFuture 28 | } else if symbol.ends_with("-P") || symbol.ends_with("-C") { 29 | MarketType::EuropeanOption 30 | } else { 31 | MarketType::Unknown 32 | } 33 | } 34 | 35 | #[cfg(test)] 36 | mod tests { 37 | use crypto_market_type::MarketType; 38 | 39 | #[test] 40 | fn test_get_market_type() { 41 | assert_eq!(MarketType::InverseFuture, super::get_market_type("BTC-30DEC22")); 42 | assert_eq!(MarketType::InverseSwap, super::get_market_type("BTC-PERPETUAL")); 43 | assert_eq!(MarketType::EuropeanOption, super::get_market_type("BTC-17JUN22-21000-P")); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /crypto-pair/src/exchanges/dydx.rs: -------------------------------------------------------------------------------- 1 | pub(crate) fn normalize_pair(symbol: &str) -> Option { 2 | if symbol.contains('-') { 3 | let result = str::replace(symbol, "-", "/"); 4 | Some(result) 5 | } else { 6 | None 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /crypto-pair/src/exchanges/ftx.rs: -------------------------------------------------------------------------------- 1 | use crypto_market_type::MarketType; 2 | 3 | pub(crate) fn normalize_pair(symbol: &str) -> Option { 4 | if symbol.ends_with("-PERP") { 5 | // linear swap 6 | let base = symbol.strip_suffix("-PERP").unwrap(); 7 | Some(format!("{base}/USD")) 8 | } else if symbol.contains("-MOVE-") { 9 | let v: Vec<&str> = symbol.split('-').collect(); 10 | Some(format!("{}/USD", v[0])) 11 | } else if symbol.contains("BVOL/") || symbol.contains('/') { 12 | // BVOL and Spot 13 | Some(symbol.to_string()) 14 | } else if let Some(pos) = symbol.rfind('-') { 15 | // linear future 16 | let base = &symbol[..pos]; 17 | Some(format!("{base}/USD")) 18 | } else { 19 | // prediction 20 | Some(format!("{symbol}/USD")) 21 | } 22 | } 23 | 24 | pub(crate) fn get_market_type(symbol: &str) -> MarketType { 25 | if symbol.ends_with("-PERP") { 26 | MarketType::LinearSwap 27 | } else if symbol.contains("-MOVE-") { 28 | MarketType::Move 29 | } else if symbol.contains("BVOL/") { 30 | MarketType::BVOL 31 | } else if symbol.contains('/') { 32 | MarketType::Spot 33 | } else if symbol.contains('-') { 34 | MarketType::LinearFuture 35 | } else { 36 | // prediction 37 | MarketType::Unknown 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /crypto-pair/src/exchanges/gate.rs: -------------------------------------------------------------------------------- 1 | use crypto_market_type::MarketType; 2 | 3 | pub(crate) fn get_market_type(symbol: &str, is_spot: Option) -> MarketType { 4 | if symbol.ends_with("_USD") { 5 | if let Some(is_spot) = is_spot { 6 | if is_spot { MarketType::Spot } else { MarketType::InverseSwap } 7 | } else { 8 | MarketType::InverseSwap 9 | } 10 | } else if symbol.ends_with("_USDT") { 11 | if let Some(is_spot) = is_spot { 12 | if is_spot { MarketType::Spot } else { MarketType::LinearSwap } 13 | } else { 14 | MarketType::LinearSwap 15 | } 16 | } else if symbol.len() > 8 && symbol[(symbol.len() - 8)..].parse::().is_ok() { 17 | if symbol.contains("_USD_") { 18 | MarketType::InverseFuture 19 | } else if symbol.contains("_USDT_") { 20 | MarketType::LinearFuture 21 | } else { 22 | MarketType::Unknown 23 | } 24 | } else if symbol.contains('_') { 25 | MarketType::Spot 26 | } else { 27 | MarketType::Unknown 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /crypto-pair/src/exchanges/huobi.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{BTreeSet, HashSet}; 2 | 3 | use super::utils::{http_get, normalize_pair_with_quotes}; 4 | use crypto_market_type::MarketType; 5 | use once_cell::sync::Lazy; 6 | 7 | use serde::{Deserialize, Serialize}; 8 | use serde_json::Value; 9 | use std::collections::HashMap; 10 | 11 | static SPOT_QUOTES: Lazy> = Lazy::new(|| { 12 | // offline data, in case the network is down 13 | let mut set: HashSet = vec![ 14 | "brl", "btc", "eth", "eur", "euroc", "gbp", "ht", "husd", "rub", "trx", "try", "tusd", 15 | "uah", "usdc", "usdd", "usdt", "ust", "ustc", 16 | ] 17 | .into_iter() 18 | .map(|x| x.to_string()) 19 | .collect(); 20 | 21 | let from_online = fetch_spot_quotes(); 22 | set.extend(from_online); 23 | 24 | set 25 | }); 26 | 27 | #[derive(Serialize, Deserialize)] 28 | #[serde(rename_all = "kebab-case")] 29 | struct SpotMarket { 30 | base_currency: String, 31 | quote_currency: String, 32 | symbol: String, 33 | #[serde(flatten)] 34 | extra: HashMap, 35 | } 36 | 37 | #[derive(Serialize, Deserialize)] 38 | struct Response { 39 | status: String, 40 | data: Vec, 41 | } 42 | 43 | // see 44 | fn fetch_spot_quotes() -> BTreeSet { 45 | if let Ok(txt) = http_get("https://api.huobi.pro/v1/common/symbols") { 46 | let resp = serde_json::from_str::>(&txt).unwrap(); 47 | resp.data.into_iter().map(|m| m.quote_currency).collect::>() 48 | } else { 49 | BTreeSet::new() 50 | } 51 | } 52 | 53 | pub(crate) fn normalize_pair(symbol: &str) -> Option { 54 | if symbol.ends_with("-USD") || symbol.ends_with("-USDT") { 55 | // inverse and linear swap 56 | Some(symbol.replace('-', "/")) 57 | } else if symbol.contains("-C-") || symbol.contains("-P-") { 58 | // option 59 | let (base, quote) = { 60 | let v: Vec<&str> = symbol.split('-').collect(); 61 | (v[0].to_uppercase(), v[1].to_uppercase()) 62 | }; 63 | Some(format!("{base}/{quote}")) 64 | } else if symbol.ends_with("_CW") 65 | || symbol.ends_with("_NW") 66 | || symbol.ends_with("_CQ") 67 | || symbol.ends_with("_NQ") 68 | { 69 | // inverse future 70 | let base = &symbol[..symbol.len() - 3]; 71 | Some(format!("{base}/USD")) 72 | } else { 73 | // spot 74 | let quotes = &(*SPOT_QUOTES); 75 | normalize_pair_with_quotes(symbol, quotes) 76 | } 77 | } 78 | 79 | pub(crate) fn get_market_type(symbol: &str) -> MarketType { 80 | if symbol.ends_with("-USD") { 81 | MarketType::InverseSwap 82 | } else if symbol.ends_with("-USDT") { 83 | MarketType::LinearSwap 84 | } else if symbol.contains("-C-") || symbol.contains("-P-") { 85 | MarketType::EuropeanOption 86 | } else if symbol.ends_with("_CW") 87 | || symbol.ends_with("_NW") 88 | || symbol.ends_with("_CQ") 89 | || symbol.ends_with("_NQ") 90 | { 91 | MarketType::InverseFuture 92 | } else { 93 | MarketType::Spot 94 | } 95 | } 96 | 97 | #[cfg(test)] 98 | mod tests { 99 | use super::fetch_spot_quotes; 100 | 101 | #[test] 102 | fn spot_quotes() { 103 | let mut set = fetch_spot_quotes(); 104 | for quote in super::SPOT_QUOTES.iter() { 105 | set.insert(quote.clone()); 106 | } 107 | for quote in set { 108 | println!("\"{quote}\","); 109 | } 110 | } 111 | 112 | #[test] 113 | fn test_normalize_pair() { 114 | assert_eq!("MIR/UST", super::normalize_pair("mirust").unwrap()); 115 | assert_eq!("BTT/TRX", super::normalize_pair("btttrx").unwrap()); 116 | assert_eq!("ETH/EUROC", super::normalize_pair("etheuroc").unwrap()); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /crypto-pair/src/exchanges/kraken.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{BTreeSet, HashMap, HashSet}; 2 | 3 | use super::utils::http_get; 4 | use crypto_market_type::MarketType; 5 | use once_cell::sync::Lazy; 6 | use serde::{Deserialize, Serialize}; 7 | use serde_json::Value; 8 | 9 | static SPOT_QUOTES: Lazy> = Lazy::new(|| { 10 | // offline data, in case the network is down 11 | let mut set: HashSet = vec![ 12 | "AUD", "CAD", "CHF", "DAI", "DOT", "ETH", "EUR", "GBP", "JPY", "PYUSD", "USD", "USDC", 13 | "USDT", "XBT", "XET", "XXB", "ZAU", "ZCA", "ZEU", "ZGB", "ZJP", "ZUS", 14 | ] 15 | .into_iter() 16 | .map(|x| x.to_string()) 17 | .collect(); 18 | 19 | let from_online = fetch_spot_quotes(); 20 | set.extend(from_online); 21 | 22 | set 23 | }); 24 | 25 | // see 26 | fn fetch_spot_quotes() -> BTreeSet { 27 | #[derive(Serialize, Deserialize)] 28 | struct Response { 29 | error: Vec, 30 | result: HashMap, 31 | } 32 | 33 | #[derive(Serialize, Deserialize)] 34 | #[allow(non_snake_case)] 35 | struct SpotMarket { 36 | altname: String, 37 | wsname: Option, 38 | aclass_base: String, 39 | base: String, 40 | aclass_quote: String, 41 | quote: String, 42 | #[serde(flatten)] 43 | extra: HashMap, 44 | } 45 | 46 | if let Ok(txt) = http_get("https://api.kraken.com/0/public/AssetPairs") { 47 | let resp = serde_json::from_str::(&txt).unwrap(); 48 | resp.result 49 | .into_values() 50 | .map(|m| m.quote) 51 | .map(|s| { 52 | if s.len() > 3 && (s.starts_with('X') || s.starts_with('Z')) { 53 | s[1..].to_string() 54 | } else { 55 | s 56 | } 57 | }) 58 | .collect::>() 59 | } else { 60 | BTreeSet::new() 61 | } 62 | } 63 | 64 | pub(crate) fn normalize_currency(currency: &str) -> String { 65 | let uppercase = currency.to_uppercase(); 66 | let mut currency = uppercase.as_str(); 67 | // https://support.kraken.com/hc/en-us/articles/360001185506-How-to-interpret-asset-codes 68 | if currency.len() > 3 && (currency.starts_with('X') || currency.starts_with('Z')) { 69 | currency = ¤cy[1..] 70 | } 71 | 72 | if currency == "XBT" { 73 | "BTC" 74 | } else if currency == "XDG" { 75 | "DOGE" 76 | } else { 77 | currency 78 | } 79 | .to_string() 80 | } 81 | 82 | pub(crate) fn normalize_pair(symbol: &str) -> Option { 83 | if symbol.contains('/') { 84 | // Spot 85 | let (base, quote) = { 86 | let v: Vec<&str> = symbol.split('/').collect(); 87 | (v[0].to_string(), v[1].to_string()) 88 | }; 89 | 90 | Some(format!("{}/{}", normalize_currency(&base), normalize_currency("e))) 91 | } else if symbol.starts_with("pi_") 92 | || symbol.starts_with("fi_") 93 | || symbol.starts_with("PI_") 94 | || symbol.starts_with("FI_") 95 | { 96 | let pos = if let Some(pos) = symbol.find("usd") { 97 | pos 98 | } else if let Some(pos) = symbol.find("USD") { 99 | pos 100 | } else { 101 | panic!("Can not find usd or USD in symbol: {symbol}"); 102 | }; 103 | let base = symbol[3..pos].to_uppercase(); 104 | Some(format!("{}/USD", normalize_currency(&base),)) 105 | } else if symbol.len() > 5 && SPOT_QUOTES.contains(&symbol[(symbol.len() - 5)..]) { 106 | let base = &symbol[..(symbol.len() - 5)]; 107 | let quote = &symbol[(symbol.len() - 5)..]; 108 | Some(format!("{}/{}", normalize_currency(base), normalize_currency(quote))) 109 | } else if symbol.len() > 4 && SPOT_QUOTES.contains(&symbol[(symbol.len() - 4)..]) { 110 | let base = &symbol[..(symbol.len() - 4)]; 111 | let quote = &symbol[(symbol.len() - 4)..]; 112 | Some(format!("{}/{}", normalize_currency(base), normalize_currency(quote))) 113 | } else if symbol.len() > 3 && SPOT_QUOTES.contains(&symbol[(symbol.len() - 3)..]) { 114 | let base = &symbol[..(symbol.len() - 3)]; 115 | let quote = &symbol[(symbol.len() - 3)..]; 116 | Some(format!("{}/{}", normalize_currency(base), normalize_currency(quote))) 117 | } else { 118 | None 119 | } 120 | } 121 | 122 | pub(crate) fn get_market_type(symbol: &str) -> MarketType { 123 | if symbol.starts_with("pi_") || symbol.starts_with("PI_") { 124 | MarketType::InverseSwap 125 | } else if symbol.starts_with("fi_") || symbol.starts_with("FI_") { 126 | MarketType::InverseFuture 127 | } else { 128 | MarketType::Spot 129 | } 130 | } 131 | 132 | #[cfg(test)] 133 | mod tests { 134 | use super::{fetch_spot_quotes, SPOT_QUOTES}; 135 | 136 | #[test] 137 | fn normalize_pair() { 138 | assert_eq!("BTC/PYUSD", super::normalize_pair("XBT/PYUSD").unwrap()); 139 | assert_eq!("BTC/PYUSD", super::normalize_pair("XBTPYUSD").unwrap()); 140 | } 141 | 142 | #[test] 143 | fn spot_quotes() { 144 | let map = fetch_spot_quotes(); 145 | for quote in map { 146 | if !SPOT_QUOTES.contains("e) { 147 | println!("\"{quote}\","); 148 | } 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /crypto-pair/src/exchanges/kucoin.rs: -------------------------------------------------------------------------------- 1 | use crypto_market_type::MarketType; 2 | 3 | pub(crate) fn normalize_currency(currency: &str) -> String { 4 | if currency == "XBT" { 5 | "BTC" 6 | } else if currency == "BCHSV" { 7 | "BSV" 8 | } else if currency == "ETH2" { 9 | "ksETH" 10 | } else if currency == "R" { 11 | "REV" 12 | } else if currency == "WAX" { 13 | "WAXP" 14 | } else if currency == "LOKI" { 15 | "OXEN" 16 | } else if currency == "GALAX" { 17 | "GALA" 18 | } else { 19 | currency 20 | } 21 | .to_uppercase() 22 | } 23 | 24 | pub(crate) fn normalize_pair(symbol: &str) -> Option { 25 | let (base, quote) = if symbol.ends_with("USDM") { 26 | // inverse swap 27 | (symbol.strip_suffix("USDM").unwrap().to_string(), "USD".to_string()) 28 | } else if symbol.ends_with("USDTM") || symbol.ends_with("USDCM") { 29 | // linear swap 30 | let base = &symbol[0..symbol.len() - 5]; 31 | let quote = &symbol[symbol.len() - 5..symbol.len() - 1]; 32 | (base.to_string(), quote.to_string()) 33 | } else if symbol[(symbol.len() - 2)..].parse::().is_ok() { 34 | // inverse future 35 | let base = &symbol[..symbol.len() - 4]; 36 | (base.to_string(), "USD".to_string()) 37 | } else if symbol.contains('-') { 38 | // spot 39 | let v: Vec<&str> = symbol.split('-').collect(); 40 | (v[0].to_string(), v[1].to_string()) 41 | } else { 42 | panic!("Unknown symbol {symbol}"); 43 | }; 44 | 45 | Some(format!("{}/{}", normalize_currency(&base), normalize_currency("e))) 46 | } 47 | 48 | pub(crate) fn get_market_type(symbol: &str) -> MarketType { 49 | if symbol.ends_with("USDM") { 50 | MarketType::InverseSwap 51 | } else if symbol.ends_with("USDTM") || symbol.ends_with("USDCM") { 52 | MarketType::LinearSwap 53 | } else if symbol[(symbol.len() - 2)..].parse::().is_ok() { 54 | MarketType::InverseFuture 55 | } else if symbol.contains('-') { 56 | MarketType::Spot 57 | } else { 58 | MarketType::Unknown 59 | } 60 | } 61 | 62 | #[cfg(test)] 63 | mod tests { 64 | use crypto_market_type::MarketType; 65 | 66 | #[test] 67 | fn test_get_market_type() { 68 | assert_eq!(MarketType::Spot, super::get_market_type("ETH2-ETH")); 69 | assert_eq!(MarketType::LinearSwap, super::get_market_type("XBTUSDTM")); 70 | assert_eq!(MarketType::LinearSwap, super::get_market_type("XBTUSDCM")); 71 | } 72 | 73 | #[test] 74 | fn test_normalize_pair() { 75 | assert_eq!("KSETH/ETH", super::normalize_pair("ETH2-ETH").unwrap()); 76 | assert_eq!("BTC/USDT", super::normalize_pair("XBTUSDTM").unwrap()); 77 | assert_eq!("BTC/USDC", super::normalize_pair("XBTUSDCM").unwrap()); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /crypto-pair/src/exchanges/mexc.rs: -------------------------------------------------------------------------------- 1 | use crypto_market_type::MarketType; 2 | 3 | pub(crate) fn get_market_type(symbol: &str, is_spot: Option) -> MarketType { 4 | if symbol.ends_with("_USD") { 5 | MarketType::InverseSwap 6 | } else if symbol.ends_with("_USDT") { 7 | if let Some(is_spot) = is_spot { 8 | if is_spot { MarketType::Spot } else { MarketType::LinearSwap } 9 | } else { 10 | MarketType::LinearSwap 11 | } 12 | } else if symbol.contains('_') { 13 | MarketType::Spot 14 | } else { 15 | MarketType::Unknown 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /crypto-pair/src/exchanges/mod.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | pub(super) mod binance; 4 | pub(super) mod bitfinex; 5 | pub(super) mod bitget; 6 | pub(super) mod bitmex; 7 | pub(super) mod bitstamp; 8 | pub(super) mod bybit; 9 | pub(super) mod deribit; 10 | pub(super) mod dydx; 11 | pub(super) mod ftx; 12 | pub(super) mod gate; 13 | pub(super) mod huobi; 14 | pub(super) mod kraken; 15 | pub(super) mod kucoin; 16 | pub(super) mod mexc; 17 | pub(super) mod okx; 18 | pub(super) mod zb; 19 | pub(super) mod zbg; 20 | -------------------------------------------------------------------------------- /crypto-pair/src/exchanges/okx.rs: -------------------------------------------------------------------------------- 1 | use crypto_market_type::MarketType; 2 | 3 | pub(crate) fn get_market_type(symbol: &str) -> MarketType { 4 | if symbol.ends_with("-USD-SWAP") { 5 | MarketType::InverseSwap 6 | } else if symbol.ends_with("-USDT-SWAP") || symbol.ends_with("-USDC-SWAP") { 7 | MarketType::LinearSwap 8 | } else if symbol.ends_with("-C") || symbol.ends_with("-P") { 9 | MarketType::EuropeanOption 10 | } else if symbol[(symbol.len() - 6)..].parse::().is_ok() { 11 | if symbol.contains("-USD-") { 12 | MarketType::InverseFuture 13 | } else if symbol.contains("-USDT-") || symbol.contains("-USDC-") { 14 | MarketType::LinearFuture 15 | } else { 16 | MarketType::Unknown 17 | } 18 | } else if symbol.contains('-') { 19 | MarketType::Spot 20 | } else { 21 | MarketType::Unknown 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /crypto-pair/src/exchanges/utils.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use reqwest::{header, Result}; 4 | 5 | pub(super) fn http_get(url: &str) -> Result { 6 | let mut headers = header::HeaderMap::new(); 7 | headers.insert(header::CONTENT_TYPE, header::HeaderValue::from_static("application/json")); 8 | 9 | let client = reqwest::blocking::Client::builder() 10 | .default_headers(headers) 11 | .user_agent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36") 12 | .gzip(true) 13 | .build()?; 14 | let response = client.get(url).send()?; 15 | 16 | match response.error_for_status() { 17 | Ok(resp) => Ok(resp.text()?), 18 | Err(error) => Err(error), 19 | } 20 | } 21 | 22 | pub(super) fn normalize_pair_with_quotes(symbol: &str, quotes: &HashSet) -> Option { 23 | for quote in quotes.iter() { 24 | if symbol.ends_with(quote) { 25 | let base = symbol.strip_suffix(quote).unwrap(); 26 | return Some(format!("{base}/{quote}").to_uppercase()); 27 | } 28 | } 29 | 30 | None 31 | } 32 | -------------------------------------------------------------------------------- /crypto-pair/src/exchanges/zb.rs: -------------------------------------------------------------------------------- 1 | use crypto_market_type::MarketType; 2 | 3 | #[allow(clippy::manual_map)] 4 | pub(crate) fn normalize_pair(symbol: &str) -> Option { 5 | if symbol.contains('_') { 6 | Some(symbol.replace('_', "/").to_uppercase()) 7 | } else if let Some(base) = symbol.strip_suffix("usdt") { 8 | Some(format!("{}/USDT", base.to_uppercase())) 9 | } else if let Some(base) = symbol.strip_suffix("usdc") { 10 | Some(format!("{}/USDC", base.to_uppercase())) 11 | } else if let Some(base) = symbol.strip_suffix("qc") { 12 | Some(format!("{}/QC", base.to_uppercase())) 13 | } else if let Some(base) = symbol.strip_suffix("btc") { 14 | Some(format!("{}/BTC", base.to_uppercase())) 15 | } else { 16 | None 17 | } 18 | } 19 | 20 | pub(crate) fn get_market_type(symbol: &str) -> MarketType { 21 | let lowercase = symbol.to_lowercase(); 22 | if lowercase.as_str() == symbol { MarketType::Spot } else { MarketType::LinearSwap } 23 | } 24 | 25 | #[cfg(test)] 26 | mod tests { 27 | use super::normalize_pair; 28 | 29 | #[test] 30 | fn test_normalize_pair() { 31 | assert_eq!(Some("BTC/USDT".to_string()), normalize_pair("btc_usdt")); 32 | assert_eq!(Some("BTC/USDT".to_string()), normalize_pair("btcusdt")); 33 | assert_eq!(Some("BTC/USDT".to_string()), normalize_pair("BTC_USDT")); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /crypto-pair/src/exchanges/zbg.rs: -------------------------------------------------------------------------------- 1 | use crypto_market_type::MarketType; 2 | 3 | pub(crate) fn normalize_pair(symbol: &str) -> Option { 4 | if symbol.ends_with("_USD-R") { 5 | let base = symbol.strip_suffix("_USD-R").unwrap(); 6 | Some(format!("{base}/USD")) 7 | } else { 8 | Some(symbol.replace('_', "/").to_uppercase()) 9 | } 10 | } 11 | 12 | pub(crate) fn get_market_type(symbol: &str) -> MarketType { 13 | if symbol.ends_with("_USD-R") { 14 | MarketType::InverseSwap 15 | } else if symbol.ends_with("_USDT") || symbol.ends_with("_ZUSD") { 16 | MarketType::LinearSwap 17 | } else { 18 | MarketType::Spot 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /crypto-pair/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::unnecessary_wraps)] 2 | 3 | use crypto_market_type::MarketType; 4 | mod exchanges; 5 | 6 | /// Normalize a trading currency. 7 | /// 8 | /// # Arguments 9 | /// 10 | /// * `currency` - The exchange-specific currency 11 | /// * `exchange` - The normalized symbol 12 | pub fn normalize_currency(currency: &str, exchange: &str) -> String { 13 | match exchange { 14 | "bitfinex" => exchanges::bitfinex::normalize_currency(currency), 15 | "bitmex" => exchanges::bitmex::normalize_currency(currency), 16 | "kraken" => exchanges::kraken::normalize_currency(currency), 17 | "kucoin" => exchanges::kucoin::normalize_currency(currency), 18 | _ => currency.to_uppercase(), 19 | } 20 | } 21 | 22 | /// Normalize a cryptocurrency trading symbol. 23 | /// 24 | /// # Arguments 25 | /// 26 | /// * `symbol` - The original pair of an exchange 27 | /// * `exchange` - The exchange name 28 | /// 29 | /// # Examples 30 | /// 31 | /// ``` 32 | /// use crypto_pair::normalize_pair; 33 | /// 34 | /// assert_eq!(Some("BTC/USD".to_string()), normalize_pair("XBTUSD", "bitmex")); 35 | /// assert_eq!(Some("BTC/USD".to_string()), normalize_pair("XBTH21", "bitmex")); 36 | /// assert_eq!(Some("BTC/USDT".to_string()), normalize_pair("BTCUSDT", "binance")); 37 | /// assert_eq!(Some("BTC/USDT".to_string()), normalize_pair("btcusdt", "huobi")); 38 | /// assert_eq!(Some("BTC/USDT".to_string()), normalize_pair("BTCUST", "bitfinex")); 39 | /// ``` 40 | pub fn normalize_pair(symbol: &str, exchange: &str) -> Option { 41 | match exchange { 42 | "binance" => exchanges::binance::normalize_pair(symbol), 43 | "bitfinex" => exchanges::bitfinex::normalize_pair(symbol), 44 | "bitget" => exchanges::bitget::normalize_pair(symbol), 45 | "bithumb" => Some(symbol.replace('-', "/")), 46 | "bitmex" => exchanges::bitmex::normalize_pair(symbol), 47 | "bitstamp" => exchanges::bitstamp::normalize_pair(symbol), 48 | "bitz" => Some(symbol.replace('_', "/").to_uppercase()), 49 | "bybit" => exchanges::bybit::normalize_pair(symbol), 50 | "coinbase_pro" => Some(symbol.replace('-', "/")), 51 | "deribit" => exchanges::deribit::normalize_pair(symbol), 52 | "dydx" => exchanges::dydx::normalize_pair(symbol), 53 | "ftx" => exchanges::ftx::normalize_pair(symbol), 54 | "gate" => { 55 | let (base, quote) = { 56 | let v: Vec<&str> = symbol.split('_').collect(); 57 | (v[0].to_string(), v[1].to_string()) 58 | }; 59 | 60 | Some(format!("{base}/{quote}")) 61 | } 62 | "huobi" => exchanges::huobi::normalize_pair(symbol), 63 | "kraken" => exchanges::kraken::normalize_pair(symbol), 64 | "kucoin" => exchanges::kucoin::normalize_pair(symbol), 65 | "mxc" | "mexc" => Some(symbol.replace('_', "/")), 66 | "okex" | "okx" => { 67 | let v: Vec<&str> = symbol.split('-').collect(); 68 | Some(format!("{}/{}", v[0], v[1])) 69 | } 70 | "Poloniex" => Some(symbol.replace('_', "/")), 71 | "Upbit" => Some(symbol.replace('-', "/")), 72 | "zb" => exchanges::zb::normalize_pair(symbol), 73 | "zbg" => exchanges::zbg::normalize_pair(symbol), 74 | _ => panic!("Unknown exchange {exchange}"), 75 | } 76 | } 77 | 78 | /// Infer out market type from the symbol. 79 | /// 80 | /// The `is_spot` parameter is not needed in most cases, but at some exchanges 81 | /// (including binance, gate and mexc) a symbol might exist in both spot and 82 | /// contract markets, for example: 83 | /// * At binance `BTCUSDT` exists in both spot and linear_swap markets 84 | /// * At gate `BTC_USDT` exists in both spot and linear_swap markets, 85 | /// `BTC_USD` exists in both spot and inverse_swap markets 86 | pub fn get_market_type(symbol: &str, exchange: &str, is_spot: Option) -> MarketType { 87 | match exchange { 88 | "binance" => exchanges::binance::get_market_type(symbol, is_spot), 89 | "bitfinex" => exchanges::bitfinex::get_market_type(symbol), 90 | "bitget" => exchanges::bitget::get_market_type(symbol), 91 | "bithumb" => MarketType::Spot, 92 | "bitmex" => exchanges::bitmex::get_market_type(symbol), 93 | "bitstamp" => MarketType::Spot, 94 | "bybit" => exchanges::bybit::get_market_type(symbol), 95 | "coinbase_pro" => MarketType::Spot, 96 | "deribit" => exchanges::deribit::get_market_type(symbol), 97 | "dydx" => MarketType::LinearSwap, 98 | "ftx" => exchanges::ftx::get_market_type(symbol), 99 | "gate" => exchanges::gate::get_market_type(symbol, is_spot), 100 | "huobi" => exchanges::huobi::get_market_type(symbol), 101 | "kraken" => exchanges::kraken::get_market_type(symbol), 102 | "kucoin" => exchanges::kucoin::get_market_type(symbol), 103 | "mxc" | "mexc" => exchanges::mexc::get_market_type(symbol, is_spot), 104 | "okex" | "okx" => exchanges::okx::get_market_type(symbol), 105 | "zb" => exchanges::zb::get_market_type(symbol), 106 | "zbg" => exchanges::zbg::get_market_type(symbol), 107 | _ => MarketType::Unknown, 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /crypto-pair/tests/binance.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | use crypto_market_type::MarketType; 4 | use crypto_pair::{get_market_type, normalize_currency, normalize_pair}; 5 | use serde::{Deserialize, Serialize}; 6 | use serde_json::Value; 7 | use std::collections::HashMap; 8 | use utils::http_get; 9 | 10 | const EXCHANGE_NAME: &str = "binance"; 11 | #[derive(Serialize, Deserialize)] 12 | struct BinanceResponse { 13 | symbols: Vec, 14 | } 15 | // Spot, Future and Swap markets 16 | #[derive(Serialize, Deserialize)] 17 | #[allow(non_snake_case)] 18 | struct Market { 19 | symbol: String, 20 | baseAsset: String, 21 | quoteAsset: String, 22 | #[serde(flatten)] 23 | extra: HashMap, 24 | } 25 | 26 | #[derive(Serialize, Deserialize)] 27 | #[allow(non_snake_case)] 28 | struct OptionMarket { 29 | symbol: String, 30 | quoteAsset: String, 31 | underlying: String, 32 | #[serde(flatten)] 33 | extra: HashMap, 34 | } 35 | 36 | // see 37 | fn fetch_spot_markets_raw() -> Vec { 38 | let txt = http_get("https://api.binance.com/api/v3/exchangeInfo").unwrap(); 39 | let resp = serde_json::from_str::>(&txt).unwrap(); 40 | resp.symbols 41 | } 42 | 43 | // see 44 | fn fetch_inverse_markets_raw() -> Vec { 45 | let txt = http_get("https://dapi.binance.com/dapi/v1/exchangeInfo").unwrap(); 46 | let resp = serde_json::from_str::>(&txt).unwrap(); 47 | resp.symbols 48 | } 49 | 50 | // see 51 | fn fetch_linear_markets_raw() -> Vec { 52 | let txt = http_get("https://fapi.binance.com/fapi/v1/exchangeInfo").unwrap(); 53 | let resp = serde_json::from_str::>(&txt).unwrap(); 54 | resp.symbols 55 | } 56 | 57 | fn fetch_option_markets_raw() -> Vec { 58 | #[derive(Serialize, Deserialize)] 59 | #[allow(non_snake_case)] 60 | struct OptionData { 61 | timezone: String, 62 | serverTime: i64, 63 | optionContracts: Vec, 64 | optionAssets: Vec, 65 | optionSymbols: Vec, 66 | } 67 | #[derive(Serialize, Deserialize)] 68 | #[allow(non_snake_case)] 69 | struct BinanceOptionResponse { 70 | code: i64, 71 | msg: String, 72 | data: OptionData, 73 | } 74 | 75 | let txt = 76 | http_get("https://voptions.binance.com/options-api/v1/public/exchange/symbols").unwrap(); 77 | let resp = serde_json::from_str::(&txt).unwrap(); 78 | resp.data.optionSymbols 79 | } 80 | 81 | #[ignore = "Binance has banned US IP addresses and returned 451 status code"] 82 | #[test] 83 | fn verify_spot_symbols() { 84 | let markets = fetch_spot_markets_raw(); 85 | for market in markets.iter() { 86 | let pair = normalize_pair(&market.symbol, EXCHANGE_NAME).unwrap(); 87 | let pair_expected = format!( 88 | "{}/{}", 89 | normalize_currency(&market.baseAsset, EXCHANGE_NAME), 90 | normalize_currency(&market.quoteAsset, EXCHANGE_NAME) 91 | ); 92 | 93 | assert_eq!(pair, pair_expected); 94 | assert_eq!(MarketType::Spot, get_market_type(&market.symbol, EXCHANGE_NAME, Some(true))); 95 | } 96 | } 97 | 98 | #[ignore = "Binance has banned US IP addresses and returned 451 status code"] 99 | #[test] 100 | fn verify_inverse_symbols() { 101 | let markets = fetch_inverse_markets_raw(); 102 | for market in markets.iter() { 103 | let pair = normalize_pair(&market.symbol, EXCHANGE_NAME).unwrap(); 104 | let pair_expected = format!( 105 | "{}/{}", 106 | normalize_currency(&market.baseAsset, EXCHANGE_NAME), 107 | normalize_currency(&market.quoteAsset, EXCHANGE_NAME) 108 | ); 109 | 110 | assert_eq!(pair, pair_expected); 111 | 112 | let market_type = get_market_type(&market.symbol, EXCHANGE_NAME, None); 113 | assert!(market_type == MarketType::InverseSwap || market_type == MarketType::InverseFuture); 114 | } 115 | } 116 | 117 | #[ignore = "Binance has banned US IP addresses and returned 451 status code"] 118 | #[test] 119 | fn verify_linear_symbols() { 120 | let markets = fetch_linear_markets_raw(); 121 | for market in markets.iter() { 122 | let pair = normalize_pair(&market.symbol, EXCHANGE_NAME).unwrap(); 123 | let pair_expected = format!( 124 | "{}/{}", 125 | normalize_currency(&market.baseAsset, EXCHANGE_NAME), 126 | normalize_currency(&market.quoteAsset, EXCHANGE_NAME) 127 | ); 128 | 129 | assert_eq!(pair, pair_expected); 130 | 131 | let market_type = get_market_type(&market.symbol, EXCHANGE_NAME, None); 132 | assert!(market_type == MarketType::LinearSwap || market_type == MarketType::LinearFuture); 133 | } 134 | } 135 | 136 | #[ignore = "Binance has shutdown option markets"] 137 | #[test] 138 | fn verify_option_symbols() { 139 | let markets = fetch_option_markets_raw(); 140 | for market in markets.iter() { 141 | let pair = normalize_pair(&market.symbol, EXCHANGE_NAME).unwrap(); 142 | 143 | let base = market.underlying.strip_suffix(market.quoteAsset.as_str()).unwrap(); 144 | let pair_expected = format!( 145 | "{}/{}", 146 | normalize_currency(base, EXCHANGE_NAME), 147 | normalize_currency(&market.quoteAsset, EXCHANGE_NAME) 148 | ); 149 | 150 | assert_eq!(pair, pair_expected); 151 | assert_eq!( 152 | MarketType::EuropeanOption, 153 | get_market_type(&market.symbol, EXCHANGE_NAME, None) 154 | ); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /crypto-pair/tests/bitfinex.rs: -------------------------------------------------------------------------------- 1 | use crypto_market_type::MarketType; 2 | use crypto_pair::{get_market_type, normalize_pair}; 3 | 4 | const EXCHANGE_NAME: &str = "bitfinex"; 5 | 6 | #[test] 7 | fn verify_spot_symbols() { 8 | assert_eq!("BTC/USD".to_string(), normalize_pair("BTCUSD", EXCHANGE_NAME).unwrap()); 9 | assert_eq!("BTC/USDT".to_string(), normalize_pair("BTCUST", EXCHANGE_NAME).unwrap()); 10 | assert_eq!("BTC/USDT".to_string(), normalize_pair("tBTCUST", EXCHANGE_NAME).unwrap()); 11 | 12 | assert_eq!(MarketType::Spot, get_market_type("BTCUSD", EXCHANGE_NAME, None)); 13 | assert_eq!(MarketType::Spot, get_market_type("BTCUST", EXCHANGE_NAME, None)); 14 | assert_eq!(MarketType::Spot, get_market_type("tBTCUST", EXCHANGE_NAME, None)); 15 | } 16 | 17 | #[test] 18 | fn verify_linear_swap_symbols() { 19 | assert_eq!("BTC/USDT".to_string(), normalize_pair("BTCF0:USTF0", EXCHANGE_NAME).unwrap()); 20 | assert_eq!("BTC/USDT".to_string(), normalize_pair("tBTCF0:USTF0", EXCHANGE_NAME).unwrap()); 21 | 22 | assert_eq!(MarketType::LinearSwap, get_market_type("BTCF0:USTF0", EXCHANGE_NAME, None)); 23 | assert_eq!(MarketType::LinearSwap, get_market_type("tBTCF0:USTF0", EXCHANGE_NAME, None)); 24 | } 25 | -------------------------------------------------------------------------------- /crypto-pair/tests/bitget.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | use crypto_market_type::MarketType; 4 | use crypto_pair::{get_market_type, normalize_currency, normalize_pair}; 5 | use serde::{Deserialize, Serialize}; 6 | use serde_json::Value; 7 | use std::collections::HashMap; 8 | use utils::http_get; 9 | 10 | const EXCHANGE_NAME: &str = "bitget"; 11 | 12 | // See https://bitgetlimited.github.io/apidoc/en/spot/#get-all-instruments 13 | #[derive(Clone, Serialize, Deserialize)] 14 | #[allow(non_snake_case)] 15 | struct SpotMarket { 16 | symbol: String, // symbol Id 17 | symbolName: String, // symbol name 18 | baseCoin: String, // Base coin 19 | quoteCoin: String, // Denomination coin 20 | status: String, // Status 21 | #[serde(flatten)] 22 | extra: HashMap, 23 | } 24 | 25 | // See https://bitgetlimited.github.io/apidoc/en/mix/#get-all-symbols 26 | #[derive(Clone, Serialize, Deserialize)] 27 | #[allow(non_snake_case)] 28 | struct SwapMarket { 29 | symbol: String, // symbol Id 30 | baseCoin: String, // Base coin 31 | quoteCoin: String, // Denomination coin 32 | #[serde(flatten)] 33 | extra: HashMap, 34 | } 35 | 36 | // See https://bitgetlimited.github.io/apidoc/en/spot/#get-all-instruments 37 | fn fetch_spot_markets_raw() -> Vec { 38 | #[derive(Serialize, Deserialize)] 39 | #[allow(non_snake_case)] 40 | struct Response { 41 | code: String, 42 | msg: String, 43 | data: Vec, 44 | requestTime: Value, 45 | #[serde(flatten)] 46 | extra: HashMap, 47 | } 48 | 49 | let txt = http_get("https://api.bitget.com/api/spot/v1/public/products").unwrap(); 50 | let resp = serde_json::from_str::(&txt).unwrap(); 51 | if resp.msg != "success" { 52 | Vec::new() 53 | } else { 54 | resp.data 55 | .into_iter() 56 | // Ignored ETH_SPBL and BTC_SPBL for now because they're not tradable 57 | .filter(|x| x.status == "online" && x.symbol.ends_with("USDT_SPBL")) 58 | .collect::>() 59 | } 60 | } 61 | 62 | // See https://bitgetlimited.github.io/apidoc/en/mix/#get-all-symbols 63 | // product_type: umcbl, LinearSwap; dmcbl, InverseSwap; 64 | fn fetch_swap_markets_raw(product_type: &str) -> Vec { 65 | #[derive(Serialize, Deserialize)] 66 | #[allow(non_snake_case)] 67 | struct Response { 68 | code: String, 69 | msg: String, 70 | data: Vec, 71 | requestTime: Value, 72 | #[serde(flatten)] 73 | extra: HashMap, 74 | } 75 | 76 | let txt = http_get( 77 | format!("https://api.bitget.com/api/mix/v1/market/contracts?productType={product_type}") 78 | .as_str(), 79 | ) 80 | .unwrap(); 81 | let resp = serde_json::from_str::(&txt).unwrap(); 82 | if resp.msg != "success" { Vec::new() } else { resp.data } 83 | } 84 | 85 | #[test] 86 | fn verify_spot_symbols() { 87 | let markets = fetch_spot_markets_raw(); 88 | for market in markets.iter() { 89 | let pair = normalize_pair(&market.symbol, EXCHANGE_NAME).unwrap(); 90 | let pair_expected = format!( 91 | "{}/{}", 92 | normalize_currency(&market.baseCoin, EXCHANGE_NAME), 93 | normalize_currency(&market.quoteCoin, EXCHANGE_NAME) 94 | ); 95 | 96 | assert_eq!(pair.as_str(), pair_expected); 97 | assert_eq!(MarketType::Spot, get_market_type(&market.symbol, EXCHANGE_NAME, None)); 98 | } 99 | } 100 | 101 | #[test] 102 | fn verify_swap_symbols() { 103 | let usdt_linear_markets = fetch_swap_markets_raw("umcbl"); 104 | let usdc_linear_markets = fetch_swap_markets_raw("cmcbl"); 105 | let inverse_markets = fetch_swap_markets_raw("dmcbl"); 106 | let markets = usdt_linear_markets 107 | .into_iter() 108 | .chain(usdc_linear_markets) 109 | .chain(inverse_markets) 110 | .collect::>(); 111 | for market in markets.iter() { 112 | let pair = normalize_pair(&market.symbol, EXCHANGE_NAME).unwrap(); 113 | let pair_expected = format!( 114 | "{}/{}", 115 | normalize_currency(&market.baseCoin, EXCHANGE_NAME), 116 | normalize_currency( 117 | if market.symbol.ends_with("PERP_CMCBL") { "USDC" } else { &market.quoteCoin }, 118 | EXCHANGE_NAME 119 | ) 120 | ); 121 | assert_eq!(pair.as_str(), pair_expected); 122 | 123 | let market_type = get_market_type(&market.symbol, EXCHANGE_NAME, None); 124 | assert!( 125 | market_type == MarketType::LinearSwap 126 | || market_type == MarketType::InverseSwap 127 | || market_type == MarketType::InverseFuture 128 | ); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /crypto-pair/tests/bithumb.rs: -------------------------------------------------------------------------------- 1 | use crypto_market_type::MarketType; 2 | use crypto_pair::{get_market_type, normalize_pair}; 3 | 4 | const EXCHANGE_NAME: &str = "bithumb"; 5 | 6 | #[test] 7 | fn verify_spot_symbols() { 8 | assert_eq!("ETH/USDT".to_string(), normalize_pair("ETH-USDT", EXCHANGE_NAME).unwrap()); 9 | assert_eq!(MarketType::Spot, get_market_type("ETH-USDT", EXCHANGE_NAME, None)); 10 | } 11 | -------------------------------------------------------------------------------- /crypto-pair/tests/bitmex.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | use crypto_pair::{normalize_currency, normalize_pair}; 4 | use serde::{Deserialize, Serialize}; 5 | use serde_json::Value; 6 | use std::collections::HashMap; 7 | use utils::http_get; 8 | 9 | const EXCHANGE_NAME: &str = "bitmex"; 10 | 11 | #[derive(Clone, Serialize, Deserialize)] 12 | #[allow(non_snake_case)] 13 | struct Instrument { 14 | symbol: String, 15 | rootSymbol: String, 16 | state: String, 17 | positionCurrency: Option, 18 | underlying: String, 19 | quoteCurrency: String, 20 | underlyingSymbol: Option, 21 | isQuanto: bool, 22 | isInverse: bool, 23 | expiry: Option, 24 | hasLiquidity: bool, 25 | openInterest: i64, 26 | fairMethod: Option, 27 | volume: i64, 28 | volume24h: i64, 29 | turnover: i64, 30 | turnover24h: i64, 31 | #[serde(flatten)] 32 | extra: HashMap, 33 | } 34 | 35 | fn fetch_instruments() -> Vec { 36 | let text = http_get("https://www.bitmex.com/api/v1/instrument/active").unwrap(); 37 | let instruments: Vec = serde_json::from_str::>(&text) 38 | .unwrap() 39 | .into_iter() 40 | .filter(|x| x.state == "Open" && x.hasLiquidity && x.volume24h > 0 && x.turnover24h > 0) 41 | .collect(); 42 | instruments 43 | } 44 | 45 | #[test] 46 | fn verify_normalize_pair() { 47 | let instruments = fetch_instruments(); 48 | for instrument in instruments.iter() { 49 | let symbol = instrument.symbol.as_str(); 50 | let pair = normalize_pair(symbol, EXCHANGE_NAME).unwrap(); 51 | 52 | let base_id = instrument.underlying.as_str(); 53 | let quote_id = instrument.quoteCurrency.as_str(); 54 | let pair_expected = format!( 55 | "{}/{}", 56 | normalize_currency(base_id, EXCHANGE_NAME), 57 | normalize_currency(quote_id, EXCHANGE_NAME) 58 | ); 59 | 60 | assert_eq!(pair, pair_expected); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /crypto-pair/tests/bitstamp.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | use crypto_market_type::MarketType; 4 | use crypto_pair::{get_market_type, normalize_pair}; 5 | use serde::{Deserialize, Serialize}; 6 | use serde_json::Value; 7 | use std::collections::HashMap; 8 | use utils::http_get; 9 | 10 | const EXCHANGE_NAME: &str = "bitstamp"; 11 | 12 | #[derive(Serialize, Deserialize)] 13 | struct SpotMarket { 14 | name: String, 15 | trading: String, 16 | url_symbol: String, 17 | #[serde(flatten)] 18 | extra: HashMap, 19 | } 20 | 21 | // see 22 | fn fetch_spot_markets_raw() -> Vec { 23 | let txt = http_get("https://www.bitstamp.net/api/v2/trading-pairs-info/").unwrap(); 24 | serde_json::from_str::>(&txt).unwrap() 25 | } 26 | 27 | #[test] 28 | fn verify_spot_symbols() { 29 | let markets = fetch_spot_markets_raw(); 30 | for market in markets.iter() { 31 | let pair = normalize_pair(&market.url_symbol, EXCHANGE_NAME).unwrap(); 32 | let pair_expected = market.name.as_str(); 33 | 34 | assert_eq!(pair.as_str(), pair_expected); 35 | assert_eq!(MarketType::Spot, get_market_type(&market.url_symbol, EXCHANGE_NAME, None)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /crypto-pair/tests/bitz.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | use crypto_pair::{normalize_currency, normalize_pair}; 4 | use serde::{Deserialize, Serialize}; 5 | use serde_json::Value; 6 | use std::collections::HashMap; 7 | use utils::http_get; 8 | 9 | const EXCHANGE_NAME: &str = "bitz"; 10 | 11 | #[derive(Clone, Serialize, Deserialize)] 12 | #[allow(non_snake_case)] 13 | struct SpotMarket { 14 | symbol: String, 15 | baseCurrency: String, 16 | quoteCurrency: String, 17 | #[serde(flatten)] 18 | extra: HashMap, 19 | } 20 | 21 | #[derive(Serialize, Deserialize)] 22 | struct SpotResponse { 23 | status: i64, 24 | msg: String, 25 | data: HashMap, 26 | time: i64, 27 | microtime: String, 28 | source: String, 29 | } 30 | 31 | // See https://apidocv2.bitz.plus/en/#get-data-of-trading-pairs 32 | fn fetch_spot_markets_raw() -> Vec { 33 | let txt = http_get("https://apiv2.bitz.com/V2/Market/symbolList").unwrap(); 34 | let resp = serde_json::from_str::(&txt).unwrap(); 35 | 36 | resp.data.values().cloned().collect::>() 37 | } 38 | 39 | #[derive(Clone, Serialize, Deserialize)] 40 | #[allow(non_snake_case)] 41 | struct SwapMarket { 42 | symbol: String, 43 | settleAnchor: String, // settle anchor 44 | quoteAnchor: String, // quote anchor 45 | contractAnchor: String, // contract anchor 46 | pair: String, // contract market 47 | #[serde(flatten)] 48 | extra: HashMap, 49 | } 50 | 51 | #[derive(Serialize, Deserialize)] 52 | struct SwapResponse { 53 | status: i64, 54 | msg: String, 55 | data: Vec, 56 | time: i64, 57 | microtime: String, 58 | source: String, 59 | } 60 | 61 | // See https://apidocv2.bitz.plus/en/#get-market-list-of-contract-transactions 62 | fn fetch_swap_markets_raw() -> Vec { 63 | let txt = http_get("https://apiv2.bitz.com/V2/Market/getContractCoin").unwrap(); 64 | let resp = serde_json::from_str::(&txt).unwrap(); 65 | resp.data 66 | } 67 | 68 | #[test] 69 | #[ignore = "bitz.com has shutdown since October 2021"] 70 | fn verify_spot_symbols() { 71 | let markets = fetch_spot_markets_raw(); 72 | for market in markets.iter() { 73 | let pair = normalize_pair(&market.symbol, EXCHANGE_NAME).unwrap(); 74 | let pair_expected = format!( 75 | "{}/{}", 76 | normalize_currency(&market.baseCurrency, EXCHANGE_NAME), 77 | normalize_currency(&market.quoteCurrency, EXCHANGE_NAME) 78 | ); 79 | 80 | assert_eq!(pair.as_str(), pair_expected); 81 | } 82 | } 83 | 84 | #[test] 85 | #[ignore = "bitz.com has shutdown since October 2021"] 86 | fn verify_swap_symbols() { 87 | let markets = fetch_swap_markets_raw(); 88 | for market in markets.iter() { 89 | let pair = normalize_pair(&market.pair, EXCHANGE_NAME).unwrap(); 90 | let pair_expected = format!( 91 | "{}/{}", 92 | normalize_currency(&market.symbol, EXCHANGE_NAME), 93 | normalize_currency(&market.quoteAnchor, EXCHANGE_NAME) 94 | ); 95 | 96 | assert_eq!(pair.as_str(), pair_expected); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /crypto-pair/tests/bybit.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | use crypto_market_type::MarketType; 4 | use crypto_pair::{get_market_type, normalize_currency, normalize_pair}; 5 | use serde::{Deserialize, Serialize}; 6 | use serde_json::Value; 7 | use std::collections::HashMap; 8 | use utils::http_get; 9 | 10 | const EXCHANGE_NAME: &str = "bybit"; 11 | 12 | #[derive(Serialize, Deserialize)] 13 | struct BybitMarket { 14 | name: String, 15 | base_currency: String, 16 | quote_currency: String, 17 | #[serde(flatten)] 18 | extra: HashMap, 19 | } 20 | 21 | #[derive(Serialize, Deserialize)] 22 | struct Response { 23 | ret_code: i64, 24 | ret_msg: String, 25 | ext_code: String, 26 | ext_info: String, 27 | result: Vec, 28 | } 29 | 30 | // See https://bybit-exchange.github.io/docs/inverse/#t-querysymbol 31 | fn fetch_swap_markets_raw() -> Vec { 32 | let txt = http_get("https://api.bybit.com/v2/public/symbols").unwrap(); 33 | let resp = serde_json::from_str::(&txt).unwrap(); 34 | assert_eq!(resp.ret_code, 0); 35 | resp.result 36 | } 37 | 38 | #[ignore = "403 ERROR, US IP addresses are blocked"] 39 | #[test] 40 | fn verify_swap_symbols() { 41 | let markets = fetch_swap_markets_raw(); 42 | for market in markets.iter() { 43 | let pair = normalize_pair(&market.name, EXCHANGE_NAME).unwrap(); 44 | let pair_expected = format!( 45 | "{}/{}", 46 | normalize_currency(&market.base_currency, EXCHANGE_NAME), 47 | normalize_currency(&market.quote_currency, EXCHANGE_NAME) 48 | ); 49 | 50 | assert_eq!(pair.as_str(), pair_expected); 51 | 52 | let market_type = get_market_type(&market.name, EXCHANGE_NAME, None); 53 | assert!( 54 | market_type == MarketType::LinearSwap 55 | || market_type == MarketType::InverseSwap 56 | || market_type == MarketType::InverseFuture 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /crypto-pair/tests/coinbase_pro.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | use crypto_market_type::MarketType; 4 | use crypto_pair::{get_market_type, normalize_currency, normalize_pair}; 5 | use serde::{Deserialize, Serialize}; 6 | use utils::http_get; 7 | 8 | const EXCHANGE_NAME: &str = "coinbase_pro"; 9 | 10 | #[derive(Serialize, Deserialize)] 11 | struct SpotMarket { 12 | id: String, 13 | base_currency: String, 14 | quote_currency: String, 15 | } 16 | 17 | // see 18 | fn fetch_spot_markets_raw() -> Vec { 19 | let txt = http_get("https://api.pro.coinbase.com/products").unwrap(); 20 | serde_json::from_str::>(&txt) 21 | .unwrap() 22 | .into_iter() 23 | .filter(|m| !m.id.contains("AUCTION")) 24 | .collect() 25 | } 26 | 27 | #[test] 28 | fn verify_spot_symbols() { 29 | let markets = fetch_spot_markets_raw(); 30 | for market in markets.iter() { 31 | let pair = normalize_pair(&market.id, EXCHANGE_NAME).unwrap(); 32 | let pair_expected = format!( 33 | "{}/{}", 34 | normalize_currency(&market.base_currency, EXCHANGE_NAME), 35 | normalize_currency(&market.quote_currency, EXCHANGE_NAME) 36 | ); 37 | 38 | assert_eq!(pair.as_str(), pair_expected); 39 | assert_eq!(MarketType::Spot, get_market_type(&market.id, EXCHANGE_NAME, None)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /crypto-pair/tests/deribit.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | use crypto_pair::{normalize_currency, normalize_pair}; 4 | use serde::{Deserialize, Serialize}; 5 | use serde_json::Value; 6 | use std::collections::HashMap; 7 | use utils::http_get; 8 | 9 | const EXCHANGE_NAME: &str = "deribit"; 10 | 11 | #[derive(Serialize, Deserialize)] 12 | #[allow(non_snake_case)] 13 | struct DeribitResponse { 14 | id: Option, 15 | jsonrpc: String, 16 | result: Vec, 17 | usIn: i64, 18 | usOut: i64, 19 | usDiff: i64, 20 | testnet: bool, 21 | 22 | #[serde(flatten)] 23 | extra: HashMap, 24 | } 25 | 26 | #[derive(Serialize, Deserialize)] 27 | struct Instrument { 28 | kind: String, 29 | quote_currency: String, 30 | instrument_name: String, 31 | base_currency: String, 32 | #[serde(flatten)] 33 | extra: HashMap, 34 | } 35 | 36 | /// Get active trading instruments. 37 | /// 38 | /// doc: 39 | /// 40 | /// `currency`, available values are `BTC` and `ETH`. 41 | /// 42 | /// `kind`, available values are `future` and `option`. 43 | /// 44 | /// Example: 45 | fn fetch_instruments(currency: &str, kind: &str) -> Vec { 46 | let url = format!( 47 | "https://www.deribit.com/api/v2/public/get_instruments?currency={currency}&kind={kind}" 48 | ); 49 | let txt = http_get(&url).unwrap(); 50 | let resp = serde_json::from_str::>(&txt).unwrap(); 51 | resp.result 52 | } 53 | 54 | #[test] 55 | fn verify_all_symbols() { 56 | let mut markets = fetch_instruments("BTC", "future"); 57 | 58 | let mut tmp_markets = fetch_instruments("ETH", "future"); 59 | markets.append(&mut tmp_markets); 60 | 61 | tmp_markets = fetch_instruments("BTC", "option"); 62 | markets.append(&mut tmp_markets); 63 | 64 | tmp_markets = fetch_instruments("ETH", "option"); 65 | markets.append(&mut tmp_markets); 66 | 67 | for market in markets.iter() { 68 | let pair = normalize_pair(&market.instrument_name, EXCHANGE_NAME).unwrap(); 69 | let pair_expected = format!( 70 | "{}/{}", 71 | normalize_currency(&market.base_currency, EXCHANGE_NAME), 72 | normalize_currency(&market.quote_currency, EXCHANGE_NAME) 73 | ); 74 | 75 | assert_eq!(pair.as_str(), pair_expected); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /crypto-pair/tests/dydx.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | use crypto_market_type::MarketType; 4 | use crypto_pair::{get_market_type, normalize_currency, normalize_pair}; 5 | use serde::{Deserialize, Serialize}; 6 | use serde_json::Value; 7 | use std::collections::HashMap; 8 | use utils::http_get; 9 | 10 | const EXCHANGE_NAME: &str = "dydx"; 11 | 12 | #[derive(Clone, Serialize, Deserialize)] 13 | #[allow(non_snake_case)] 14 | struct PerpetualMarket { 15 | market: String, 16 | status: String, 17 | baseAsset: String, 18 | quoteAsset: String, 19 | #[serde(rename = "type")] 20 | type_: String, 21 | #[serde(flatten)] 22 | extra: HashMap, 23 | } 24 | 25 | #[derive(Clone, Serialize, Deserialize)] 26 | #[allow(non_snake_case)] 27 | struct MarketsResponse { 28 | markets: HashMap, 29 | } 30 | 31 | // See https://docs.dydx.exchange/#get-markets 32 | fn fetch_markets_raw() -> Vec { 33 | let txt = http_get("https://api.dydx.exchange/v3/markets").unwrap(); 34 | let resp = serde_json::from_str::(&txt).unwrap(); 35 | resp.markets 36 | .values() 37 | .cloned() 38 | .filter(|x| x.status == "ONLINE") 39 | .collect::>() 40 | } 41 | 42 | #[test] 43 | fn verify_linear_swap_symbols() { 44 | let markets = fetch_markets_raw(); 45 | for market in markets { 46 | let pair = normalize_pair(&market.market, EXCHANGE_NAME).unwrap(); 47 | let pair_expected = format!( 48 | "{}/{}", 49 | normalize_currency(&market.baseAsset, EXCHANGE_NAME), 50 | normalize_currency(&market.quoteAsset, EXCHANGE_NAME) 51 | ); 52 | 53 | assert_eq!(pair, pair_expected); 54 | assert_eq!(MarketType::LinearSwap, get_market_type(&market.market, EXCHANGE_NAME, None)); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /crypto-pair/tests/ftx.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | use crypto_pair::normalize_pair; 4 | use serde::{Deserialize, Serialize}; 5 | use serde_json::Value; 6 | use std::collections::HashMap; 7 | use utils::http_get; 8 | 9 | const EXCHANGE_NAME: &str = "ftx"; 10 | 11 | #[derive(Serialize, Deserialize)] 12 | #[allow(non_snake_case)] 13 | struct FtxMarket { 14 | name: String, 15 | baseCurrency: Option, 16 | quoteCurrency: Option, 17 | underlying: Option, 18 | #[serde(rename = "type")] 19 | type_: String, 20 | #[serde(flatten)] 21 | extra: HashMap, 22 | } 23 | 24 | #[derive(Serialize, Deserialize)] 25 | struct Response { 26 | success: bool, 27 | result: Vec, 28 | } 29 | 30 | fn fetch_markets_raw() -> Vec { 31 | let txt = http_get("https://ftx.com/api/markets").unwrap(); 32 | let resp = serde_json::from_str::(&txt).unwrap(); 33 | assert!(resp.success); 34 | resp.result 35 | } 36 | 37 | #[ignore = "503 Service Temporarily Unavailable"] 38 | #[test] 39 | fn verify_all_symbols() { 40 | let markets = fetch_markets_raw(); 41 | for market in markets.iter() { 42 | let pair = normalize_pair(&market.name, EXCHANGE_NAME).unwrap(); 43 | let pair_expected = if market.type_ == "spot" { 44 | // spot 45 | market.name.clone() 46 | } else if market.type_ == "future" { 47 | if market.name.ends_with("-PERP") || market.name.contains("-MOVE-") { 48 | format!("{}/USD", market.underlying.clone().unwrap()) 49 | } else if market.name.contains("BVOL/") { 50 | format!( 51 | "{}/{}", 52 | market.baseCurrency.clone().unwrap(), 53 | market.quoteCurrency.clone().unwrap() 54 | ) 55 | } else { 56 | // linear future 57 | if market.name.contains('-') { 58 | format!("{}/USD", market.underlying.clone().unwrap()) 59 | } else { 60 | // prediction 61 | format!("{}/USD", market.name) 62 | } 63 | } 64 | } else { 65 | panic!("Unknown symbol {}", market.name); 66 | }; 67 | 68 | assert_eq!(pair.as_str(), pair_expected); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /crypto-pair/tests/gate.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | use crypto_market_type::MarketType; 4 | use crypto_pair::{get_market_type, normalize_currency, normalize_pair}; 5 | use serde::{Deserialize, Serialize}; 6 | use serde_json::Value; 7 | use std::collections::HashMap; 8 | use utils::http_get; 9 | 10 | const EXCHANGE_NAME: &str = "gate"; 11 | 12 | #[derive(Serialize, Deserialize)] 13 | #[allow(non_snake_case)] 14 | struct SpotMarket { 15 | id: String, 16 | base: String, 17 | quote: String, 18 | #[serde(flatten)] 19 | extra: HashMap, 20 | } 21 | 22 | // See https://www.gateio.pro/docs/apiv4/zh_CN/index.html#611e43ef81 23 | fn fetch_spot_markets_raw() -> Vec { 24 | let txt = http_get("https://api.gateio.ws/api/v4/spot/currency_pairs").unwrap(); 25 | serde_json::from_str::>(&txt).unwrap() 26 | } 27 | 28 | #[test] 29 | fn verify_spot_symbols() { 30 | let markets = fetch_spot_markets_raw(); 31 | for market in markets.iter() { 32 | let pair = normalize_pair(&market.id, EXCHANGE_NAME).unwrap(); 33 | let pair_expected = format!( 34 | "{}/{}", 35 | normalize_currency(&market.base, EXCHANGE_NAME), 36 | normalize_currency(&market.quote, EXCHANGE_NAME) 37 | ); 38 | 39 | assert_eq!(pair.as_str(), pair_expected); 40 | assert_eq!(MarketType::Spot, get_market_type(&market.id, EXCHANGE_NAME, Some(true))); 41 | } 42 | } 43 | 44 | #[test] 45 | fn verify_swap_symbols() { 46 | assert_eq!("BTC/USD".to_string(), normalize_pair("BTC_USD", EXCHANGE_NAME).unwrap()); 47 | 48 | assert_eq!("BTC/USDT".to_string(), normalize_pair("BTC_USDT", EXCHANGE_NAME).unwrap()); 49 | 50 | assert_eq!(MarketType::InverseSwap, get_market_type("BTC_USD", EXCHANGE_NAME, None)); 51 | assert_eq!(MarketType::LinearSwap, get_market_type("BTC_USDT", EXCHANGE_NAME, None)); 52 | } 53 | 54 | #[derive(Clone, Serialize, Deserialize)] 55 | #[allow(non_snake_case)] 56 | struct FutureMarket { 57 | name: String, 58 | underlying: String, 59 | cycle: String, 60 | #[serde(flatten)] 61 | extra: HashMap, 62 | } 63 | 64 | // See https://www.gateio.pro/docs/apiv4/zh_CN/index.html#595cd9fe3c-2 65 | fn fetch_future_markets_raw(settle: &str) -> Vec { 66 | let txt = 67 | http_get(format!("https://api.gateio.ws/api/v4/delivery/{settle}/contracts").as_str()) 68 | .unwrap(); 69 | serde_json::from_str::>(&txt).unwrap() 70 | } 71 | 72 | #[test] 73 | fn verify_future_symbols() { 74 | let markets = fetch_future_markets_raw("usdt"); 75 | for market in markets.iter() { 76 | let pair = normalize_pair(&market.name, EXCHANGE_NAME).unwrap(); 77 | let pair_expected = market.underlying.replace('_', "/"); 78 | 79 | assert_eq!(pair, pair_expected); 80 | 81 | let market_type = get_market_type(&market.name, EXCHANGE_NAME, None); 82 | assert!( 83 | market_type == MarketType::InverseFuture || market_type == MarketType::LinearFuture 84 | ); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /crypto-pair/tests/huobi.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | use crypto_market_type::MarketType; 4 | use crypto_pair::{get_market_type, normalize_currency, normalize_pair}; 5 | use serde::{Deserialize, Serialize}; 6 | use serde_json::Value; 7 | use std::collections::HashMap; 8 | use utils::http_get; 9 | 10 | const EXCHANGE_NAME: &str = "huobi"; 11 | 12 | #[derive(Serialize, Deserialize)] 13 | #[serde(rename_all = "kebab-case")] 14 | struct SpotMarket { 15 | base_currency: String, 16 | quote_currency: String, 17 | symbol: String, 18 | state: String, 19 | #[serde(flatten)] 20 | extra: HashMap, 21 | } 22 | 23 | #[derive(Serialize, Deserialize)] 24 | struct Response { 25 | status: String, 26 | data: Vec, 27 | } 28 | 29 | // see 30 | fn fetch_spot_markets_raw() -> Vec { 31 | let txt = http_get("https://api.huobi.pro/v1/common/symbols").unwrap(); 32 | let resp = serde_json::from_str::>(&txt).unwrap(); 33 | resp.data 34 | } 35 | 36 | #[test] 37 | fn verify_spot_symbols() { 38 | let markets = fetch_spot_markets_raw(); 39 | for market in markets.iter().filter(|x| x.state == "online") { 40 | let pair = normalize_pair(&market.symbol, EXCHANGE_NAME).unwrap(); 41 | let pair_expected = format!( 42 | "{}/{}", 43 | normalize_currency(&market.base_currency, EXCHANGE_NAME), 44 | normalize_currency(&market.quote_currency, EXCHANGE_NAME) 45 | ); 46 | 47 | assert_eq!(pair.as_str(), pair_expected); 48 | assert_eq!(MarketType::Spot, get_market_type(&market.symbol, EXCHANGE_NAME, None)); 49 | } 50 | } 51 | 52 | #[test] 53 | fn verify_inverse_future_symbols() { 54 | assert_eq!("BTC/USD".to_string(), normalize_pair("BTC_CW", EXCHANGE_NAME).unwrap()); 55 | assert_eq!("BTC/USD".to_string(), normalize_pair("BTC_CQ", EXCHANGE_NAME).unwrap()); 56 | 57 | assert_eq!(MarketType::InverseFuture, get_market_type("BTC_CW", EXCHANGE_NAME, None)); 58 | assert_eq!(MarketType::InverseFuture, get_market_type("BTC_CQ", EXCHANGE_NAME, None)); 59 | } 60 | 61 | #[test] 62 | fn verify_swap_symbols() { 63 | assert_eq!("BTC/USD".to_string(), normalize_pair("BTC-USD", EXCHANGE_NAME).unwrap()); 64 | assert_eq!("BTC/USDT".to_string(), normalize_pair("BTC-USDT", EXCHANGE_NAME).unwrap()); 65 | 66 | assert_eq!(MarketType::InverseSwap, get_market_type("BTC-USD", EXCHANGE_NAME, None)); 67 | assert_eq!(MarketType::LinearSwap, get_market_type("BTC-USDT", EXCHANGE_NAME, None)); 68 | } 69 | 70 | #[derive(Serialize, Deserialize)] 71 | struct OptionMarket { 72 | symbol: String, 73 | contract_code: String, 74 | delivery_asset: String, 75 | quote_asset: String, 76 | trade_partition: String, 77 | #[serde(flatten)] 78 | extra: HashMap, 79 | } 80 | 81 | // see 82 | fn fetch_option_markets_raw() -> Vec { 83 | let txt = http_get("https://api.hbdm.com/option-api/v1/option_contract_info").unwrap(); 84 | let resp = serde_json::from_str::>(&txt).unwrap(); 85 | resp.data 86 | } 87 | 88 | #[test] 89 | #[ignore] 90 | fn verify_option_symbols() { 91 | let markets = fetch_option_markets_raw(); 92 | for market in markets.iter() { 93 | let pair = normalize_pair(&market.contract_code, EXCHANGE_NAME).unwrap(); 94 | let pair_expected = format!( 95 | "{}/{}", 96 | normalize_currency(&market.symbol, EXCHANGE_NAME), 97 | normalize_currency(&market.quote_asset, EXCHANGE_NAME) 98 | ); 99 | 100 | assert_eq!(pair.as_str(), pair_expected); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /crypto-pair/tests/kraken.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | use crypto_market_type::MarketType; 4 | use crypto_pair::{get_market_type, normalize_currency, normalize_pair}; 5 | use serde::{Deserialize, Serialize}; 6 | use serde_json::Value; 7 | use std::collections::HashMap; 8 | use utils::http_get; 9 | 10 | const EXCHANGE_NAME: &str = "kraken"; 11 | 12 | // https://docs.kraken.com/rest/#operation/getTradableAssetPairs 13 | #[derive(Clone, Serialize, Deserialize)] 14 | struct SpotMarket { 15 | altname: String, 16 | wsname: String, 17 | base: String, 18 | quote: String, 19 | #[serde(flatten)] 20 | extra: HashMap, 21 | } 22 | 23 | #[derive(Serialize, Deserialize)] 24 | struct KrakenResponse { 25 | result: HashMap, 26 | } 27 | 28 | #[test] 29 | fn verify_spot_symbols() { 30 | let txt = http_get("https://api.kraken.com/0/public/AssetPairs").unwrap(); 31 | let resp = serde_json::from_str::(&txt).unwrap(); 32 | 33 | for (_key, market) in resp.result.iter() { 34 | let pair = normalize_pair(&market.wsname, EXCHANGE_NAME).unwrap(); 35 | let pair2 = normalize_pair(&market.altname, EXCHANGE_NAME).unwrap(); 36 | // let pair3 = normalize_pair(key, EXCHANGE_NAME).unwrap(); 37 | let pair_expected = format!( 38 | "{}/{}", 39 | normalize_currency(&market.base, EXCHANGE_NAME), 40 | normalize_currency(&market.quote, EXCHANGE_NAME) 41 | ); 42 | 43 | assert_eq!(pair.as_str(), pair_expected); 44 | assert_eq!(pair2.as_str(), pair_expected); 45 | // assert_eq!(pair3.as_str(), pair_expected); 46 | assert_eq!(MarketType::Spot, get_market_type(&market.wsname, EXCHANGE_NAME, None)); 47 | assert_eq!(MarketType::Spot, get_market_type(&market.altname, EXCHANGE_NAME, None)); 48 | } 49 | } 50 | 51 | #[derive(Clone, Serialize, Deserialize)] 52 | struct FuturesMarket { 53 | symbol: String, 54 | #[serde(rename = "type")] 55 | type_: String, 56 | tradeable: bool, 57 | underlying: Option, 58 | #[serde(flatten)] 59 | extra: HashMap, 60 | } 61 | 62 | #[derive(Serialize, Deserialize)] 63 | struct FuturesResponse { 64 | result: String, 65 | instruments: Vec, 66 | } 67 | 68 | // see 69 | fn fetch_futures_markets_raw() -> Vec { 70 | let txt = http_get("https://futures.kraken.com/derivatives/api/v3/instruments").unwrap(); 71 | let obj = serde_json::from_str::>(&txt).unwrap(); 72 | 73 | obj.instruments 74 | .into_iter() 75 | .filter(|x| x.tradeable) 76 | .filter(|m| m.symbol.starts_with("pi_") || m.symbol.starts_with("fi_")) 77 | .collect::>() 78 | } 79 | 80 | #[test] 81 | fn verify_futures_symbols() { 82 | let markets = fetch_futures_markets_raw(); 83 | 84 | for market in markets.iter() { 85 | let underlying = market.underlying.clone().unwrap(); 86 | let pair = normalize_pair(&market.symbol, EXCHANGE_NAME).unwrap(); 87 | let pair_expected = format!( 88 | "{}/USD", 89 | normalize_currency(&underlying[3..(underlying.len() - 3)], EXCHANGE_NAME), 90 | ); 91 | 92 | assert_eq!(pair.as_str(), pair_expected); 93 | 94 | let market_type = get_market_type(&market.symbol, EXCHANGE_NAME, None); 95 | 96 | if market.symbol.starts_with("fi_") { 97 | assert_eq!(market_type, MarketType::InverseFuture); 98 | } else { 99 | assert_eq!(market_type, MarketType::InverseSwap); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /crypto-pair/tests/kucoin.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | use crypto_market_type::MarketType; 4 | use crypto_pair::{get_market_type, normalize_currency, normalize_pair}; 5 | use serde::{Deserialize, Serialize}; 6 | use serde_json::Value; 7 | use std::collections::HashMap; 8 | use utils::http_get; 9 | 10 | const EXCHANGE_NAME: &str = "kucoin"; 11 | 12 | #[derive(Clone, Serialize, Deserialize)] 13 | #[allow(non_snake_case)] 14 | struct SpotMarket { 15 | symbol: String, 16 | baseCurrency: String, 17 | quoteCurrency: String, 18 | #[serde(flatten)] 19 | extra: HashMap, 20 | } 21 | 22 | #[derive(Serialize, Deserialize)] 23 | struct Response { 24 | code: String, 25 | data: Vec, 26 | } 27 | 28 | // See https://docs.kucoin.com/#get-symbols-list 29 | fn fetch_spot_markets_raw() -> Vec { 30 | let txt = http_get("https://api.kucoin.com/api/v1/symbols").unwrap(); 31 | let resp = serde_json::from_str::>(&txt).unwrap(); 32 | resp.data 33 | } 34 | 35 | // See https://docs.kucoin.com/#get-symbols-list 36 | fn fetch_swap_markets_raw() -> Vec { 37 | let txt = http_get("https://api-futures.kucoin.com/api/v1/contracts/active").unwrap(); 38 | let resp = serde_json::from_str::>(&txt).unwrap(); 39 | resp.data 40 | } 41 | 42 | #[test] 43 | fn verify_spot_symbols() { 44 | let markets = fetch_spot_markets_raw(); 45 | for market in markets.iter() { 46 | let pair = normalize_pair(&market.symbol, EXCHANGE_NAME).unwrap(); 47 | let pair_expected = format!( 48 | "{}/{}", 49 | normalize_currency(&market.baseCurrency, EXCHANGE_NAME), 50 | normalize_currency(&market.quoteCurrency, EXCHANGE_NAME) 51 | ); 52 | 53 | assert_eq!(pair.as_str(), pair_expected); 54 | assert_eq!(MarketType::Spot, get_market_type(&market.symbol, EXCHANGE_NAME, None)); 55 | } 56 | } 57 | 58 | #[test] 59 | fn verify_swap_symbols() { 60 | let markets = fetch_swap_markets_raw(); 61 | for market in markets.iter() { 62 | let pair = normalize_pair(&market.symbol, EXCHANGE_NAME).unwrap(); 63 | let pair_expected = format!( 64 | "{}/{}", 65 | normalize_currency(&market.baseCurrency, EXCHANGE_NAME), 66 | normalize_currency(&market.quoteCurrency, EXCHANGE_NAME) 67 | ); 68 | 69 | assert_eq!(pair.as_str(), pair_expected); 70 | 71 | let market_type = get_market_type(&market.symbol, EXCHANGE_NAME, None); 72 | assert!( 73 | market_type == MarketType::LinearSwap 74 | || market_type == MarketType::InverseSwap 75 | || market_type == MarketType::InverseFuture 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /crypto-pair/tests/mexc.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | use crypto_market_type::MarketType; 4 | use crypto_pair::{get_market_type, normalize_currency, normalize_pair}; 5 | use serde::{Deserialize, Serialize}; 6 | use serde_json::Value; 7 | use std::collections::HashMap; 8 | use utils::http_get; 9 | 10 | const EXCHANGE_NAME: &str = "mexc"; 11 | 12 | #[test] 13 | fn verify_spot_symbols() { 14 | assert_eq!("BTC/USDT".to_string(), normalize_pair("BTC_USDT", EXCHANGE_NAME).unwrap()); 15 | } 16 | 17 | #[derive(Serialize, Deserialize)] 18 | #[allow(non_snake_case)] 19 | struct SwapMarket { 20 | symbol: String, 21 | baseCoin: String, 22 | quoteCoin: String, 23 | settleCoin: String, 24 | #[serde(flatten)] 25 | extra: HashMap, 26 | } 27 | 28 | #[derive(Serialize, Deserialize)] 29 | struct Response { 30 | success: bool, 31 | code: i64, 32 | data: Vec, 33 | } 34 | 35 | // see 36 | fn fetch_swap_markets_raw() -> Vec { 37 | let txt = http_get("https://contract.mexc.com/api/v1/contract/detail").unwrap(); 38 | let resp = serde_json::from_str::(&txt).unwrap(); 39 | resp.data 40 | } 41 | 42 | #[test] 43 | fn verify_swap_symbols() { 44 | let markets = fetch_swap_markets_raw(); 45 | for market in markets.iter() { 46 | let pair = normalize_pair(&market.symbol, EXCHANGE_NAME).unwrap(); 47 | let pair_expected = format!( 48 | "{}/{}", 49 | normalize_currency(&market.baseCoin, EXCHANGE_NAME), 50 | normalize_currency(&market.quoteCoin, EXCHANGE_NAME) 51 | ); 52 | 53 | assert_eq!(pair.as_str(), pair_expected); 54 | 55 | let market_type = get_market_type(&market.symbol, EXCHANGE_NAME, None); 56 | assert!(market_type == MarketType::LinearSwap || market_type == MarketType::InverseSwap); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /crypto-pair/tests/utils/mod.rs: -------------------------------------------------------------------------------- 1 | use reqwest::{header, Result}; 2 | 3 | pub(super) fn http_get(url: &str) -> Result { 4 | let mut headers = header::HeaderMap::new(); 5 | headers.insert(header::CONTENT_TYPE, header::HeaderValue::from_static("application/json")); 6 | 7 | let client = reqwest::blocking::Client::builder() 8 | .default_headers(headers) 9 | .danger_accept_invalid_certs(true) // For zbg 'unable to get local issuer certificate' error 10 | .user_agent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36") 11 | .gzip(true) 12 | .build()?; 13 | let response = client.get(url).send()?; 14 | 15 | match response.error_for_status() { 16 | Ok(resp) => Ok(resp.text()?), 17 | Err(error) => Err(error), 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /crypto-pair/tests/zb.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | use crypto_market_type::MarketType; 4 | use crypto_pair::{get_market_type, normalize_pair}; 5 | use serde::{Deserialize, Serialize}; 6 | use serde_json::Value; 7 | use std::collections::HashMap; 8 | use utils::http_get; 9 | 10 | const EXCHANGE_NAME: &str = "zb"; 11 | 12 | #[derive(Serialize, Deserialize)] 13 | #[allow(non_snake_case)] 14 | struct SwapMarket { 15 | symbol: String, 16 | marginCurrencyName: String, 17 | buyerCurrencyName: String, 18 | sellerCurrencyName: String, 19 | #[serde(flatten)] 20 | extra: HashMap, 21 | } 22 | 23 | #[derive(Serialize, Deserialize)] 24 | #[allow(non_snake_case)] 25 | struct Response { 26 | code: i64, 27 | data: Vec, 28 | } 29 | 30 | // See https://github.com/ZBFuture/docs/blob/main/API%20V2%20_en.md#71-trading-pair 31 | fn fetch_swap_markets(url: &str) -> Vec { 32 | let txt = http_get(url).unwrap(); 33 | match serde_json::from_str::(&txt) { 34 | Ok(resp) => { 35 | if resp.code == 1000 { 36 | resp.data 37 | } else { 38 | Vec::new() 39 | } 40 | } 41 | Err(_) => Vec::new(), 42 | } 43 | } 44 | 45 | fn fetch_swap_markets_all() -> Vec { 46 | fetch_swap_markets("https://fapi.zb.com/Server/api/v2/config/marketList") 47 | // let qc_markets = fetch_swap_markets("https://fapi.zb.com/qc/Server/api/v2/config/marketList"); 48 | // usdt_markets 49 | // .into_iter() 50 | // .chain(qc_markets.into_iter()) 51 | // .collect() 52 | } 53 | 54 | #[test] 55 | #[ignore = "zb.com has shut down"] 56 | fn verify_spot_symbols() { 57 | assert_eq!(Some("BTC/USDT".to_string()), normalize_pair("btc_usdt", EXCHANGE_NAME)); 58 | assert_eq!(Some("BTC/USDT".to_string()), normalize_pair("btcusdt", EXCHANGE_NAME)); 59 | } 60 | 61 | #[test] 62 | #[ignore = "zb.com has shut down"] 63 | fn verify_swap_symbols() { 64 | let markets = fetch_swap_markets_all(); 65 | for market in markets.iter() { 66 | let pair = normalize_pair(&market.symbol, EXCHANGE_NAME).unwrap(); 67 | let pair_expected = format!("{}/{}", &market.buyerCurrencyName, &market.sellerCurrencyName); 68 | 69 | assert_eq!(pair.as_str(), pair_expected); 70 | 71 | let market_type = get_market_type(&market.symbol, EXCHANGE_NAME, None); 72 | assert_eq!(MarketType::LinearSwap, market_type); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /crypto-pair/tests/zbg.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | use crypto_market_type::MarketType; 4 | use crypto_pair::{get_market_type, normalize_currency, normalize_pair}; 5 | use serde::{Deserialize, Serialize}; 6 | use serde_json::Value; 7 | use std::collections::HashMap; 8 | use utils::http_get; 9 | 10 | const EXCHANGE_NAME: &str = "zbg"; 11 | 12 | #[derive(Serialize, Deserialize)] 13 | #[serde(rename_all = "kebab-case")] 14 | struct SpotMarket { 15 | symbol: String, 16 | base_currency: String, 17 | quote_currency: String, 18 | #[serde(flatten)] 19 | extra: HashMap, 20 | } 21 | 22 | #[derive(Serialize, Deserialize)] 23 | struct ResMsg { 24 | message: String, 25 | method: Option, 26 | code: String, 27 | } 28 | 29 | #[derive(Serialize, Deserialize)] 30 | #[allow(non_snake_case)] 31 | struct Response { 32 | datas: Vec, 33 | resMsg: ResMsg, 34 | } 35 | 36 | // See https://zbgapi.github.io/docs/spot/v1/en/#public-get-all-supported-trading-symbols 37 | fn fetch_spot_markets_raw() -> Vec { 38 | let txt = http_get("https://www.zbg.com/exchange/api/v1/common/symbols").unwrap(); 39 | let resp = serde_json::from_str::>(&txt).unwrap(); 40 | resp.datas 41 | } 42 | 43 | #[ignore = "Always get HTTP 503 on Github action runners"] 44 | #[test] 45 | fn verify_spot_symbols() { 46 | let markets = fetch_spot_markets_raw(); 47 | for market in markets.iter() { 48 | let pair = normalize_pair(&market.symbol, EXCHANGE_NAME).unwrap(); 49 | let pair_expected = format!( 50 | "{}/{}", 51 | normalize_currency(&market.base_currency, EXCHANGE_NAME), 52 | normalize_currency(&market.quote_currency, EXCHANGE_NAME) 53 | ); 54 | 55 | assert_eq!(pair.as_str(), pair_expected); 56 | assert_eq!(MarketType::Spot, get_market_type(&market.symbol, EXCHANGE_NAME, None)); 57 | } 58 | } 59 | 60 | #[derive(Serialize, Deserialize)] 61 | #[allow(non_snake_case)] 62 | struct SwapMarket { 63 | symbol: String, 64 | currencyName: String, 65 | commodityName: Option, 66 | #[serde(flatten)] 67 | extra: HashMap, 68 | } 69 | 70 | // See https://zbgapi.github.io/docs/future/v1/en/#public-get-contracts 71 | fn fetch_swap_markets_raw() -> Vec { 72 | let txt = http_get("https://www.zbg.com/exchange/api/v1/future/common/contracts").unwrap(); 73 | let resp = serde_json::from_str::>(&txt).unwrap(); 74 | resp.datas 75 | } 76 | 77 | #[ignore = "prone to HTTP request timeout"] 78 | #[test] 79 | fn verify_swap_symbols() { 80 | let markets = fetch_swap_markets_raw(); 81 | for market in markets.iter().filter(|x| x.commodityName.is_some()) { 82 | let pair = normalize_pair(&market.symbol, EXCHANGE_NAME).unwrap(); 83 | let pair_expected = if market.symbol.ends_with("_USD-R") { 84 | format!( 85 | "{}/{}", 86 | normalize_currency(&market.currencyName, EXCHANGE_NAME), 87 | normalize_currency(&market.commodityName.clone().unwrap(), EXCHANGE_NAME) 88 | ) 89 | } else { 90 | format!( 91 | "{}/{}", 92 | normalize_currency(&market.commodityName.clone().unwrap(), EXCHANGE_NAME), 93 | normalize_currency(&market.currencyName, EXCHANGE_NAME) 94 | ) 95 | }; 96 | 97 | assert_eq!(pair.as_str(), pair_expected); 98 | 99 | let market_type = get_market_type(&market.symbol, EXCHANGE_NAME, None); 100 | assert!(market_type == MarketType::LinearSwap || market_type == MarketType::InverseSwap); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | version = "Two" 3 | use_small_heuristics = "Max" 4 | newline_style = "Unix" 5 | wrap_comments = true 6 | format_generated_files = false 7 | imports_granularity="Crate" 8 | --------------------------------------------------------------------------------