├── .gitignore ├── .travis.yml ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── Cargo.toml ├── README.md └── src │ ├── authenticated_channels.rs │ ├── private_endpoints.rs │ ├── public_channels.rs │ └── public_endpoints.rs └── src ├── account.rs ├── api.rs ├── auth.rs ├── book.rs ├── candles.rs ├── client.rs ├── currency.rs ├── errors.rs ├── events.rs ├── ledger.rs ├── lib.rs ├── orders.rs ├── pairs.rs ├── precision.rs ├── ticker.rs ├── trades.rs └── websockets.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | 4 | *.iml 5 | *.fmt 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | sudo: false 3 | dist: trusty 4 | matrix: 5 | fast_finish: true 6 | include: 7 | - rust: nightly 8 | - rust: stable 9 | 10 | cache: 11 | apt: true 12 | directories: 13 | - target/debug/deps 14 | - target/debug/build -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bitfinex" 3 | version = "0.5.0" 4 | license = "MIT OR Apache-2.0" 5 | authors = ["Flavio Oliveira "] 6 | 7 | description = "Rust Library for the Bitfinex API" 8 | keywords = ["cryptocurrency", "trading", "bitcoin"] 9 | documentation = "https://docs.rs/crate/bitfinex/" 10 | repository = "https://github.com/wisespace-io/bitfinex-rs" 11 | readme = "README.md" 12 | 13 | [badges] 14 | travis-ci = { repository = "wisespace-io/bitfinex-rs" } 15 | 16 | [lib] 17 | name = "bitfinex" 18 | path = "src/lib.rs" 19 | 20 | [dependencies] 21 | hex = "0.4" 22 | serde = "1.0" 23 | serde_json = "1.0" 24 | serde_derive = "1.0" 25 | error-chain = "0.12" 26 | ring = "0.16" 27 | reqwest = "0.9" 28 | url = "2.1" 29 | log = "0.3" 30 | tungstenite = "0.9" 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Flavio Oliveira (flavio@wisespace.io) 2 | 3 | Licensed under either of 4 | 5 | * Apache License, Version 2.0, (http://www.apache.org/licenses/LICENSE-2.0) 6 | * MIT license (http://opensource.org/licenses/MIT) 7 | 8 | at your option. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Crates.io](https://img.shields.io/crates/v/bitfinex.svg)](https://crates.io/crates/bitfinex) 2 | [![Build Status](https://travis-ci.org/wisespace-io/bitfinex-rs.png?branch=master)](https://travis-ci.org/wisespace-io/bitfinex-rs) 3 | [![MIT licensed](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE-MIT) 4 | [![Apache-2.0 licensed](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](./LICENSE-APACHE) 5 | 6 | # bitfinex-rs 7 | 8 | Unofficial Rust Library for the [Bitfinex API V2](https://bitfinex.readme.io/v2/docs/getting-started) 9 | 10 | # Risk Warning 11 | 12 | It is a personal project, use at your own risk. I will not be responsible for your investment losses. 13 | Cryptocurrency investment is subject to high market risk. 14 | 15 | # Usage 16 | 17 | Add this to your Cargo.toml 18 | 19 | ```toml 20 | [dependencies] 21 | bitfinex = { git = "https://github.com/wisespace-io/bitfinex-rs.git" } 22 | ``` 23 | 24 | ## PUBLIC ENDPOINTS 25 | 26 | Ticker, Trades, Book, Candles, see [example](https://github.com/wisespace-io/bitfinex-rs/blob/master/examples/src/public_endpoints.rs) 27 | 28 | ## PRIVATE ENDPOINTS 29 | 30 | Wallets, Orders, Trades, Margin and Funding Info, see [example](https://github.com/wisespace-io/bitfinex-rs/blob/master/examples/src/private_endpoints.rs) 31 | 32 | ## PUBLIC CHANNELS (WEBSOCKETS) 33 | 34 | Ticker, Trades, Book, Raw Book, Candles, see [example](https://github.com/wisespace-io/bitfinex-rs/blob/master/examples/src/public_channels.rs) 35 | 36 | # Other Exchanges 37 | 38 | If you use [Binance](https://www.binance.com/) check out my [Rust library for Binance API](https://github.com/wisespace-io/binance-rs) 39 | -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bitfinex-app" 3 | version = "0.4.0" 4 | authors = ["Flavio Oliveira "] 5 | 6 | [[bin]] 7 | name = "private_endpoints" 8 | path = "src/private_endpoints.rs" 9 | 10 | [[bin]] 11 | name = "public_endpoints" 12 | path = "src/public_endpoints.rs" 13 | 14 | [[bin]] 15 | name = "public_channels" 16 | path = "src/public_channels.rs" 17 | 18 | [[bin]] 19 | name = "authenticated_channels" 20 | path = "src/authenticated_channels.rs" 21 | 22 | [dependencies] 23 | bitfinex = { path = "../" } 24 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ## Private Endpoints 4 | 5 | cargo run --release --bin "private_endpoints" 6 | 7 | ## Public Endpoints 8 | 9 | cargo run --release --bin "public_endpoints" 10 | 11 | ## Public Channels 12 | 13 | cargo run --release --bin "public_channels" -------------------------------------------------------------------------------- /examples/src/authenticated_channels.rs: -------------------------------------------------------------------------------- 1 | extern crate bitfinex; 2 | 3 | use bitfinex::{ errors::*, events::*, websockets::* }; 4 | 5 | struct WebSocketHandler; 6 | 7 | impl EventHandler for WebSocketHandler { 8 | fn on_connect(&mut self, event: NotificationEvent) { 9 | if let NotificationEvent::Info(info) = event { 10 | println!("Platform status: {:?}, Version {}", info.platform, info.version); 11 | } 12 | } 13 | 14 | fn on_auth(&mut self, event: NotificationEvent) { 15 | if let NotificationEvent::Auth(auth) = event { 16 | println!("Auth {}: {:?}", auth.status, auth.msg); 17 | } 18 | } 19 | 20 | fn on_subscribed(&mut self, _event: NotificationEvent) {} 21 | 22 | fn on_data_event(&mut self, _event: DataEvent) {} 23 | 24 | fn on_error(&mut self, message: Error) { 25 | println!("{:?}", message); 26 | } 27 | } 28 | 29 | fn main() { 30 | let api_key = "YOUR_API_KEY"; 31 | let secret_key = "YOUR_SECRET_KEY"; 32 | let mut web_socket: WebSockets = WebSockets::new(); 33 | 34 | web_socket.add_event_handler(WebSocketHandler); 35 | web_socket.connect().unwrap(); // check error 36 | 37 | web_socket.auth(api_key, secret_key, false, &[]).unwrap(); 38 | 39 | // TODO: Handle authenticated channels 40 | 41 | web_socket.event_loop().unwrap(); // check error 42 | } 43 | -------------------------------------------------------------------------------- /examples/src/private_endpoints.rs: -------------------------------------------------------------------------------- 1 | extern crate bitfinex; 2 | 3 | use std::time::SystemTime; 4 | use bitfinex::api::*; 5 | use bitfinex::pairs::*; 6 | use bitfinex::currency::*; 7 | 8 | fn main() { 9 | let api_key = Some("YOUR_API_KEY".into()); 10 | let secret_key = Some("YOUR_SECRET_KEY".into()); 11 | let api = Bitfinex::new(api_key, secret_key); 12 | 13 | // ORDERS 14 | match api.orders.active_orders() { 15 | Ok(orders) => { 16 | for order in &orders { 17 | println!("Active orders => Symbol: {:?} amount: {:?} price: {:?}", order.symbol, order.amount, order.price); 18 | } 19 | }, 20 | Err(e) => println!("Error: {}", e), 21 | } 22 | 23 | let order_history = api.orders.history(BTCUSD.to_owned()); // Use None if you don't want a pair 24 | match order_history { 25 | Ok(orders) => { 26 | for order in &orders { 27 | println!("Order History => Symbol: {:?} amount: {:?} price: {:?}", order.symbol, order.amount, order.price); 28 | } 29 | }, 30 | Err(e) => println!("Error: {}", e), 31 | } 32 | 33 | // WALLET 34 | match api.account.get_wallets() { 35 | Ok(wallets) => { 36 | for wallet in &wallets { 37 | println!("Wallet => Currency: {:?} Balance: {:?}", wallet.currency, wallet.balance); 38 | } 39 | }, 40 | Err(e) => println!("Error: {}", e), 41 | } 42 | 43 | // MARGIN INFO 44 | match api.account.margin_base() { 45 | Ok(info) => { 46 | println!("Margin Base Info => Profile/Loss: {:?}", info.margin.user_profit_loss); 47 | }, 48 | Err(e) => println!("Error: {}", e), 49 | } 50 | 51 | match api.account.margin_symbol(ETHUSD) { 52 | Ok(info) => { 53 | println!("Margin Symbol Info => Gross Balance: {:?}", info.margin.gross_balance); 54 | }, 55 | Err(e) => println!("Error: {}", e), 56 | } 57 | 58 | // FUNDING INFO 59 | match api.account.funding_info(USD) { 60 | Ok(info) => { 61 | println!("Funding Info => Yield Loan: {:?} Yield Lend: {:?}", info.funding.yield_loan, info.funding.yield_lend); 62 | }, 63 | Err(e) => println!("Error: {}", e), 64 | } 65 | 66 | // LEDGER 67 | let now = SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis(); 68 | match api.ledger.get_history(USD, now - 3600000, now, 5) { 69 | Ok(entries) => { 70 | for entry in &entries { 71 | println!("Ledger Entry => {}{} => {}: {}", entry.amount, entry.currency, entry.balance, entry.description); 72 | } 73 | }, 74 | Err(e) => println!("Error: {}", e), 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /examples/src/public_channels.rs: -------------------------------------------------------------------------------- 1 | extern crate bitfinex; 2 | 3 | use bitfinex::{ errors::*, events::*, websockets::* }; 4 | use bitfinex::{ pairs::*, precision::* }; 5 | 6 | struct WebSocketHandler; 7 | 8 | impl EventHandler for WebSocketHandler { 9 | fn on_connect(&mut self, event: NotificationEvent) { 10 | if let NotificationEvent::Info(info) = event { 11 | println!("Platform status: {:?}, Version {}", info.platform, info.version); 12 | } 13 | } 14 | 15 | fn on_auth(&mut self, _event: NotificationEvent) {} 16 | 17 | fn on_subscribed(&mut self, event: NotificationEvent) { 18 | if let NotificationEvent::TradingSubscribed(msg) = event { 19 | println!("Subscribed: {:?}", msg); 20 | } else if let NotificationEvent::CandlesSubscribed(msg) = event { 21 | println!("Subscribed: {:?}", msg); 22 | } else if let NotificationEvent::RawBookSubscribed(msg) = event { 23 | println!("Subscribed: {:?}", msg); 24 | } 25 | } 26 | 27 | fn on_data_event(&mut self, event: DataEvent) { 28 | if let DataEvent::TickerTradingEvent(channel, trading) = event { 29 | println!("Ticker Trading ({}) - Bid {:?}, Ask: {}", channel, trading.bid, trading.ask); 30 | } else if let DataEvent::RawBookEvent(channel, raw_book) = event { 31 | println!("Raw book ({}) - Price {:?}, Amount: {}", channel, raw_book.price, raw_book.amount); 32 | } else if let DataEvent::TradesTradingUpdateEvent(channel, pair, trading) = event { 33 | println!("Trade update ({}) - Id: {}, Time: {}, Price: {}, Amount: {}", channel, trading.id, trading.mts, trading.price, trading.amount); 34 | } 35 | // ... Add for all events you have subscribed (Trades, Books, ...) 36 | } 37 | 38 | fn on_error(&mut self, message: Error) { 39 | println!("{:?}", message); 40 | } 41 | } 42 | 43 | fn main() { 44 | let mut web_socket: WebSockets = WebSockets::new(); 45 | 46 | web_socket.add_event_handler(WebSocketHandler); 47 | web_socket.connect().unwrap(); // check error 48 | 49 | // TICKER 50 | web_socket.subscribe_ticker(BTCUSD, EventType::Trading); 51 | 52 | // TRADES 53 | web_socket.subscribe_trades(BTCUSD, EventType::Trading); 54 | 55 | // BOOKS 56 | web_socket.subscribe_books(BTCUSD, EventType::Trading, P0, "F0", 25); 57 | 58 | // RAW BOOKS 59 | web_socket.subscribe_raw_books(BTCUSD, EventType::Trading); 60 | 61 | // CANDLES 62 | web_socket.subscribe_candles(BTCUSD, "1m"); 63 | 64 | web_socket.event_loop().unwrap(); // check error 65 | } 66 | -------------------------------------------------------------------------------- /examples/src/public_endpoints.rs: -------------------------------------------------------------------------------- 1 | extern crate bitfinex; 2 | 3 | use bitfinex::api::*; 4 | use bitfinex::pairs::*; 5 | use bitfinex::currency::*; 6 | use bitfinex::precision::*; 7 | 8 | fn main() { 9 | let api = Bitfinex::new(None, None); 10 | 11 | // TICKER 12 | let trading_pair = api.ticker.trading_pair(ETHUSD); 13 | match trading_pair { 14 | Ok(answer) => println!("bid: {:?} ask: {:?}", answer.bid, answer.ask), 15 | Err(e) => println!("Error: {}", e), 16 | } 17 | 18 | let funding_currency = api.ticker.funding_currency(USD); 19 | match funding_currency { 20 | Ok(answer) => println!("bid: {:?} ask: {:?}", answer.bid, answer.ask), 21 | Err(e) => println!("Error: {}", e), 22 | } 23 | 24 | // TRADES 25 | let trading_pairs = api.trades.trading_pair(ETHUSD); 26 | match trading_pairs { 27 | Ok(trades) => { 28 | for trade in &trades { 29 | println!("Trading => amount: {:?} price: {:?}", trade.amount, trade.price); 30 | } 31 | }, 32 | Err(e) => println!("Error: {}", e), 33 | } 34 | 35 | let funding_currency = api.trades.funding_currency(USD); 36 | match funding_currency { 37 | Ok(trades) => { 38 | for trade in &trades { 39 | println!("Funding => amount: {:?} price: {:?}", trade.amount, trade.price); 40 | } 41 | }, 42 | Err(e) => println!("Error: {}", e), 43 | } 44 | 45 | // BOOK 46 | let trading_pairs = api.book.trading_pair(ETHUSD, P0); 47 | match trading_pairs { 48 | Ok(books) => { 49 | for book in &books { 50 | println!("Trading => price: {:?} amount: {:?}", book.price, book.amount); 51 | } 52 | }, 53 | Err(e) => println!("Error: {}", e), 54 | } 55 | 56 | let funding_currency = api.book.funding_currency(USD, P0); 57 | match funding_currency { 58 | Ok(books) => { 59 | for book in &books { 60 | println!("Funding => rate: {:?} amount: {:?}", book.rate, book.amount); 61 | } 62 | }, 63 | Err(e) => println!("Error: {}", e), 64 | } 65 | 66 | // CANDLES 67 | let last = api.candles.last(ETHUSD, "1m"); 68 | match last { 69 | Ok(answer) => println!("Candle Last => High: {:?} low: {:?}", answer.high, answer.low), 70 | Err(e) => println!("Error: {}", e), 71 | } 72 | 73 | let history = api.candles.history(ETHUSD, "12h", &CandleHistoryParams::new()); 74 | match history { 75 | Ok(candles) => { 76 | for candle in &candles { 77 | println!("Candle History => High: {:?} Low: {:?}", candle.high, candle.low); 78 | } 79 | }, 80 | Err(e) => println!("Error: {}", e), 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/account.rs: -------------------------------------------------------------------------------- 1 | use client::*; 2 | use errors::*; 3 | use serde_json::from_str; 4 | 5 | #[derive(Serialize, Deserialize)] 6 | pub struct Wallet { 7 | pub wallet_type: String, 8 | pub currency: String, 9 | pub balance: f64, 10 | pub unsettled_interest: f64, 11 | pub balance_available: Option 12 | } 13 | 14 | #[derive(Serialize, Deserialize)] 15 | pub struct MarginBase { 16 | key: String, 17 | pub margin: Base 18 | } 19 | 20 | #[derive(Serialize, Deserialize)] 21 | pub struct Base { 22 | pub user_profit_loss: f64, 23 | pub user_swaps: f64, 24 | pub margin_balance: f64, 25 | pub margin_net: f64 26 | } 27 | 28 | #[derive(Serialize, Deserialize)] 29 | pub struct MarginSymbol { 30 | key: String, 31 | symbol: String, 32 | pub margin: Symbol 33 | } 34 | 35 | #[derive(Serialize, Deserialize)] 36 | pub struct Symbol { 37 | pub tradable_balance: f64, 38 | pub gross_balance: f64, 39 | pub buy: f64, 40 | pub sell: f64, 41 | 42 | #[serde(skip_serializing)] 43 | _placeholder_1: Option, 44 | #[serde(skip_serializing)] 45 | _placeholder_2: Option, 46 | #[serde(skip_serializing)] 47 | _placeholder_3: Option, 48 | #[serde(skip_serializing)] 49 | _placeholder_4: Option 50 | } 51 | 52 | #[derive(Serialize, Deserialize)] 53 | pub struct FundingInfo { 54 | key: String, 55 | symbol: String, 56 | pub funding: Funding 57 | } 58 | 59 | #[derive(Serialize, Deserialize)] 60 | pub struct Funding { 61 | pub yield_loan: f64, 62 | pub yield_lend: f64, 63 | pub duration_loan: f64, 64 | pub duration_lend: f64 65 | } 66 | 67 | #[derive(Clone)] 68 | pub struct Account { 69 | client: Client, 70 | } 71 | 72 | impl Account { 73 | pub fn new(api_key: Option, secret_key: Option) -> Self { 74 | Account { 75 | client: Client::new(api_key, secret_key), 76 | } 77 | } 78 | 79 | pub fn get_wallets(&self) -> Result> { 80 | let payload: String = format!("{}", "{}"); 81 | let data = self.client.post_signed("wallets".into(), payload)?; 82 | 83 | let wallets: Vec = from_str(data.as_str())?; 84 | 85 | Ok(wallets) 86 | } 87 | 88 | pub fn margin_base(&self) -> Result 89 | { 90 | let payload: String = format!("{}", "{}"); 91 | 92 | let data = self.client.post_signed("info/margin/base".into(), payload)?; 93 | 94 | let margin: MarginBase = from_str(data.as_str())?; 95 | 96 | Ok(margin) 97 | } 98 | 99 | pub fn margin_symbol(&self, key: S) -> Result 100 | where S: Into 101 | { 102 | let payload: String = format!("{}", "{}"); 103 | let request: String = format!("info/margin/t{}", key.into()); 104 | 105 | let data = self.client.post_signed(request, payload)?; 106 | 107 | let margin: MarginSymbol = from_str(data.as_str())?; 108 | 109 | Ok(margin) 110 | } 111 | 112 | pub fn funding_info(&self, key: S) -> Result 113 | where S: Into 114 | { 115 | let payload: String = format!("{}", "{}"); 116 | let request: String = format!("info/funding/f{}", key.into()); 117 | 118 | let data = self.client.post_signed(request, payload)?; 119 | 120 | let info: FundingInfo = from_str(data.as_str())?; 121 | 122 | Ok(info) 123 | } 124 | } -------------------------------------------------------------------------------- /src/api.rs: -------------------------------------------------------------------------------- 1 | use book::*; 2 | use ticker::*; 3 | use trades::*; 4 | use candles::*; 5 | use orders::*; 6 | use account::*; 7 | use ledger::*; 8 | 9 | #[derive(Clone)] 10 | pub struct Bitfinex { 11 | pub book: Book, 12 | pub ticker: Ticker, 13 | pub trades: Trades, 14 | pub candles: Candles, 15 | pub orders: Orders, 16 | pub account: Account, 17 | pub ledger: Ledger 18 | } 19 | 20 | impl Bitfinex { 21 | pub fn new(api_key: Option, secret_key: Option) -> Self { 22 | Bitfinex { 23 | book: Book::new(), 24 | ticker: Ticker::new(), 25 | trades: Trades::new(), 26 | candles: Candles::new(), 27 | orders: Orders::new(api_key.clone(), secret_key.clone()), 28 | account: Account::new(api_key.clone(), secret_key.clone()), 29 | ledger: Ledger::new(api_key.clone(), secret_key.clone()), 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/auth.rs: -------------------------------------------------------------------------------- 1 | use errors::*; 2 | use hex::encode; 3 | use ring::hmac; 4 | use std::time::{SystemTime, UNIX_EPOCH}; 5 | 6 | pub fn sign_payload(secret: &[u8], payload: &[u8]) -> Result { 7 | let signed_key = hmac::Key::new(hmac::HMAC_SHA384, secret); 8 | let signature = encode(hmac::sign(&signed_key, payload).as_ref()); 9 | 10 | Ok(signature) 11 | } 12 | 13 | pub fn generate_nonce() -> Result { 14 | let start = SystemTime::now(); 15 | let since_epoch = start.duration_since(UNIX_EPOCH)?; 16 | 17 | let timestamp = since_epoch.as_secs() * 1000 + since_epoch.subsec_nanos() as u64 / 1_000_000; 18 | 19 | Ok((timestamp + 1).to_string()) 20 | } 21 | -------------------------------------------------------------------------------- /src/book.rs: -------------------------------------------------------------------------------- 1 | use client::*; 2 | use errors::*; 3 | use serde_json::from_str; 4 | 5 | #[derive(Serialize, Deserialize, Debug)] 6 | pub struct TradingPair { 7 | pub price: f64, 8 | pub count: i64, 9 | pub amount: f64, 10 | } 11 | 12 | #[derive(Serialize, Deserialize, Debug)] 13 | pub struct FundingCurrency { 14 | pub rate: f64, 15 | pub period: f64, 16 | pub count: i64, 17 | pub amount: f64, 18 | } 19 | 20 | #[derive(Clone)] 21 | pub struct Book { 22 | client: Client, 23 | } 24 | 25 | // Trading: if AMOUNT > 0 then bid else ask; Funding: if AMOUNT < 0 then bid else ask; 26 | #[derive(Serialize, Deserialize, Debug)] 27 | pub struct RawBook { 28 | pub order_id: i64, 29 | pub price: f64, 30 | pub amount: f64, 31 | } 32 | 33 | impl Book { 34 | pub fn new() -> Self { 35 | Book { client: Client::new(None, None) } 36 | } 37 | 38 | pub fn funding_currency(&self, symbol: S, precision: S) -> Result> 39 | where S: Into 40 | { 41 | let endpoint: String = format!("book/f{}/{}", symbol.into(), precision.into()); 42 | let data = self.client.get(endpoint, String::new())?; 43 | 44 | let book: Vec = from_str(data.as_str())?; 45 | 46 | Ok(book) 47 | } 48 | 49 | pub fn trading_pair(&self, symbol: S, precision: S) -> Result> 50 | where S: Into 51 | { 52 | let endpoint: String = format!("book/t{}/{}", symbol.into(), precision.into()); 53 | let data = self.client.get(endpoint, String::new())?; 54 | 55 | let book: Vec = from_str(data.as_str())?; 56 | 57 | Ok(book) 58 | } 59 | } -------------------------------------------------------------------------------- /src/candles.rs: -------------------------------------------------------------------------------- 1 | use client::*; 2 | use errors::*; 3 | use serde_json::from_str; 4 | 5 | #[derive(Debug, Clone, Default)] 6 | pub struct CandleHistoryParams { 7 | /// Number of candles requested (Max: 10000) 8 | pub limit: Option, 9 | 10 | /// Filter start (ms) 11 | pub start: Option, 12 | 13 | /// Filter end (ms) 14 | pub end: Option, 15 | 16 | /// Sorts the results from old > new 17 | pub sort: Option, 18 | } 19 | 20 | impl CandleHistoryParams { 21 | pub fn new() -> Self { 22 | Self { 23 | limit: Some(120), 24 | sort: Some(false), 25 | start: None, 26 | end: None, 27 | } 28 | } 29 | 30 | pub fn to_query(&self) -> String { 31 | format!("{}={}&{}={}&{}={}&{}={}", 32 | "limit", self.limit 33 | .map(|a| a.to_string()) 34 | .unwrap_or("".into()), 35 | "start", self.start 36 | .map(|a| a.to_string()) 37 | .unwrap_or("".into()), 38 | "end", self.end 39 | .map(|a| a.to_string()) 40 | .unwrap_or("".into()), 41 | "sort", self.sort 42 | .map(|a| if a { "1" } else { "0" }) 43 | .unwrap_or("".into()), 44 | ) 45 | } 46 | } 47 | 48 | #[derive(Serialize, Deserialize, Debug)] 49 | pub struct Candle { 50 | pub timestamp: i64, 51 | pub open: f64, 52 | pub close: f64, 53 | pub high: f64, 54 | pub low: f64, 55 | pub volume: f64 56 | } 57 | 58 | #[derive(Clone)] 59 | pub struct Candles { 60 | client: Client, 61 | } 62 | 63 | impl Candles { 64 | pub fn new() -> Self { 65 | Candles { 66 | client: Client::new(None, None), 67 | } 68 | } 69 | 70 | pub fn last(&self, symbol: S, timeframe: S) -> Result 71 | where S: Into 72 | { 73 | let endpoint: String = format!("candles/trade:{}:t{}/last", timeframe.into(), symbol.into()); 74 | let data = self.client.get(endpoint, String::new())?; 75 | 76 | let history: Candle = from_str(data.as_str())?; 77 | 78 | Ok(history) 79 | } 80 | 81 | pub fn history( 82 | &self, 83 | symbol: S, 84 | timeframe: S, 85 | params: &CandleHistoryParams, 86 | ) -> Result> 87 | where S: Into 88 | { 89 | let endpoint: String = format!("candles/trade:{}:t{}/hist", timeframe.into(), symbol.into()); 90 | let data = self.client.get(endpoint, params.to_query())?; 91 | 92 | let history: Vec = from_str(data.as_str())?; 93 | 94 | Ok(history) 95 | } 96 | } -------------------------------------------------------------------------------- /src/client.rs: -------------------------------------------------------------------------------- 1 | use errors::*; 2 | use auth; 3 | use reqwest; 4 | use reqwest::{StatusCode, Response}; 5 | use reqwest::header::{HeaderMap, HeaderName, HeaderValue, USER_AGENT, CONTENT_TYPE}; 6 | use std::io::Read; 7 | use serde::Serialize; 8 | 9 | static API1_HOST : &'static str = "https://api.bitfinex.com/v2/"; 10 | static API_SIGNATURE_PATH : &'static str = "/api/v2/auth/r/"; 11 | static NO_PARAMS: &'static [(); 0] = &[]; 12 | 13 | #[derive(Clone)] 14 | pub struct Client { 15 | api_key: String, 16 | secret_key: String 17 | } 18 | 19 | impl Client { 20 | pub fn new(api_key: Option, secret_key: Option) -> Self { 21 | Client { 22 | api_key : api_key.unwrap_or("".into()), 23 | secret_key : secret_key.unwrap_or("".into()) 24 | } 25 | } 26 | 27 | pub fn get(&self, endpoint: String, request: String) -> Result { 28 | let mut url: String = format!("{}{}", API1_HOST, endpoint); 29 | if !request.is_empty() { 30 | url.push_str(format!("?{}", request).as_str()); 31 | } 32 | 33 | let response = reqwest::get(url.as_str())?; 34 | 35 | self.handler(response) 36 | } 37 | 38 | pub fn post_signed(&self, request: String, payload: String) -> Result { 39 | self.post_signed_params(request, payload, NO_PARAMS) 40 | } 41 | 42 | pub fn post_signed_params( 43 | &self, 44 | request: String, 45 | payload: String, 46 | params: &P, 47 | ) -> Result { 48 | let url: String = format!("{}auth/r/{}", API1_HOST, request); 49 | 50 | let client = reqwest::Client::new(); 51 | let response = client.post(url.as_str()) 52 | .headers(self.build_headers(request, payload.clone())?) 53 | .body(payload) 54 | .query(params) 55 | .send()?; 56 | 57 | self.handler(response) 58 | } 59 | 60 | fn build_headers(&self, request: String, payload: String) -> Result { 61 | let nonce: String = auth::generate_nonce()?; 62 | let signature_path: String = format!("{}{}{}{}", API_SIGNATURE_PATH, request, nonce, payload); 63 | 64 | let signature = auth::sign_payload(self.secret_key.as_bytes(), signature_path.as_bytes())?; 65 | 66 | let mut headers = HeaderMap::new(); 67 | headers.insert(USER_AGENT, HeaderValue::from_static("bitfinex-rs")); 68 | headers.insert(HeaderName::from_static("bfx-nonce"), HeaderValue::from_str(nonce.as_str())?); 69 | headers.insert(HeaderName::from_static("bfx-apikey"), HeaderValue::from_str(self.api_key.as_str())?); 70 | headers.insert(HeaderName::from_static("bfx-signature"), HeaderValue::from_str(signature.as_str())?); 71 | headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); 72 | 73 | Ok(headers) 74 | } 75 | 76 | fn handler(&self, mut response: Response) -> Result { 77 | match response.status() { 78 | StatusCode::OK => { 79 | let mut body = String::new(); 80 | response.read_to_string(&mut body)?; 81 | return Ok(body); 82 | }, 83 | StatusCode::INTERNAL_SERVER_ERROR => { 84 | bail!("Internal Server Error"); 85 | } 86 | StatusCode::SERVICE_UNAVAILABLE => { 87 | bail!("Service Unavailable"); 88 | } 89 | StatusCode::UNAUTHORIZED => { 90 | bail!("Unauthorized"); 91 | } 92 | StatusCode::BAD_REQUEST => { 93 | bail!(format!("Bad Request: {:?}", response)); 94 | } 95 | s => { 96 | bail!(format!("Received response: {:?}", s)); 97 | } 98 | }; 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/currency.rs: -------------------------------------------------------------------------------- 1 | pub static USD : &'static str = "USD"; 2 | pub static EUR : &'static str = "EUR"; -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use std; 2 | use reqwest; 3 | use url; 4 | use serde_json; 5 | use tungstenite; 6 | 7 | error_chain! { 8 | types { 9 | Error, ErrorKind, ResultExt, Result; 10 | } 11 | 12 | errors { 13 | Internal(t: String) { 14 | description("invalid toolchain name") 15 | display("invalid toolchain name: '{}'", t) 16 | } 17 | } 18 | 19 | foreign_links { 20 | ReqError(reqwest::Error); 21 | InvalidHeaderError(reqwest::header::InvalidHeaderValue); 22 | IoError(std::io::Error); 23 | ParseFloatError(std::num::ParseFloatError); 24 | UrlParserError(url::ParseError); 25 | Json(serde_json::Error); 26 | Tungstenite(tungstenite::Error); 27 | TimestampError(std::time::SystemTimeError); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/events.rs: -------------------------------------------------------------------------------- 1 | use ticker::*; 2 | use candles::Candle; 3 | use trades::{TradingPair as TradesTradingPair, FundingCurrency as TradesFundingCurrency}; 4 | use book::{TradingPair as BookTradingPair, FundingCurrency as BookFundingCurrency, RawBook}; 5 | 6 | #[derive(Debug, Deserialize)] 7 | #[serde(untagged)] 8 | #[serde(rename_all = "camelCase")] 9 | pub enum NotificationEvent { 10 | Auth(AuthMessage), 11 | Info(InfoMessage), 12 | TradingSubscribed(TradingSubscriptionMessage), 13 | FundingSubscribed(FundingSubscriptionMessage), 14 | CandlesSubscribed(CandlesSubscriptionMessage), 15 | RawBookSubscribed(RawBookSubscriptionMessage), 16 | } 17 | 18 | #[derive(Debug, Deserialize)] 19 | #[serde(untagged)] 20 | pub enum DataEvent { 21 | TickerTradingEvent (i32, TradingPair), 22 | TickerFundingEvent (i32, FundingCurrency), 23 | TradesTradingSnapshotEvent (i32, Vec), 24 | TradesTradingUpdateEvent (i32, String, TradesTradingPair), 25 | TradesFundingSnapshotEvent (i32, Vec), 26 | TradesFundingUpdateEvent (i32, String, TradesFundingCurrency), 27 | BookTradingSnapshotEvent (i32, Vec), 28 | BookTradingUpdateEvent (i32, BookTradingPair), 29 | BookFundingSnapshotEvent (i32, Vec), 30 | BookFundingUpdateEvent (i32, BookFundingCurrency), 31 | RawBookEvent (i32, RawBook), 32 | RawBookUpdateEvent (i32, Vec), 33 | CandlesSnapshotEvent (i32, Vec), 34 | CandlesUpdateEvent (i32, Candle), 35 | HeartbeatEvent (i32, String) 36 | } 37 | 38 | #[serde(rename_all = "camelCase")] 39 | #[derive(Debug, Deserialize)] 40 | pub struct AuthMessage { 41 | pub event: String, 42 | pub status: String, 43 | pub chan_id: u32, 44 | pub code: Option, 45 | pub msg: Option, 46 | pub user_id: Option, 47 | pub auth_id: Option, 48 | } 49 | 50 | impl AuthMessage { 51 | pub fn is_ok(&self) -> bool { 52 | self.status == "OK" 53 | } 54 | } 55 | 56 | #[serde(rename_all = "camelCase")] 57 | #[derive(Debug, Deserialize)] 58 | pub struct InfoMessage { 59 | pub event: String, 60 | pub version: u16, 61 | pub server_id: String, 62 | pub platform: Platform, 63 | } 64 | 65 | #[derive(Debug, Deserialize)] 66 | pub struct Platform { 67 | pub status: u16, 68 | } 69 | 70 | #[serde(rename_all = "camelCase")] 71 | #[derive(Debug, Deserialize)] 72 | pub struct TradingSubscriptionMessage { 73 | pub event: String, 74 | pub channel: String, 75 | pub chan_id: u32, 76 | pub symbol: String, 77 | pub pair: String 78 | } 79 | 80 | #[serde(rename_all = "camelCase")] 81 | #[derive(Debug, Deserialize)] 82 | pub struct FundingSubscriptionMessage { 83 | pub event: String, 84 | pub channel: String, 85 | pub chan_id: u32, 86 | pub symbol: String, 87 | pub currency: String 88 | } 89 | 90 | #[serde(rename_all = "camelCase")] 91 | #[derive(Debug, Deserialize)] 92 | pub struct CandlesSubscriptionMessage { 93 | pub event: String, 94 | pub channel: String, 95 | pub chan_id: u32, 96 | pub key: String 97 | } 98 | 99 | #[serde(rename_all = "camelCase")] 100 | #[derive(Debug, Deserialize)] 101 | pub struct RawBookSubscriptionMessage { 102 | pub event: String, 103 | pub channel: String, 104 | pub chan_id: u32, 105 | pub symbol: String, 106 | pub prec: String, 107 | pub freq: String, 108 | pub len: String, 109 | pub pair: String 110 | } 111 | -------------------------------------------------------------------------------- /src/ledger.rs: -------------------------------------------------------------------------------- 1 | use client::*; 2 | use errors::*; 3 | use serde_json::from_str; 4 | 5 | #[derive(Serialize, Deserialize, Debug)] 6 | pub struct Entry { 7 | pub id: i64, 8 | pub currency: String, 9 | _field3: Option<()>, 10 | pub timestamp_milli: i64, 11 | _field5: Option<()>, 12 | pub amount: f64, 13 | pub balance: f64, 14 | _field8: Option<()>, 15 | pub description: String, 16 | } 17 | 18 | #[derive(Clone)] 19 | pub struct Ledger { 20 | client: Client, 21 | } 22 | 23 | #[derive(Serialize)] 24 | struct HistoryParams { 25 | pub start: String, 26 | pub end: String, 27 | pub limit: i32, 28 | } 29 | 30 | impl Ledger { 31 | pub fn new(api_key: Option, secret_key: Option) -> Self { 32 | Ledger { 33 | client: Client::new(api_key, secret_key), 34 | } 35 | } 36 | 37 | pub fn get_history( 38 | &self, 39 | symbol: S, 40 | start: u128, 41 | end: u128, 42 | limit: i32, 43 | ) -> Result> 44 | where 45 | S: Into, 46 | { 47 | let payload: String = format!("{}", "{}"); 48 | let request: String = format!("ledgers/{}/hist", symbol.into()); 49 | let params = HistoryParams{ 50 | start: format!("{}", start), 51 | end: format!("{}", end), 52 | limit: limit, 53 | }; 54 | 55 | let data = self.client.post_signed_params(request, payload, ¶ms)?; 56 | 57 | let entry: Vec = from_str(data.as_str())?; 58 | 59 | Ok(entry) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny( 2 | unstable_features, 3 | unused_must_use, 4 | unused_mut, 5 | unused_imports, 6 | unused_import_braces)] 7 | 8 | #[macro_use] 9 | extern crate error_chain; 10 | 11 | extern crate hex; 12 | extern crate ring; 13 | extern crate reqwest; 14 | extern crate serde; 15 | #[macro_use] 16 | extern crate serde_json; 17 | extern crate tungstenite; 18 | extern crate url; 19 | 20 | #[macro_use] 21 | extern crate serde_derive; 22 | 23 | mod book; 24 | mod client; 25 | mod ticker; 26 | mod trades; 27 | mod orders; 28 | mod account; 29 | mod ledger; 30 | mod auth; 31 | 32 | pub mod candles; 33 | pub mod api; 34 | pub mod pairs; 35 | pub mod currency; 36 | pub mod precision; 37 | pub mod websockets; 38 | pub mod events; 39 | pub mod errors; 40 | -------------------------------------------------------------------------------- /src/orders.rs: -------------------------------------------------------------------------------- 1 | use client::*; 2 | use errors::*; 3 | use serde_json::from_str; 4 | 5 | #[derive(Serialize, Deserialize)] 6 | pub struct Order { 7 | pub id: i64, 8 | pub group_id: Option, 9 | pub client_id: i64, 10 | pub symbol: String, 11 | pub creation_timestamp: i64, 12 | pub update_timestamp: i64, 13 | pub amount: f64, 14 | pub amount_original: f64, 15 | pub order_type: String, 16 | pub previous_order_type: Option, 17 | 18 | #[serde(skip_serializing)] 19 | _placeholder_1: Option, 20 | #[serde(skip_serializing)] 21 | _placeholder_2: Option, 22 | 23 | pub flags: Option, 24 | pub order_status: Option, 25 | 26 | #[serde(skip_serializing)] 27 | _placeholder_3: Option, 28 | #[serde(skip_serializing)] 29 | _placeholder_4: Option, 30 | 31 | pub price: f64, 32 | pub price_avg: f64, 33 | pub price_trailing: Option, 34 | pub price_aux_limit: Option, 35 | 36 | #[serde(skip_serializing)] 37 | __placeholder_5: Option, 38 | #[serde(skip_serializing)] 39 | _placeholder_6: Option, 40 | #[serde(skip_serializing)] 41 | _placeholder_7: Option, 42 | 43 | pub notify: i32, 44 | pub hidden: i32, 45 | pub placed_id: Option 46 | } 47 | 48 | #[derive(Clone)] 49 | pub struct Orders { 50 | client: Client, 51 | } 52 | 53 | impl Orders { 54 | pub fn new(api_key: Option, secret_key: Option) -> Self { 55 | Orders { 56 | client: Client::new(api_key, secret_key), 57 | } 58 | } 59 | 60 | pub fn active_orders(&self) -> Result> { 61 | let payload: String = format!("{}", "{}"); 62 | 63 | self.orders("orders".to_owned(), payload) 64 | } 65 | 66 | pub fn history(&self, symbol: T) -> Result> 67 | where T: Into> 68 | { 69 | let value = symbol.into().unwrap_or("".into()); 70 | let payload: String = format!("{}", "{}"); 71 | 72 | if value.is_empty() { 73 | return self.orders("orders/hist".into(), payload); 74 | } else { 75 | let request: String = format!("orders/t{}/hist", value); 76 | return self.orders(request, payload); 77 | } 78 | } 79 | 80 | pub fn orders(&self, request: S, payload: S) -> Result> 81 | where S: Into 82 | { 83 | let data = self.client.post_signed(request.into(), payload.into())?; 84 | 85 | let orders: Vec = from_str(data.as_str())?; 86 | 87 | Ok(orders) 88 | } 89 | } -------------------------------------------------------------------------------- /src/pairs.rs: -------------------------------------------------------------------------------- 1 | pub static BTCUSD : &'static str = "BTCUSD"; 2 | pub static LTCUSD : &'static str = "LTCUSD"; 3 | pub static LTCBTC : &'static str = "LTCBTC"; 4 | pub static ETHUSD : &'static str = "ETHUSD"; 5 | pub static ETHBTC : &'static str = "ETHBTC"; 6 | pub static ETCUSD : &'static str = "ETCUSD"; 7 | pub static ETCBTC : &'static str = "ETCBTC"; 8 | pub static BFXUSD : &'static str = "BFXUSD"; 9 | pub static BFXBTC : &'static str = "BFXBTC"; 10 | pub static ZECUSD : &'static str = "ZECUSD"; 11 | pub static ZECBTC : &'static str = "ZECBTC"; 12 | pub static XMRUSD : &'static str = "XMRUSD"; 13 | pub static XMRBTC : &'static str = "XMRBTC"; 14 | pub static RRTUSD : &'static str = "RRTUSD"; 15 | pub static RRTBTC : &'static str = "RRTBTC"; 16 | pub static XRPUSD : &'static str = "XRPUSD"; 17 | pub static XRPBTC : &'static str = "XRPBTC"; 18 | pub static EOSETH : &'static str = "EOSETH"; 19 | pub static EOSUSD : &'static str = "EOSUSD"; 20 | pub static EOSBTC : &'static str = "EOSBTC"; 21 | pub static IOTUSD : &'static str = "IOTUSD"; 22 | pub static IOTBTC : &'static str = "IOTBTC"; 23 | pub static IOTETH : &'static str = "IOTETH"; 24 | pub static IOTEUR : &'static str = "IOTEUR"; 25 | pub static BCCBTC : &'static str = "BCCBTC"; 26 | pub static BCUBTC : &'static str = "BCUBTC"; 27 | pub static BCCUSD : &'static str = "BCCUSD"; 28 | pub static BCUUSD : &'static str = "BCUUSD"; 29 | pub static GNTETH : &'static str = "GNTETH"; 30 | pub static GNTUSD : &'static str = "GNTUSD"; 31 | pub static GNTBTC : &'static str = "GNTBTC"; 32 | pub static SANETH : &'static str = "SANETH"; 33 | pub static SANUSD : &'static str = "SANUSD"; 34 | pub static SANBTC : &'static str = "SANBTC"; 35 | pub static AVTETH : &'static str = "AVTETH"; 36 | pub static AVTUSD : &'static str = "AVTUSD"; 37 | pub static AVTBTC : &'static str = "AVTBTC"; 38 | pub static QASHETH : &'static str = "QASHETH"; 39 | pub static QASHUSD : &'static str = "QASHUSD"; 40 | pub static QASHBTC : &'static str = "QASHBTC"; -------------------------------------------------------------------------------- /src/precision.rs: -------------------------------------------------------------------------------- 1 | pub static P0 : &'static str = "P0"; 2 | pub static P1 : &'static str = "P1"; 3 | pub static P2 : &'static str = "P2"; 4 | pub static P3 : &'static str = "P3"; 5 | pub static R0 : &'static str = "R0"; -------------------------------------------------------------------------------- /src/ticker.rs: -------------------------------------------------------------------------------- 1 | use client::*; 2 | use errors::*; 3 | use serde_json::from_str; 4 | 5 | #[derive(Serialize, Deserialize, Debug)] 6 | pub struct TradingPair { 7 | pub bid: f64, 8 | pub bid_size: f64, 9 | pub ask: f64, 10 | pub ask_size: f64, 11 | pub daily_change: f64, 12 | pub daily_change_perc: f64, 13 | pub last_price: f64, 14 | pub volume: f64, 15 | pub high: f64, 16 | pub low: f64 17 | } 18 | 19 | #[derive(Serialize, Deserialize, Debug)] 20 | pub struct FundingCurrency { 21 | pub frr: f64, 22 | pub bid: f64, 23 | pub bid_period: i64, 24 | pub bid_size: f64, 25 | pub ask: f64, 26 | pub ask_period: i64, 27 | pub ask_size: f64, 28 | pub daily_change: f64, 29 | pub daily_change_perc: f64, 30 | pub last_price: f64, 31 | pub volume: f64, 32 | pub high: f64, 33 | pub low: f64 34 | } 35 | 36 | #[derive(Clone)] 37 | pub struct Ticker { 38 | client: Client, 39 | } 40 | 41 | impl Ticker { 42 | pub fn new() -> Self { 43 | Ticker { 44 | client: Client::new(None, None), 45 | } 46 | } 47 | 48 | pub fn funding_currency(&self, symbol: S) -> Result 49 | where S: Into 50 | { 51 | let endpoint: String = format!("ticker/f{}", symbol.into()); 52 | let data = self.client.get(endpoint, String::new())?; 53 | 54 | let ticker: FundingCurrency = from_str(data.as_str())?; 55 | 56 | Ok(ticker) 57 | } 58 | 59 | pub fn trading_pair(&self, symbol: S) -> Result 60 | where S: Into 61 | { 62 | let endpoint: String = format!("ticker/t{}", symbol.into()); 63 | let data = self.client.get(endpoint, String::new())?; 64 | 65 | let ticker: TradingPair = from_str(data.as_str())?; 66 | 67 | Ok(ticker) 68 | } 69 | } -------------------------------------------------------------------------------- /src/trades.rs: -------------------------------------------------------------------------------- 1 | use client::*; 2 | use errors::*; 3 | use serde_json::from_str; 4 | 5 | #[derive(Serialize, Deserialize)] 6 | pub struct Trade { 7 | pub id: i64, 8 | pub pair: String, 9 | pub execution_timestamp: i64, 10 | pub order_id: i32, 11 | pub execution_amount: f64, 12 | pub execution_price: f64, 13 | pub order_type: String, 14 | pub order_price: f64, 15 | pub maker: i32, 16 | pub fee: f64, 17 | pub fee_currency: String 18 | } 19 | 20 | #[derive(Serialize, Deserialize, Debug)] 21 | pub struct TradingPair { 22 | pub id: i64, 23 | pub mts: i64, 24 | pub amount: f64, 25 | pub price: f64 26 | } 27 | 28 | #[derive(Serialize, Deserialize, Debug)] 29 | pub struct FundingCurrency { 30 | pub mts: i64, 31 | pub amount: f64, 32 | pub price: f64, 33 | pub rate: f64, 34 | pub period: i64 35 | } 36 | 37 | #[derive(Clone)] 38 | pub struct Trades { 39 | client: Client, 40 | } 41 | 42 | impl Trades { 43 | pub fn new() -> Self { 44 | Trades { 45 | client: Client::new(None, None), 46 | } 47 | } 48 | 49 | pub fn funding_currency(&self, symbol: S) -> Result> 50 | where S: Into 51 | { 52 | let endpoint: String = format!("trades/f{}/hist", symbol.into()); 53 | let data = self.client.get(endpoint, String::new())?; 54 | 55 | let trades: Vec = from_str(data.as_str())?; 56 | 57 | Ok(trades) 58 | } 59 | 60 | pub fn trading_pair(&self, symbol: S) -> Result> 61 | where S: Into 62 | { 63 | let endpoint: String = format!("trades/t{}/hist", symbol.into()); 64 | let data = self.client.get(endpoint, String::new())?; 65 | 66 | let trades: Vec = from_str(data.as_str())?; 67 | 68 | Ok(trades) 69 | } 70 | 71 | pub fn history(&self, symbol: S) -> Result> 72 | where S: Into 73 | { 74 | let payload: String = format!("{}", "{}"); 75 | 76 | let request: String = format!("trades/t{}/hist", symbol.into()); 77 | println!("HISTORY {}", request); 78 | return self.trades(request, payload); 79 | } 80 | 81 | pub fn generated_by_order(&self, symbol: S, order_id: S) -> Result> 82 | where S: Into 83 | { 84 | let payload: String = format!("{}", "{}"); 85 | 86 | let request: String = format!("order/t{}:{}/trades", symbol.into(), order_id.into()); 87 | return self.trades(request, payload); 88 | } 89 | 90 | pub fn trades(&self, request: S, payload: S) -> Result> 91 | where S: Into 92 | { 93 | let data = self.client.post_signed(request.into(), payload.into())?; 94 | 95 | let orders: Vec = from_str(data.as_str())?; 96 | 97 | Ok(orders) 98 | } 99 | } -------------------------------------------------------------------------------- /src/websockets.rs: -------------------------------------------------------------------------------- 1 | use url::Url; 2 | use errors::*; 3 | use events::*; 4 | use serde_json::from_str; 5 | use auth; 6 | 7 | use tungstenite::connect; 8 | use tungstenite::Message; 9 | use tungstenite::protocol::WebSocket; 10 | use tungstenite::client::AutoStream; 11 | use tungstenite::handshake::client::Response; 12 | 13 | use std::sync::mpsc::{self, channel}; 14 | 15 | static INFO: &'static str = "info"; 16 | static SUBSCRIBED: &'static str = "subscribed"; 17 | static AUTH: &'static str = "auth"; 18 | static WEBSOCKET_URL: &'static str = "wss://api.bitfinex.com/ws/2"; 19 | static DEAD_MAN_SWITCH_FLAG: u8 = 4; 20 | 21 | pub trait EventHandler { 22 | fn on_connect(&mut self, event: NotificationEvent); 23 | fn on_auth(&mut self, event: NotificationEvent); 24 | fn on_subscribed(&mut self, event: NotificationEvent); 25 | fn on_data_event(&mut self, event: DataEvent); 26 | fn on_error(&mut self, message: Error); 27 | } 28 | 29 | pub enum EventType { 30 | Funding, 31 | Trading 32 | } 33 | 34 | #[derive(Debug)] 35 | enum WsMessage { 36 | Close, 37 | Text(String), 38 | } 39 | 40 | pub struct WebSockets { 41 | socket: Option<(WebSocket, Response)>, 42 | sender: Sender, 43 | rx: mpsc::Receiver, 44 | event_handler: Option>, 45 | } 46 | 47 | impl WebSockets { 48 | pub fn new() -> WebSockets { 49 | let (tx, rx) = channel::(); 50 | let sender = Sender { 51 | tx: tx 52 | }; 53 | 54 | WebSockets { 55 | socket: None, 56 | sender: sender, 57 | rx: rx, 58 | event_handler: None 59 | } 60 | } 61 | 62 | pub fn connect(&mut self) -> Result<()> { 63 | let wss: String = format!("{}", WEBSOCKET_URL); 64 | let url = Url::parse(&wss)?; 65 | 66 | match connect(url) { 67 | Ok(answer) => { 68 | self.socket = Some(answer); 69 | Ok(()) 70 | } 71 | Err(e) => { 72 | bail!(format!("Error during handshake {}", e)) 73 | } 74 | } 75 | } 76 | 77 | pub fn add_event_handler(&mut self, handler: H) where H: EventHandler + 'static { 78 | self.event_handler = Some(Box::new(handler)); 79 | } 80 | 81 | /// Authenticates the connection. 82 | /// 83 | /// The connection will be authenticated until it is disconnected. 84 | /// 85 | /// # Arguments 86 | /// 87 | /// * `api_key` - The API key 88 | /// * `api_secret` - The API secret 89 | /// * `dms` - Whether the dead man switch is enabled. If true, all account orders will be 90 | /// cancelled when the socket is closed. 91 | pub fn auth( 92 | &mut self, 93 | api_key: S, 94 | api_secret: S, 95 | dms: bool, 96 | filters: &[&str], 97 | ) -> Result<()> 98 | where 99 | S: AsRef, 100 | { 101 | let nonce = auth::generate_nonce()?; 102 | let auth_payload = format!("AUTH{}", nonce); 103 | let signature = 104 | auth::sign_payload(api_secret.as_ref().as_bytes(), auth_payload.as_bytes())?; 105 | 106 | let msg = json!({ 107 | "event": "auth", 108 | "apiKey": api_key.as_ref(), 109 | "authSig": signature, 110 | "authNonce": nonce, 111 | "authPayload": auth_payload, 112 | "dms": if dms {Some(DEAD_MAN_SWITCH_FLAG)} else {None}, 113 | "filters": filters, 114 | }); 115 | 116 | if let Err(error_msg) = self.sender.send(&msg.to_string()) { 117 | self.error_hander(error_msg); 118 | } 119 | 120 | Ok(()) 121 | } 122 | 123 | pub fn subscribe_ticker(&mut self, symbol: S, et: EventType) where S: Into { 124 | let local_symbol = self.format_symbol(symbol.into(), et); 125 | let msg = json!({"event": "subscribe", "channel": "ticker", "symbol": local_symbol }); 126 | 127 | if let Err(error_msg) = self.sender.send(&msg.to_string()) { 128 | self.error_hander(error_msg); 129 | } 130 | } 131 | 132 | pub fn subscribe_trades(&mut self, symbol: S, et: EventType) where S: Into { 133 | let local_symbol = self.format_symbol(symbol.into(), et); 134 | let msg = json!({"event": "subscribe", "channel": "trades", "symbol": local_symbol }); 135 | 136 | if let Err(error_msg) = self.sender.send(&msg.to_string()) { 137 | self.error_hander(error_msg); 138 | } 139 | } 140 | 141 | pub fn subscribe_candles(&mut self, symbol: S, timeframe: S) where S: Into { 142 | let key: String = format!("trade:{}:t{}", timeframe.into(), symbol.into()); 143 | let msg = json!({"event": "subscribe", "channel": "candles", "key": key }); 144 | 145 | if let Err(error_msg) = self.sender.send(&msg.to_string()) { 146 | self.error_hander(error_msg); 147 | } 148 | } 149 | 150 | pub fn subscribe_books(&mut self, symbol: S, et: EventType, prec: P, freq: F, len: u32) 151 | where S: Into, P: Into, F: Into 152 | { 153 | let msg = json!( 154 | { 155 | "event": "subscribe", 156 | "channel": "book", 157 | "symbol": self.format_symbol(symbol.into(), et), 158 | "prec": prec.into(), 159 | "freq": freq.into(), 160 | "len": len 161 | }); 162 | 163 | if let Err(error_msg) = self.sender.send(&msg.to_string()) { 164 | self.error_hander(error_msg); 165 | } 166 | } 167 | 168 | pub fn subscribe_raw_books(&mut self, symbol: S, et: EventType) 169 | where S: Into 170 | { 171 | let msg = json!( 172 | { 173 | "event": "subscribe", 174 | "channel": "book", 175 | "prec": "R0", 176 | "pair": self.format_symbol(symbol.into(), et) 177 | }); 178 | 179 | if let Err(error_msg) = self.sender.send(&msg.to_string()) { 180 | self.error_hander(error_msg); 181 | } 182 | } 183 | 184 | fn error_hander(&mut self, error_msg: Error) { 185 | if let Some(ref mut h) = self.event_handler { 186 | h.on_error(error_msg); 187 | } 188 | } 189 | 190 | fn format_symbol(&mut self, symbol: String, et: EventType) -> String { 191 | let local_symbol = match et { 192 | EventType::Funding => format!("f{}", symbol), 193 | EventType::Trading => format!("t{}", symbol), 194 | }; 195 | 196 | local_symbol 197 | } 198 | 199 | pub fn event_loop(&mut self) -> Result<()> { 200 | loop { 201 | if let Some(ref mut socket) = self.socket { 202 | loop { 203 | match self.rx.try_recv() { 204 | Ok(msg) => { 205 | match msg { 206 | WsMessage::Text(text) => { 207 | socket.0.write_message(Message::Text(text))?; 208 | } 209 | WsMessage::Close => { 210 | return socket.0.close(None).map_err(|e| e.into()); 211 | } 212 | } 213 | } 214 | Err(mpsc::TryRecvError::Disconnected) => { 215 | bail!("Disconnected") 216 | } 217 | Err(mpsc::TryRecvError::Empty) => break, 218 | } 219 | } 220 | 221 | let message = socket.0.read_message()?; 222 | 223 | match message { 224 | Message::Text(text) => { 225 | if let Some(ref mut h) = self.event_handler { 226 | if text.find(INFO) != None { 227 | let event: NotificationEvent = from_str(&text)?; 228 | h.on_connect(event); 229 | } else if text.find(SUBSCRIBED) != None { 230 | let event: NotificationEvent = from_str(&text)?; 231 | h.on_subscribed(event); 232 | } else if text.find(AUTH).is_some() { 233 | let event: NotificationEvent = from_str(&text)?; 234 | h.on_auth(event); 235 | } else { 236 | let event: DataEvent = from_str(&text)?; 237 | if let DataEvent::HeartbeatEvent(_a,_b) = event { 238 | continue; 239 | } else { 240 | h.on_data_event(event); 241 | } 242 | } 243 | } 244 | } 245 | Message::Binary(_) => {} 246 | Message::Ping(_) | 247 | Message::Pong(_) => {} 248 | Message::Close(e) => { 249 | bail!(format!("Disconnected {:?}", e)); 250 | } 251 | } 252 | } 253 | } 254 | } 255 | } 256 | 257 | 258 | #[derive(Clone)] 259 | pub struct Sender { 260 | tx: mpsc::Sender 261 | } 262 | 263 | impl Sender { 264 | pub fn send(&self, raw: &str) -> Result<()> { 265 | self.tx.send(WsMessage::Text(raw.to_string())) 266 | .map_err(|e| Error::with_chain(e, "Not able to send a message"))?; 267 | Ok(()) 268 | } 269 | 270 | pub fn shutdown(&self) -> Result<()> { 271 | self.tx.send(WsMessage::Close) 272 | .map_err(|e| Error::with_chain(e, "Error during shutdown")) 273 | } 274 | } 275 | --------------------------------------------------------------------------------