├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── barter-data ├── Cargo.toml ├── LICENSE ├── README.md ├── examples │ ├── dynamic_multi_stream_multi_exchange.rs │ ├── multi_stream_multi_exchange.rs │ ├── order_books_l1_streams.rs │ ├── order_books_l1_streams_multi_exchange.rs │ ├── order_books_l2_streams.rs │ ├── public_trades_streams.rs │ └── public_trades_streams_multi_exchange.rs └── src │ ├── error.rs │ ├── event.rs │ ├── exchange │ ├── binance │ │ ├── book │ │ │ ├── l1.rs │ │ │ ├── l2.rs │ │ │ └── mod.rs │ │ ├── channel.rs │ │ ├── futures │ │ │ ├── l2.rs │ │ │ ├── liquidation.rs │ │ │ └── mod.rs │ │ ├── market.rs │ │ ├── mod.rs │ │ ├── spot │ │ │ ├── l2.rs │ │ │ └── mod.rs │ │ ├── subscription.rs │ │ └── trade.rs │ ├── bitfinex │ │ ├── channel.rs │ │ ├── market.rs │ │ ├── message.rs │ │ ├── mod.rs │ │ ├── subscription.rs │ │ ├── trade.rs │ │ └── validator.rs │ ├── bitmex │ │ ├── channel.rs │ │ ├── market.rs │ │ ├── message.rs │ │ ├── mod.rs │ │ ├── subscription.rs │ │ └── trade.rs │ ├── bybit │ │ ├── channel.rs │ │ ├── futures │ │ │ └── mod.rs │ │ ├── market.rs │ │ ├── message.rs │ │ ├── mod.rs │ │ ├── spot │ │ │ └── mod.rs │ │ ├── subscription.rs │ │ └── trade.rs │ ├── coinbase │ │ ├── channel.rs │ │ ├── market.rs │ │ ├── mod.rs │ │ ├── subscription.rs │ │ └── trade.rs │ ├── gateio │ │ ├── channel.rs │ │ ├── future │ │ │ └── mod.rs │ │ ├── market.rs │ │ ├── message.rs │ │ ├── mod.rs │ │ ├── option │ │ │ └── mod.rs │ │ ├── perpetual │ │ │ ├── mod.rs │ │ │ └── trade.rs │ │ ├── spot │ │ │ ├── mod.rs │ │ │ └── trade.rs │ │ └── subscription.rs │ ├── kraken │ │ ├── book │ │ │ ├── l1.rs │ │ │ └── mod.rs │ │ ├── channel.rs │ │ ├── market.rs │ │ ├── message.rs │ │ ├── mod.rs │ │ ├── subscription.rs │ │ └── trade.rs │ ├── mod.rs │ ├── okx │ │ ├── channel.rs │ │ ├── market.rs │ │ ├── mod.rs │ │ ├── subscription.rs │ │ └── trade.rs │ └── subscription.rs │ ├── instrument.rs │ ├── lib.rs │ ├── streams │ ├── builder │ │ ├── dynamic.rs │ │ ├── mod.rs │ │ └── multi.rs │ ├── consumer.rs │ └── mod.rs │ ├── subscriber │ ├── mapper.rs │ ├── mod.rs │ └── validator.rs │ ├── subscription │ ├── book.rs │ ├── candle.rs │ ├── liquidation.rs │ ├── mod.rs │ └── trade.rs │ └── transformer │ ├── book.rs │ ├── mod.rs │ └── stateless.rs ├── barter-execution ├── Cargo.toml ├── LICENSE ├── README.md ├── src │ ├── error.rs │ ├── execution │ │ ├── binance │ │ │ └── mod.rs │ │ ├── ftx │ │ │ └── mod.rs │ │ └── mod.rs │ ├── lib.rs │ ├── model │ │ ├── balance.rs │ │ ├── mod.rs │ │ ├── order.rs │ │ └── trade.rs │ └── simulated │ │ ├── exchange │ │ ├── account │ │ │ ├── balance.rs │ │ │ ├── mod.rs │ │ │ └── order.rs │ │ └── mod.rs │ │ ├── execution │ │ └── mod.rs │ │ └── mod.rs └── tests │ ├── simulated_exchange.rs │ └── util │ └── mod.rs ├── barter-integration ├── Cargo.toml ├── LICENSE ├── README.md ├── examples │ ├── signed_get_request.rs │ └── simple_websocket_integration.rs └── src │ ├── de.rs │ ├── error.rs │ ├── lib.rs │ ├── metric.rs │ ├── model │ ├── instrument │ │ ├── kind.rs │ │ ├── mod.rs │ │ └── symbol.rs │ └── mod.rs │ └── protocol │ ├── http │ ├── mod.rs │ ├── private │ │ ├── encoder.rs │ │ └── mod.rs │ ├── public │ │ └── mod.rs │ └── rest │ │ ├── client.rs │ │ └── mod.rs │ ├── mod.rs │ └── websocket.rs ├── barter-macro ├── Cargo.toml ├── LICENSE └── src │ └── lib.rs ├── barter ├── Cargo.toml ├── LICENSE ├── README.md ├── examples │ ├── data │ │ └── candles_1h.json │ ├── engine_with_historic_candles.rs │ └── engine_with_live_trades.rs ├── src │ ├── data │ │ ├── error.rs │ │ ├── historical.rs │ │ ├── live.rs │ │ └── mod.rs │ ├── engine │ │ ├── error.rs │ │ ├── mod.rs │ │ └── trader.rs │ ├── event.rs │ ├── execution │ │ ├── error.rs │ │ ├── mod.rs │ │ └── simulated.rs │ ├── lib.rs │ ├── portfolio │ │ ├── allocator.rs │ │ ├── error.rs │ │ ├── mod.rs │ │ ├── portfolio.rs │ │ ├── position.rs │ │ ├── repository │ │ │ ├── error.rs │ │ │ ├── in_memory.rs │ │ │ ├── mod.rs │ │ │ └── redis.rs │ │ └── risk.rs │ ├── statistic │ │ ├── algorithm.rs │ │ ├── dispersion.rs │ │ ├── error.rs │ │ ├── metric │ │ │ ├── drawdown.rs │ │ │ ├── mod.rs │ │ │ └── ratio.rs │ │ ├── mod.rs │ │ └── summary │ │ │ ├── data.rs │ │ │ ├── drawdown.rs │ │ │ ├── mod.rs │ │ │ ├── pnl.rs │ │ │ └── trading.rs │ └── strategy │ │ ├── example.rs │ │ └── mod.rs └── tests │ └── integration.rs └── rustfmt.toml /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | rebase-strategy: "disabled" 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "Barter CI" 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - "**.md" 7 | branches: ["develop"] 8 | pull_request: 9 | branches: ["develop"] 10 | paths-ignore: 11 | - "**.md" 12 | 13 | jobs: 14 | check: 15 | name: cargo check 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout sources 19 | uses: actions/checkout@v3 20 | 21 | - name: Install stable toolchain 22 | uses: actions-rs/toolchain@v1 23 | with: 24 | profile: minimal 25 | toolchain: stable 26 | override: true 27 | 28 | - uses: Swatinem/rust-cache@v2 29 | 30 | - name: Run cargo check 31 | uses: actions-rs/cargo@v1 32 | with: 33 | command: check 34 | 35 | test: 36 | name: cargo test 37 | runs-on: ubuntu-latest 38 | steps: 39 | - name: Checkout sources 40 | uses: actions/checkout@v3 41 | 42 | - name: Install stable toolchain 43 | uses: actions-rs/toolchain@v1 44 | with: 45 | profile: minimal 46 | toolchain: stable 47 | override: true 48 | 49 | - uses: Swatinem/rust-cache@v2 50 | 51 | - name: Run cargo test 52 | uses: actions-rs/cargo@v1 53 | with: 54 | command: test 55 | 56 | lints: 57 | name: Lint 58 | runs-on: ubuntu-latest 59 | steps: 60 | - name: Checkout sources 61 | uses: actions/checkout@v3 62 | 63 | - name: Install stable toolchain 64 | uses: actions-rs/toolchain@v1 65 | with: 66 | profile: minimal 67 | toolchain: stable 68 | override: true 69 | components: rustfmt, clippy 70 | 71 | - uses: Swatinem/rust-cache@v2 72 | 73 | - name: Run cargo fmt 74 | uses: actions-rs/cargo@v1 75 | continue-on-error: false 76 | with: 77 | command: fmt 78 | args: --all -- --check 79 | 80 | - name: Run cargo clippy 81 | uses: actions-rs/cargo@v1 82 | continue-on-error: false 83 | with: 84 | command: clippy 85 | args: -- -D warnings 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Rust 2 | Cargo.lock 3 | /target 4 | 5 | # IDE 6 | .idea/ 7 | *.iml 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "barter", 5 | "barter-data", 6 | "barter-integration", 7 | "barter-execution", 8 | "barter-macro" 9 | ] 10 | 11 | [workspace.dependencies] 12 | # Logging 13 | tracing = { version = "0.1.40" } 14 | tracing-subscriber = { version = "0.3.16" } 15 | 16 | # Async 17 | tokio = { version = "1.38.0" } 18 | tokio-stream = { version = "0.1.15" } 19 | futures = { version = "0.3.30" } 20 | async-trait = { version = "0.1.57" } 21 | pin-project = { version = "1.1.5" } 22 | 23 | # Error 24 | thiserror = { version = "1.0.61" } 25 | 26 | # SerDe 27 | serde = { version = "1.0.203", features = ["derive"] } 28 | serde_json = { version = "1.0.120" } 29 | serde_qs = { version = "0.13.0" } 30 | serde_urlencoded = { version = "0.7.1" } 31 | 32 | # Protocol 33 | url = { version = "2.3.1 " } 34 | reqwest = { version = "0.12.4" } 35 | tokio-tungstenite = { version = "0.21.0" } 36 | 37 | # Strategy 38 | ta = { version = "0.5.0" } 39 | 40 | # Data Structures 41 | vecmap-rs = { version = "0.2.1" } 42 | parking_lot = { version = "0.12.3" } 43 | 44 | # Crytographic Signatures 45 | hmac = { version = "0.12.1" } 46 | sha2 = { version = "0.10.6" } 47 | hex = { version = "0.4.3" } 48 | base64 = { version = "0.22.0" } 49 | 50 | # Misc 51 | uuid = { version = "1.9.1", features = ["v4", "serde"]} 52 | chrono = { version = "0.4.38", features = ["serde"]} 53 | derive_more = { version = "0.99.17" } 54 | itertools = { version = "0.13.0" } 55 | rust_decimal = { version = "1.29.1" } 56 | rust_decimal_macros = { version = "1.29.1" } 57 | bytes = { version = "1.5.0" } 58 | 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Barter Ecosystem Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Barter Ecosystem 2 | Barter is an open-source ecosystem of Rust libraries for building live-trading and back-testing systems. It is made up 3 | of several easy-to-use, extensible crates: 4 | * **Barter**: Framework for building event-driven live-trading and back-testing systems. Algorithmic trade with the 5 | peace of mind that comes from knowing your strategies have been back-tested with a near-identical trading engine. 6 | * **Barter-Data**: High-performance WebSocket integration library for streaming public market data from leading 7 | cryptocurrency exchanges - batteries included. 8 | * **Barter-Execution**: Feature rich simulated exchange to assist with back-testing and dry-trading. Also provides a 9 | normalised trading interface capable of executing across different financial venues. 10 | * **Barter-Integration**: Low-level frameworks for flexible REST/WebSocket integrations. 11 | * **Barter-Macro**: Barter ecosystem macros. 12 | 13 | **See: [`Barter`], [`Barter-Data`], [`Barter-Integration`], [`Barter-Execution`] & [`Barter-Macro`] for more 14 | comprehensive documentation.** 15 | 16 | [![Crates.io][crates-badge]][crates-url] 17 | [![MIT licensed][mit-badge]][mit-url] 18 | [![Build Status][actions-badge]][actions-url] 19 | [![Discord chat][discord-badge]][discord-url] 20 | 21 | [crates-badge]: https://img.shields.io/crates/v/barter.svg 22 | [crates-url]: https://crates.io/crates/barter 23 | 24 | [mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg 25 | [mit-url]: https://github.com/barter-rs/barter-rs/blob/develop/LICENSE 26 | 27 | [actions-badge]: https://gitlab.com/open-source-keir/financial-modelling/trading/barter-data-rs/badges/-/blob/main/pipeline.svg 28 | [actions-url]: https://gitlab.com/open-source-keir/financial-modelling/trading/barter-data-rs/-/commits/main 29 | 30 | [discord-badge]: https://img.shields.io/discord/910237311332151317.svg?logo=discord&style=flat-square 31 | [discord-url]: https://discord.gg/wE7RqhnQMV 32 | 33 | [`Barter`]: https://crates.io/crates/barter 34 | [`Barter-Data`]: https://crates.io/crates/barter-data 35 | [`Barter-Integration`]: https://crates.io/crates/barter-integration 36 | [`Barter-Execution`]: https://crates.io/crates/barter-execution 37 | [`Barter-Macro`]: https://crates.io/crates/barter-macro 38 | [API Documentation]: https://docs.rs/barter/latest/barter/ 39 | [Chat]: https://discord.gg/wE7RqhnQMV 40 | 41 | ## Getting Help 42 | Firstly, see if the answer to your question can be found in the [API Documentation]. If the answer is not there, I'd be 43 | happy to help via [Chat] and try answer your question via Discord. 44 | 45 | ## Contributing 46 | Thanks in advance for helping to develop the Barter ecosystem! Please do get hesitate to get touch via the Discord 47 | [Chat] to discuss development, new features, and the future roadmap. 48 | 49 | ## Licence 50 | This project is licensed under the [MIT license]. 51 | 52 | [MIT license]: https://github.com/barter-rs/barter-rs/blob/develop/LICENSE 53 | 54 | ### Contribution 55 | Unless you explicitly state otherwise, any contribution intentionally submitted 56 | for inclusion in one of the Barter workspace crates by you, shall be licensed as MIT, without any additional 57 | terms or conditions. 58 | 59 | ## Disclaimer 60 | 61 | This software is for educational purposes only. Do not risk money which 62 | you are afraid to lose. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS 63 | AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS. 64 | 65 | -------------------------------------------------------------------------------- /barter-data/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "barter-data" 3 | version = "0.8.1" 4 | authors = ["JustAStream"] 5 | edition = "2021" 6 | license = "MIT" 7 | documentation = "https://docs.rs/barter-data/" 8 | repository = "https://github.com/barter-rs/barter-rs" 9 | readme = "README.md" 10 | description = "High performance & normalised WebSocket intergration for leading cryptocurrency exchanges - batteries included." 11 | keywords = ["trading", "backtesting", "crypto", "stocks", "investment"] 12 | categories = ["accessibility", "simulation"] 13 | 14 | [dev-dependencies] 15 | tracing-subscriber = { workspace = true, features = ["env-filter", "json"] } 16 | rust_decimal = { workspace = true } 17 | rust_decimal_macros = { workspace = true } 18 | 19 | [dependencies] 20 | # Barter Ecosystem 21 | barter-integration = { path = "../barter-integration", version = "0.7.3" } 22 | barter-macro = { path = "../barter-macro", version = "0.1.1" } 23 | 24 | # Logging 25 | tracing = { workspace = true } 26 | 27 | # Async 28 | tokio = { workspace = true, features = ["sync", "macros", "rt-multi-thread"] } 29 | tokio-stream = { workspace = true, features = ["sync"] } 30 | futures = { workspace = true } 31 | async-trait = { workspace = true } 32 | 33 | # Protocol 34 | url = { workspace = true } 35 | reqwest = { workspace = true } 36 | 37 | # Error 38 | thiserror = { workspace = true } 39 | 40 | # SerDe 41 | serde = { workspace = true, features = ["derive"] } 42 | serde_json = { workspace = true } 43 | 44 | # Strategy 45 | ta = { workspace = true } 46 | 47 | # Misc 48 | chrono = { workspace = true, features = ["serde"]} 49 | derive_more = { workspace = true } 50 | itertools = { workspace = true } 51 | vecmap-rs = { workspace = true } 52 | -------------------------------------------------------------------------------- /barter-data/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Barter-Data Contributors 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /barter-data/examples/dynamic_multi_stream_multi_exchange.rs: -------------------------------------------------------------------------------- 1 | use barter_data::{ 2 | event::{DataKind, MarketEvent}, 3 | exchange::ExchangeId, 4 | streams::builder::dynamic::DynamicStreams, 5 | subscription::SubKind, 6 | }; 7 | use barter_integration::model::instrument::{kind::InstrumentKind, Instrument}; 8 | use futures::StreamExt; 9 | use tracing::info; 10 | 11 | #[rustfmt::skip] 12 | #[tokio::main] 13 | async fn main() { 14 | // Initialise INFO Tracing log subscriber 15 | init_logging(); 16 | 17 | use ExchangeId::*; 18 | use InstrumentKind::*; 19 | use SubKind::*; 20 | 21 | // Notes: 22 | // - DynamicStream::init requires an IntoIterator. 23 | // - Each "subscription batch" is an IntoIterator. 24 | // - Every "subscription batch" will initialise at-least-one WebSocket stream under the hood. 25 | // - If the "subscription batch" contains more-than-one ExchangeId and/or SubKind, the batch 26 | // will be further split under the hood for compile-time reasons. 27 | 28 | // Initialise MarketEvent streams for various ExchangeIds and SubscriptionKinds 29 | let streams = DynamicStreams::init([ 30 | // Batch notes: 31 | // Since batch contains 1 ExchangeId and 1 SubscriptionKind, so only 1 (1x1) WebSockets 32 | // will be spawned for this batch. 33 | vec![ 34 | (BinanceSpot, "btc", "usdt", Spot, PublicTrades), 35 | (BinanceSpot, "eth", "usdt", Spot, PublicTrades), 36 | ], 37 | 38 | // Batch notes: 39 | // Since batch contains 1 ExchangeId and 3 SubscriptionKinds, 3 (1x3) WebSocket connections 40 | // will be spawned for this batch (back-end requires to further split). 41 | vec![ 42 | (BinanceFuturesUsd, "btc", "usdt", Perpetual, PublicTrades), 43 | (BinanceFuturesUsd, "btc", "usdt", Perpetual, OrderBooksL1), 44 | (BinanceFuturesUsd, "btc", "usdt", Perpetual, Liquidations), 45 | 46 | ], 47 | 48 | // Batch notes: 49 | // Since batch contains 2 ExchangeIds and 1 SubscriptionKind, 2 (2x1) WebSocket connections 50 | // will be spawned for this batch (back-end requires to further split). 51 | vec![ 52 | (Okx, "btc", "usdt", Spot, PublicTrades), 53 | (Okx, "btc", "usdt", Perpetual, PublicTrades), 54 | (Bitmex, "btc", "usdt", Perpetual, PublicTrades), 55 | (Okx, "eth", "usdt", Spot, PublicTrades), 56 | (Okx, "eth", "usdt", Perpetual, PublicTrades), 57 | (Bitmex, "eth", "usdt", Perpetual, PublicTrades), 58 | ], 59 | ]).await.unwrap(); 60 | 61 | // Select all streams, mapping each SubscriptionKind `MarketEvent` into a unified 62 | // `Output` (eg/ `MarketEvent`), where MarketEvent: Into 63 | // Notes on other DynamicStreams methods: 64 | // - Use `streams.select_trades(ExchangeId)` to return a stream of trades from a given exchange. 65 | // - Use `streams.select_(ExchangeId)` to return a stream of T from a given exchange. 66 | // - Use `streams.select_all_trades(ExchangeId)` to return a stream of trades from all exchanges 67 | let mut merged = streams 68 | .select_all::>(); 69 | 70 | while let Some(event) = merged.next().await { 71 | info!("{event:?}"); 72 | } 73 | } 74 | 75 | // Initialise an INFO `Subscriber` for `Tracing` Json logs and install it as the global default. 76 | fn init_logging() { 77 | tracing_subscriber::fmt() 78 | // Filter messages based on the INFO 79 | .with_env_filter( 80 | tracing_subscriber::filter::EnvFilter::builder() 81 | .with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into()) 82 | .from_env_lossy(), 83 | ) 84 | // Disable colours on release builds 85 | .with_ansi(cfg!(debug_assertions)) 86 | // Enable Json formatting 87 | .json() 88 | // Install this Tracing subscriber as global default 89 | .init() 90 | } 91 | -------------------------------------------------------------------------------- /barter-data/examples/multi_stream_multi_exchange.rs: -------------------------------------------------------------------------------- 1 | use barter_data::{ 2 | event::{DataKind, MarketEvent}, 3 | exchange::{ 4 | binance::{futures::BinanceFuturesUsd, spot::BinanceSpot}, 5 | kraken::Kraken, 6 | okx::Okx, 7 | }, 8 | streams::Streams, 9 | subscription::{ 10 | book::{OrderBooksL1, OrderBooksL2}, 11 | trade::PublicTrades, 12 | }, 13 | }; 14 | use barter_integration::model::instrument::{kind::InstrumentKind, Instrument}; 15 | use tokio_stream::StreamExt; 16 | use tracing::info; 17 | 18 | #[rustfmt::skip] 19 | #[tokio::main] 20 | async fn main() { 21 | // Initialise INFO Tracing log subscriber 22 | init_logging(); 23 | 24 | // Notes: 25 | // - MarketEvent could use a custom enumeration if more flexibility is required. 26 | // - Each call to StreamBuilder::subscribe() creates a separate WebSocket connection for those 27 | // Subscriptions passed. 28 | 29 | // Initialise MarketEvent Streams for various exchanges 30 | let streams: Streams> = Streams::builder_multi() 31 | 32 | // Add PublicTrades Streams for various exchanges 33 | .add(Streams::::builder() 34 | .subscribe([ 35 | (BinanceSpot::default(), "btc", "usdt", InstrumentKind::Spot, PublicTrades), 36 | ]) 37 | .subscribe([ 38 | (BinanceFuturesUsd::default(), "btc", "usdt", InstrumentKind::Perpetual, PublicTrades), 39 | ]) 40 | .subscribe([ 41 | (Okx, "btc", "usdt", InstrumentKind::Spot, PublicTrades), 42 | (Okx, "btc", "usdt", InstrumentKind::Perpetual, PublicTrades), 43 | ]) 44 | ) 45 | 46 | // Add OrderBooksL1 Stream for various exchanges 47 | .add(Streams::::builder() 48 | .subscribe([ 49 | (BinanceSpot::default(), "btc", "usdt", InstrumentKind::Spot, OrderBooksL1), 50 | ]) 51 | .subscribe([ 52 | (BinanceFuturesUsd::default(), "btc", "usdt", InstrumentKind::Perpetual, OrderBooksL1), 53 | ]) 54 | .subscribe([ 55 | (Kraken, "xbt", "usd", InstrumentKind::Spot, OrderBooksL1), 56 | ]) 57 | ) 58 | 59 | // Add OrderBooksL2 Stream for various exchanges 60 | .add(Streams::::builder() 61 | .subscribe([ 62 | (BinanceSpot::default(), "btc", "usdt", InstrumentKind::Spot, OrderBooksL2), 63 | ]) 64 | .subscribe([ 65 | (BinanceFuturesUsd::default(), "btc", "usdt", InstrumentKind::Perpetual, OrderBooksL2), 66 | ]) 67 | ) 68 | .init() 69 | .await 70 | .unwrap(); 71 | 72 | // Join all exchange Streams into a single tokio_stream::StreamMap 73 | // Notes: 74 | // - Use `streams.select(ExchangeId)` to interact with the individual exchange streams! 75 | // - Use `streams.join()` to join all exchange streams into a single mpsc::UnboundedReceiver! 76 | let mut joined_stream = streams.join_map().await; 77 | 78 | while let Some((exchange, data)) = joined_stream.next().await { 79 | info!("Exchange: {exchange}, MarketEvent: {data:?}"); 80 | } 81 | } 82 | 83 | // Initialise an INFO `Subscriber` for `Tracing` Json logs and install it as the global default. 84 | fn init_logging() { 85 | tracing_subscriber::fmt() 86 | // Filter messages based on the INFO 87 | .with_env_filter( 88 | tracing_subscriber::filter::EnvFilter::builder() 89 | .with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into()) 90 | .from_env_lossy(), 91 | ) 92 | // Disable colours on release builds 93 | .with_ansi(cfg!(debug_assertions)) 94 | // Enable Json formatting 95 | .json() 96 | // Install this Tracing subscriber as global default 97 | .init() 98 | } 99 | -------------------------------------------------------------------------------- /barter-data/examples/order_books_l1_streams.rs: -------------------------------------------------------------------------------- 1 | use barter_data::{ 2 | exchange::{binance::spot::BinanceSpot, ExchangeId}, 3 | streams::Streams, 4 | subscription::book::OrderBooksL1, 5 | }; 6 | use barter_integration::model::instrument::kind::InstrumentKind; 7 | use tracing::info; 8 | 9 | #[rustfmt::skip] 10 | #[tokio::main] 11 | async fn main() { 12 | // Initialise INFO Tracing log subscriber 13 | init_logging(); 14 | 15 | // Initialise OrderBooksL1 Streams for BinanceSpot only 16 | // '--> each call to StreamBuilder::subscribe() creates a separate WebSocket connection 17 | let mut streams = Streams::::builder() 18 | 19 | // Separate WebSocket connection for BTC_USDT stream since it's very high volume 20 | .subscribe([ 21 | (BinanceSpot::default(), "btc", "usdt", InstrumentKind::Spot, OrderBooksL1), 22 | ]) 23 | 24 | // Separate WebSocket connection for ETH_USDT stream since it's very high volume 25 | .subscribe([ 26 | (BinanceSpot::default(), "eth", "usdt", InstrumentKind::Spot, OrderBooksL1), 27 | ]) 28 | 29 | // Lower volume Instruments can share a WebSocket connection 30 | .subscribe([ 31 | (BinanceSpot::default(), "xrp", "usdt", InstrumentKind::Spot, OrderBooksL1), 32 | (BinanceSpot::default(), "sol", "usdt", InstrumentKind::Spot, OrderBooksL1), 33 | (BinanceSpot::default(), "avax", "usdt", InstrumentKind::Spot, OrderBooksL1), 34 | (BinanceSpot::default(), "ltc", "usdt", InstrumentKind::Spot, OrderBooksL1), 35 | ]) 36 | .init() 37 | .await 38 | .unwrap(); 39 | 40 | // Select the ExchangeId::BinanceSpot stream 41 | // Notes: 42 | // - Use `streams.select(ExchangeId)` to interact with the individual exchange streams! 43 | // - Use `streams.join()` to join all exchange streams into a single mpsc::UnboundedReceiver! 44 | let mut binance_stream = streams 45 | .select(ExchangeId::BinanceSpot) 46 | .unwrap(); 47 | 48 | while let Some(order_book_l1) = binance_stream.recv().await { 49 | info!("MarketEvent: {order_book_l1:?}"); 50 | } 51 | } 52 | 53 | // Initialise an INFO `Subscriber` for `Tracing` Json logs and install it as the global default. 54 | fn init_logging() { 55 | tracing_subscriber::fmt() 56 | // Filter messages based on the INFO 57 | .with_env_filter( 58 | tracing_subscriber::filter::EnvFilter::builder() 59 | .with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into()) 60 | .from_env_lossy(), 61 | ) 62 | // Disable colours on release builds 63 | .with_ansi(cfg!(debug_assertions)) 64 | // Enable Json formatting 65 | .json() 66 | // Install this Tracing subscriber as global default 67 | .init() 68 | } 69 | -------------------------------------------------------------------------------- /barter-data/examples/order_books_l1_streams_multi_exchange.rs: -------------------------------------------------------------------------------- 1 | use barter_data::{ 2 | exchange::{ 3 | binance::{futures::BinanceFuturesUsd, spot::BinanceSpot}, 4 | kraken::Kraken, 5 | }, 6 | streams::Streams, 7 | subscription::book::OrderBooksL1, 8 | }; 9 | use barter_integration::model::instrument::kind::InstrumentKind; 10 | use futures::StreamExt; 11 | use tracing::info; 12 | 13 | #[rustfmt::skip] 14 | #[tokio::main] 15 | async fn main() { 16 | // Initialise INFO Tracing log subscriber 17 | init_logging(); 18 | 19 | // Initialise OrderBooksL1 Streams for various exchanges 20 | // '--> each call to StreamBuilder::subscribe() initialises a separate WebSocket connection 21 | let streams = Streams::::builder() 22 | .subscribe([ 23 | (BinanceSpot::default(), "btc", "usdt", InstrumentKind::Spot, OrderBooksL1), 24 | (BinanceSpot::default(), "eth", "usd", InstrumentKind::Spot, OrderBooksL1), 25 | ]) 26 | .subscribe([ 27 | (BinanceFuturesUsd::default(), "btc", "usdt", InstrumentKind::Perpetual, OrderBooksL1), 28 | (BinanceFuturesUsd::default(), "eth", "usd", InstrumentKind::Perpetual, OrderBooksL1), 29 | ]) 30 | .subscribe([ 31 | (Kraken, "xbt", "usd", InstrumentKind::Spot, OrderBooksL1), 32 | (Kraken, "ada", "usd", InstrumentKind::Spot, OrderBooksL1), 33 | (Kraken, "matic", "usd", InstrumentKind::Spot, OrderBooksL1), 34 | (Kraken, "dot", "usd", InstrumentKind::Spot, OrderBooksL1), 35 | ]) 36 | .init() 37 | .await 38 | .unwrap(); 39 | 40 | // Join all exchange OrderBooksL1 streams into a single tokio_stream::StreamMap 41 | // Notes: 42 | // - Use `streams.select(ExchangeId)` to interact with the individual exchange streams! 43 | // - Use `streams.join()` to join all exchange streams into a single mpsc::UnboundedReceiver! 44 | let mut joined_stream = streams.join_map().await; 45 | 46 | while let Some((exchange, order_book_l1)) = joined_stream.next().await { 47 | info!("Exchange: {exchange}, MarketEvent: {order_book_l1:?}"); 48 | } 49 | } 50 | 51 | // Initialise an INFO `Subscriber` for `Tracing` Json logs and install it as the global default. 52 | fn init_logging() { 53 | tracing_subscriber::fmt() 54 | // Filter messages based on the INFO 55 | .with_env_filter( 56 | tracing_subscriber::filter::EnvFilter::builder() 57 | .with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into()) 58 | .from_env_lossy(), 59 | ) 60 | // Disable colours on release builds 61 | .with_ansi(cfg!(debug_assertions)) 62 | // Enable Json formatting 63 | .json() 64 | // Install this Tracing subscriber as global default 65 | .init() 66 | } 67 | -------------------------------------------------------------------------------- /barter-data/examples/order_books_l2_streams.rs: -------------------------------------------------------------------------------- 1 | use barter_data::{ 2 | exchange::{binance::spot::BinanceSpot, ExchangeId}, 3 | streams::Streams, 4 | subscription::book::OrderBooksL2, 5 | }; 6 | use barter_integration::model::instrument::kind::InstrumentKind; 7 | use tracing::info; 8 | 9 | #[rustfmt::skip] 10 | #[tokio::main] 11 | async fn main() { 12 | // Initialise INFO Tracing log subscriber 13 | init_logging(); 14 | 15 | // Initialise OrderBooksL2 Streams for BinanceSpot only 16 | // '--> each call to StreamBuilder::subscribe() creates a separate WebSocket connection 17 | let mut streams = Streams::::builder() 18 | 19 | // Separate WebSocket connection for BTC_USDT stream since it's very high volume 20 | .subscribe([ 21 | (BinanceSpot::default(), "btc", "usdt", InstrumentKind::Spot, OrderBooksL2), 22 | ]) 23 | 24 | // Separate WebSocket connection for ETH_USDT stream since it's very high volume 25 | .subscribe([ 26 | (BinanceSpot::default(), "eth", "usdt", InstrumentKind::Spot, OrderBooksL2), 27 | ]) 28 | 29 | // Lower volume Instruments can share a WebSocket connection 30 | .subscribe([ 31 | (BinanceSpot::default(), "xrp", "usdt", InstrumentKind::Spot, OrderBooksL2), 32 | (BinanceSpot::default(), "sol", "usdt", InstrumentKind::Spot, OrderBooksL2), 33 | (BinanceSpot::default(), "avax", "usdt", InstrumentKind::Spot, OrderBooksL2), 34 | (BinanceSpot::default(), "ltc", "usdt", InstrumentKind::Spot, OrderBooksL2), 35 | ]) 36 | .init() 37 | .await 38 | .unwrap(); 39 | 40 | // Select the ExchangeId::BinanceSpot stream 41 | // Notes: 42 | // - Use `streams.select(ExchangeId)` to interact with the individual exchange streams! 43 | // - Use `streams.join()` to join all exchange streams into a single mpsc::UnboundedReceiver! 44 | let mut binance_stream = streams 45 | .select(ExchangeId::BinanceSpot) 46 | .unwrap(); 47 | 48 | while let Some(order_book_l2) = binance_stream.recv().await { 49 | info!("MarketEvent: {order_book_l2:?}"); 50 | } 51 | } 52 | 53 | // Initialise an INFO `Subscriber` for `Tracing` Json logs and install it as the global default. 54 | fn init_logging() { 55 | tracing_subscriber::fmt() 56 | // Filter messages based on the INFO 57 | .with_env_filter( 58 | tracing_subscriber::filter::EnvFilter::builder() 59 | .with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into()) 60 | .from_env_lossy(), 61 | ) 62 | // Disable colours on release builds 63 | .with_ansi(cfg!(debug_assertions)) 64 | // Enable Json formatting 65 | .json() 66 | // Install this Tracing subscriber as global default 67 | .init() 68 | } 69 | -------------------------------------------------------------------------------- /barter-data/examples/public_trades_streams.rs: -------------------------------------------------------------------------------- 1 | use barter_data::{ 2 | exchange::{binance::futures::BinanceFuturesUsd, ExchangeId}, 3 | streams::Streams, 4 | subscription::trade::PublicTrades, 5 | }; 6 | use barter_integration::model::instrument::kind::InstrumentKind; 7 | use tracing::info; 8 | 9 | #[rustfmt::skip] 10 | #[tokio::main] 11 | async fn main() { 12 | // Initialise INFO Tracing log subscriber 13 | init_logging(); 14 | 15 | // Initialise PublicTrades Streams for BinanceFuturesUsd only 16 | // '--> each call to StreamBuilder::subscribe() creates a separate WebSocket connection 17 | let mut streams = Streams::::builder() 18 | 19 | // Separate WebSocket connection for BTC_USDT stream since it's very high volume 20 | .subscribe([ 21 | (BinanceFuturesUsd::default(), "btc", "usdt", InstrumentKind::Perpetual, PublicTrades), 22 | ]) 23 | 24 | // Separate WebSocket connection for ETH_USDT stream since it's very high volume 25 | .subscribe([ 26 | (BinanceFuturesUsd::default(), "eth", "usdt", InstrumentKind::Perpetual, PublicTrades), 27 | ]) 28 | 29 | // Lower volume Instruments can share a WebSocket connection 30 | .subscribe([ 31 | (BinanceFuturesUsd::default(), "xrp", "usdt", InstrumentKind::Perpetual, PublicTrades), 32 | (BinanceFuturesUsd::default(), "sol", "usdt", InstrumentKind::Perpetual, PublicTrades), 33 | (BinanceFuturesUsd::default(), "avax", "usdt", InstrumentKind::Perpetual, PublicTrades), 34 | (BinanceFuturesUsd::default(), "ltc", "usdt", InstrumentKind::Perpetual, PublicTrades), 35 | ]) 36 | .init() 37 | .await 38 | .unwrap(); 39 | 40 | // Select the ExchangeId::BinanceFuturesUsd stream 41 | // Notes: 42 | // - Use `streams.select(ExchangeId)` to interact with the individual exchange streams! 43 | // - Use `streams.join()` to join all exchange streams into a single mpsc::UnboundedReceiver! 44 | let mut binance_stream = streams 45 | .select(ExchangeId::BinanceFuturesUsd) 46 | .unwrap(); 47 | 48 | while let Some(trade) = binance_stream.recv().await { 49 | info!("MarketEvent: {trade:?}"); 50 | } 51 | } 52 | 53 | // Initialise an INFO `Subscriber` for `Tracing` Json logs and install it as the global default. 54 | fn init_logging() { 55 | tracing_subscriber::fmt() 56 | // Filter messages based on the INFO 57 | .with_env_filter( 58 | tracing_subscriber::filter::EnvFilter::builder() 59 | .with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into()) 60 | .from_env_lossy(), 61 | ) 62 | // Disable colours on release builds 63 | .with_ansi(cfg!(debug_assertions)) 64 | // Enable Json formatting 65 | .json() 66 | // Install this Tracing subscriber as global default 67 | .init() 68 | } 69 | -------------------------------------------------------------------------------- /barter-data/src/error.rs: -------------------------------------------------------------------------------- 1 | use crate::{exchange::ExchangeId, subscription::SubKind}; 2 | use barter_integration::error::SocketError; 3 | use thiserror::Error; 4 | 5 | /// All errors generated in `barter-data`. 6 | #[derive(Debug, Error)] 7 | pub enum DataError { 8 | #[error("SocketError: {0}")] 9 | Socket(#[from] SocketError), 10 | 11 | #[error("unsupported dynamic Subscription for exchange: {exchange}, kind: {sub_kind}")] 12 | Unsupported { 13 | exchange: ExchangeId, 14 | sub_kind: SubKind, 15 | }, 16 | 17 | #[error( 18 | "\ 19 | InvalidSequence: first_update_id {first_update_id} does not follow on from the \ 20 | prev_last_update_id {prev_last_update_id} \ 21 | " 22 | )] 23 | InvalidSequence { 24 | prev_last_update_id: u64, 25 | first_update_id: u64, 26 | }, 27 | } 28 | 29 | impl DataError { 30 | /// Determine if an error requires a [`MarketStream`](super::MarketStream) to re-initialise. 31 | #[allow(clippy::match_like_matches_macro)] 32 | pub fn is_terminal(&self) -> bool { 33 | match self { 34 | DataError::InvalidSequence { .. } => true, 35 | _ => false, 36 | } 37 | } 38 | } 39 | 40 | #[cfg(test)] 41 | mod tests { 42 | use super::*; 43 | 44 | #[test] 45 | fn test_data_error_is_terminal() { 46 | struct TestCase { 47 | input: DataError, 48 | expected: bool, 49 | } 50 | 51 | let tests = vec![ 52 | TestCase { 53 | // TC0: is terminal w/ DataError::InvalidSequence 54 | input: DataError::InvalidSequence { 55 | prev_last_update_id: 0, 56 | first_update_id: 0, 57 | }, 58 | expected: true, 59 | }, 60 | TestCase { 61 | // TC1: is not terminal w/ DataError::Socket 62 | input: DataError::Socket(SocketError::Sink), 63 | expected: false, 64 | }, 65 | ]; 66 | 67 | for (index, test) in tests.into_iter().enumerate() { 68 | let actual = test.input.is_terminal(); 69 | assert_eq!(actual, test.expected, "TC{} failed", index); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /barter-data/src/exchange/binance/book/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::subscription::book::Level; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | /// Level 1 OrderBook types (top of book). 5 | pub mod l1; 6 | 7 | /// Level 2 OrderBook types (top of book). 8 | pub mod l2; 9 | 10 | /// [`Binance`](super::Binance) OrderBook level. 11 | /// 12 | /// #### Raw Payload Examples 13 | /// See docs: 14 | /// ```json 15 | /// ["4.00000200", "12.00000000"] 16 | /// ``` 17 | #[derive(Clone, Copy, PartialEq, PartialOrd, Debug, Deserialize, Serialize)] 18 | pub struct BinanceLevel { 19 | #[serde(deserialize_with = "barter_integration::de::de_str")] 20 | pub price: f64, 21 | #[serde(deserialize_with = "barter_integration::de::de_str")] 22 | pub amount: f64, 23 | } 24 | 25 | impl From for Level { 26 | fn from(level: BinanceLevel) -> Self { 27 | Self { 28 | price: level.price, 29 | amount: level.amount, 30 | } 31 | } 32 | } 33 | 34 | #[cfg(test)] 35 | mod tests { 36 | use super::*; 37 | 38 | mod de { 39 | use super::*; 40 | 41 | #[test] 42 | fn test_binance_level() { 43 | let input = r#"["4.00000200", "12.00000000"]"#; 44 | assert_eq!( 45 | serde_json::from_str::(input).unwrap(), 46 | BinanceLevel { 47 | price: 4.00000200, 48 | amount: 12.0 49 | }, 50 | ) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /barter-data/src/exchange/binance/channel.rs: -------------------------------------------------------------------------------- 1 | use super::{futures::BinanceFuturesUsd, Binance}; 2 | use crate::{ 3 | subscription::{ 4 | book::{OrderBooksL1, OrderBooksL2}, 5 | liquidation::Liquidations, 6 | trade::PublicTrades, 7 | Subscription, 8 | }, 9 | Identifier, 10 | }; 11 | use serde::Serialize; 12 | 13 | /// Type that defines how to translate a Barter [`Subscription`] into a [`Binance`] 14 | /// channel to be subscribed to. 15 | /// 16 | /// See docs: 17 | /// See docs: 18 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize)] 19 | pub struct BinanceChannel(pub &'static str); 20 | 21 | impl BinanceChannel { 22 | /// [`Binance`] real-time trades channel name. 23 | /// 24 | /// See docs: 25 | /// 26 | /// Note: 27 | /// For [`BinanceFuturesUsd`] this real-time 28 | /// stream is undocumented. 29 | /// 30 | /// See discord: 31 | pub const TRADES: Self = Self("@trade"); 32 | 33 | /// [`Binance`] real-time OrderBook Level1 (top of book) channel name. 34 | /// 35 | /// See docs: 36 | /// See docs: 37 | pub const ORDER_BOOK_L1: Self = Self("@bookTicker"); 38 | 39 | /// [`Binance`] OrderBook Level2 channel name (100ms delta updates). 40 | /// 41 | /// See docs: 42 | /// See docs: 43 | pub const ORDER_BOOK_L2: Self = Self("@depth@100ms"); 44 | 45 | /// [`BinanceFuturesUsd`] liquidation orders channel name. 46 | /// 47 | /// See docs: 48 | pub const LIQUIDATIONS: Self = Self("@forceOrder"); 49 | } 50 | 51 | impl Identifier 52 | for Subscription, Instrument, PublicTrades> 53 | { 54 | fn id(&self) -> BinanceChannel { 55 | BinanceChannel::TRADES 56 | } 57 | } 58 | 59 | impl Identifier 60 | for Subscription, Instrument, OrderBooksL1> 61 | { 62 | fn id(&self) -> BinanceChannel { 63 | BinanceChannel::ORDER_BOOK_L1 64 | } 65 | } 66 | 67 | impl Identifier 68 | for Subscription, Instrument, OrderBooksL2> 69 | { 70 | fn id(&self) -> BinanceChannel { 71 | BinanceChannel::ORDER_BOOK_L2 72 | } 73 | } 74 | 75 | impl Identifier 76 | for Subscription 77 | { 78 | fn id(&self) -> BinanceChannel { 79 | BinanceChannel::LIQUIDATIONS 80 | } 81 | } 82 | 83 | impl AsRef for BinanceChannel { 84 | fn as_ref(&self) -> &str { 85 | self.0 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /barter-data/src/exchange/binance/futures/mod.rs: -------------------------------------------------------------------------------- 1 | use self::{l2::BinanceFuturesBookUpdater, liquidation::BinanceLiquidation}; 2 | use super::{Binance, ExchangeServer}; 3 | use crate::{ 4 | exchange::{ExchangeId, StreamSelector}, 5 | instrument::InstrumentData, 6 | subscription::{book::OrderBooksL2, liquidation::Liquidations}, 7 | transformer::{book::MultiBookTransformer, stateless::StatelessTransformer}, 8 | ExchangeWsStream, 9 | }; 10 | use barter_integration::model::instrument::Instrument; 11 | 12 | /// Level 2 OrderBook types (top of book) and perpetual 13 | /// [`OrderBookUpdater`](crate::transformer::book::OrderBookUpdater) implementation. 14 | pub mod l2; 15 | 16 | /// Liquidation types. 17 | pub mod liquidation; 18 | 19 | /// [`BinanceFuturesUsd`] WebSocket server base url. 20 | /// 21 | /// See docs: 22 | pub const WEBSOCKET_BASE_URL_BINANCE_FUTURES_USD: &str = "wss://fstream.binance.com/ws"; 23 | 24 | /// [`Binance`] perpetual usd exchange. 25 | pub type BinanceFuturesUsd = Binance; 26 | 27 | /// [`Binance`] perpetual usd [`ExchangeServer`]. 28 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] 29 | pub struct BinanceServerFuturesUsd; 30 | 31 | impl ExchangeServer for BinanceServerFuturesUsd { 32 | const ID: ExchangeId = ExchangeId::BinanceFuturesUsd; 33 | 34 | fn websocket_url() -> &'static str { 35 | WEBSOCKET_BASE_URL_BINANCE_FUTURES_USD 36 | } 37 | } 38 | 39 | impl StreamSelector for BinanceFuturesUsd { 40 | type Stream = ExchangeWsStream< 41 | MultiBookTransformer, 42 | >; 43 | } 44 | 45 | impl StreamSelector for BinanceFuturesUsd 46 | where 47 | Instrument: InstrumentData, 48 | { 49 | type Stream = ExchangeWsStream< 50 | StatelessTransformer, 51 | >; 52 | } 53 | -------------------------------------------------------------------------------- /barter-data/src/exchange/binance/market.rs: -------------------------------------------------------------------------------- 1 | use super::Binance; 2 | use crate::{ 3 | instrument::{KeyedInstrument, MarketInstrumentData}, 4 | subscription::Subscription, 5 | Identifier, 6 | }; 7 | use barter_integration::model::instrument::{symbol::Symbol, Instrument}; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | /// Type that defines how to translate a Barter [`Subscription`] into a [`Binance`] 11 | /// market that can be subscribed to. 12 | /// 13 | /// See docs: 14 | /// See docs: 15 | #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] 16 | pub struct BinanceMarket(pub String); 17 | 18 | impl Identifier for Subscription, Instrument, Kind> { 19 | fn id(&self) -> BinanceMarket { 20 | binance_market(&self.instrument.base, &self.instrument.quote) 21 | } 22 | } 23 | 24 | impl Identifier 25 | for Subscription, KeyedInstrument, Kind> 26 | { 27 | fn id(&self) -> BinanceMarket { 28 | binance_market( 29 | &self.instrument.as_ref().base, 30 | &self.instrument.as_ref().quote, 31 | ) 32 | } 33 | } 34 | 35 | impl Identifier 36 | for Subscription, MarketInstrumentData, Kind> 37 | { 38 | fn id(&self) -> BinanceMarket { 39 | BinanceMarket(self.instrument.name_exchange.clone()) 40 | } 41 | } 42 | 43 | impl AsRef for BinanceMarket { 44 | fn as_ref(&self) -> &str { 45 | &self.0 46 | } 47 | } 48 | 49 | fn binance_market(base: &Symbol, quote: &Symbol) -> BinanceMarket { 50 | // Notes: 51 | // - Must be lowercase when subscribing (transformed to lowercase by Binance fn requests). 52 | // - Must be uppercase since Binance sends message with uppercase MARKET (eg/ BTCUSDT). 53 | BinanceMarket(format!("{base}{quote}").to_uppercase()) 54 | } 55 | -------------------------------------------------------------------------------- /barter-data/src/exchange/binance/spot/mod.rs: -------------------------------------------------------------------------------- 1 | use self::l2::BinanceSpotBookUpdater; 2 | use super::{Binance, ExchangeServer}; 3 | use crate::{ 4 | exchange::{ExchangeId, StreamSelector}, 5 | subscription::book::OrderBooksL2, 6 | transformer::book::MultiBookTransformer, 7 | ExchangeWsStream, 8 | }; 9 | use barter_integration::model::instrument::Instrument; 10 | 11 | /// Level 2 OrderBook types (top of book) and spot 12 | /// [`OrderBookUpdater`](crate::transformer::book::OrderBookUpdater) implementation. 13 | pub mod l2; 14 | 15 | /// [`BinanceSpot`] WebSocket server base url. 16 | /// 17 | /// See docs: 18 | pub const WEBSOCKET_BASE_URL_BINANCE_SPOT: &str = "wss://stream.binance.com:9443/ws"; 19 | 20 | /// [`Binance`] spot exchange. 21 | pub type BinanceSpot = Binance; 22 | 23 | /// [`Binance`] spot [`ExchangeServer`]. 24 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] 25 | pub struct BinanceServerSpot; 26 | 27 | impl ExchangeServer for BinanceServerSpot { 28 | const ID: ExchangeId = ExchangeId::BinanceSpot; 29 | 30 | fn websocket_url() -> &'static str { 31 | WEBSOCKET_BASE_URL_BINANCE_SPOT 32 | } 33 | } 34 | 35 | impl StreamSelector for BinanceSpot { 36 | type Stream = ExchangeWsStream< 37 | MultiBookTransformer, 38 | >; 39 | } 40 | -------------------------------------------------------------------------------- /barter-data/src/exchange/binance/subscription.rs: -------------------------------------------------------------------------------- 1 | use barter_integration::{error::SocketError, Validator}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | /// [`Binance`](super::Binance) subscription response message. 5 | /// 6 | /// ### Raw Payload Examples 7 | /// See docs: 8 | /// #### Subscription Success 9 | /// ```json 10 | /// { 11 | /// "id":1, 12 | /// "result":null 13 | /// } 14 | /// ``` 15 | /// 16 | /// #### Subscription Failure 17 | /// ```json 18 | /// { 19 | /// "id":1, 20 | /// "result":[] 21 | /// } 22 | /// ``` 23 | #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] 24 | pub struct BinanceSubResponse { 25 | result: Option>, 26 | id: u32, 27 | } 28 | 29 | impl Validator for BinanceSubResponse { 30 | fn validate(self) -> Result 31 | where 32 | Self: Sized, 33 | { 34 | if self.result.is_none() { 35 | Ok(self) 36 | } else { 37 | Err(SocketError::Subscribe( 38 | "received failure subscription response".to_owned(), 39 | )) 40 | } 41 | } 42 | } 43 | 44 | #[cfg(test)] 45 | mod tests { 46 | use super::*; 47 | 48 | mod de { 49 | use super::*; 50 | 51 | #[test] 52 | fn test_binance_sub_response() { 53 | struct TestCase { 54 | input: &'static str, 55 | expected: Result, 56 | } 57 | 58 | let cases = vec![ 59 | TestCase { 60 | // TC0: input response is Subscribed 61 | input: r#"{"id":1,"result":null}"#, 62 | expected: Ok(BinanceSubResponse { 63 | result: None, 64 | id: 1, 65 | }), 66 | }, 67 | TestCase { 68 | // TC1: input response is failed subscription 69 | input: r#"{"result": [], "id": 1}"#, 70 | expected: Ok(BinanceSubResponse { 71 | result: Some(vec![]), 72 | id: 1, 73 | }), 74 | }, 75 | ]; 76 | 77 | for (index, test) in cases.into_iter().enumerate() { 78 | let actual = serde_json::from_str::(test.input); 79 | match (actual, test.expected) { 80 | (Ok(actual), Ok(expected)) => { 81 | assert_eq!(actual, expected, "TC{} failed", index) 82 | } 83 | (Err(_), Err(_)) => { 84 | // Test passed 85 | } 86 | (actual, expected) => { 87 | // Test failed 88 | panic!("TC{index} failed because actual != expected. \nActual: {actual:?}\nExpected: {expected:?}\n"); 89 | } 90 | } 91 | } 92 | } 93 | } 94 | 95 | #[test] 96 | fn test_validate_binance_sub_response() { 97 | struct TestCase { 98 | input_response: BinanceSubResponse, 99 | is_valid: bool, 100 | } 101 | 102 | let cases = vec![ 103 | TestCase { 104 | // TC0: input response is successful subscription 105 | input_response: BinanceSubResponse { 106 | result: None, 107 | id: 1, 108 | }, 109 | is_valid: true, 110 | }, 111 | TestCase { 112 | // TC1: input response is failed subscription 113 | input_response: BinanceSubResponse { 114 | result: Some(vec![]), 115 | id: 1, 116 | }, 117 | is_valid: false, 118 | }, 119 | ]; 120 | 121 | for (index, test) in cases.into_iter().enumerate() { 122 | let actual = test.input_response.validate().is_ok(); 123 | assert_eq!(actual, test.is_valid, "TestCase {} failed", index); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /barter-data/src/exchange/bitfinex/channel.rs: -------------------------------------------------------------------------------- 1 | use super::Bitfinex; 2 | use crate::{ 3 | subscription::{trade::PublicTrades, Subscription}, 4 | Identifier, 5 | }; 6 | use serde::Serialize; 7 | 8 | /// Type that defines how to translate a Barter [`Subscription`] into a 9 | /// [`Bitfinex`](super::Bitfinex) channel to be subscribed to. 10 | /// 11 | /// See docs: 12 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize)] 13 | pub struct BitfinexChannel(pub &'static str); 14 | 15 | impl BitfinexChannel { 16 | /// [`Bitfinex`] real-time trades channel. 17 | /// 18 | /// See docs: 19 | pub const TRADES: Self = Self("trades"); 20 | } 21 | 22 | impl Identifier for Subscription { 23 | fn id(&self) -> BitfinexChannel { 24 | BitfinexChannel::TRADES 25 | } 26 | } 27 | 28 | impl AsRef for BitfinexChannel { 29 | fn as_ref(&self) -> &str { 30 | self.0 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /barter-data/src/exchange/bitfinex/market.rs: -------------------------------------------------------------------------------- 1 | use super::Bitfinex; 2 | use crate::{ 3 | instrument::{KeyedInstrument, MarketInstrumentData}, 4 | subscription::Subscription, 5 | Identifier, 6 | }; 7 | use barter_integration::model::instrument::{symbol::Symbol, Instrument}; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | /// Type that defines how to translate a Barter [`Subscription`] into a 11 | /// [`Bitfinex`] market that can be subscribed to. 12 | /// 13 | /// See docs: 14 | #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] 15 | pub struct BitfinexMarket(pub String); 16 | 17 | impl Identifier for Subscription { 18 | fn id(&self) -> BitfinexMarket { 19 | bitfinex_market(&self.instrument.base, &self.instrument.quote) 20 | } 21 | } 22 | 23 | impl Identifier for Subscription { 24 | fn id(&self) -> BitfinexMarket { 25 | bitfinex_market(&self.instrument.data.base, &self.instrument.data.quote) 26 | } 27 | } 28 | 29 | impl Identifier for Subscription { 30 | fn id(&self) -> BitfinexMarket { 31 | BitfinexMarket(self.instrument.name_exchange.clone()) 32 | } 33 | } 34 | 35 | impl AsRef for BitfinexMarket { 36 | fn as_ref(&self) -> &str { 37 | &self.0 38 | } 39 | } 40 | 41 | fn bitfinex_market(base: &Symbol, quote: &Symbol) -> BitfinexMarket { 42 | BitfinexMarket(format!( 43 | "t{}{}", 44 | base.to_string().to_uppercase(), 45 | quote.to_string().to_uppercase() 46 | )) 47 | } 48 | -------------------------------------------------------------------------------- /barter-data/src/exchange/bitfinex/mod.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! ### Notes 3 | //! #### SubscripionId 4 | //! - Successful Bitfinex subscription responses contain a numeric `CHANNEL_ID` that must be used to 5 | //! identify future messages relating to that subscription (not persistent across connections). 6 | //! - To identify the initial subscription response containing the `CHANNEL_ID`, the "channel" & 7 | //! "market" identifiers can be used for the `SubscriptionId(channel|market)` 8 | //! (eg/ SubscriptionId("trades|tBTCUSD")). 9 | //! - Once the subscription has been validated and the `CHANNEL_ID` determined, each `SubscriptionId` 10 | //! in the `SubscriptionIds` `HashMap` is mutated to become `SubscriptionId(CHANNEL_ID)`. 11 | //! eg/ SubscriptionId("trades|tBTCUSD") -> SubscriptionId(69) 12 | //! 13 | //! #### Connection Limits 14 | //! - The user is allowed up to 20 connections per minute on the public API. 15 | //! - Each connection can be used to connect up to 25 different channels. 16 | //! 17 | //! #### Trade Variants 18 | //! - Bitfinex trades subscriptions results in receiving tag="te" & tag="tu" trades. 19 | //! - Both appear to be identical payloads, but "te" arriving marginally faster. 20 | //! - Therefore, tag="tu" trades are filtered out and considered only as additional Heartbeats. 21 | 22 | use self::{ 23 | channel::BitfinexChannel, market::BitfinexMarket, message::BitfinexMessage, 24 | subscription::BitfinexPlatformEvent, validator::BitfinexWebSocketSubValidator, 25 | }; 26 | use crate::{ 27 | exchange::{Connector, ExchangeId, ExchangeSub, StreamSelector}, 28 | instrument::InstrumentData, 29 | subscriber::WebSocketSubscriber, 30 | subscription::trade::PublicTrades, 31 | transformer::stateless::StatelessTransformer, 32 | ExchangeWsStream, 33 | }; 34 | use barter_integration::{error::SocketError, protocol::websocket::WsMessage}; 35 | use barter_macro::{DeExchange, SerExchange}; 36 | use serde_json::json; 37 | use url::Url; 38 | 39 | /// Defines the type that translates a Barter [`Subscription`](crate::subscription::Subscription) 40 | /// into an exchange [`Connector`] specific channel used for generating [`Connector::requests`]. 41 | pub mod channel; 42 | 43 | /// Defines the type that translates a Barter [`Subscription`](crate::subscription::Subscription) 44 | /// into an exchange [`Connector`] specific market used for generating [`Connector::requests`]. 45 | pub mod market; 46 | 47 | /// [`BitfinexMessage`](message::BitfinexMessage) type for [`Bitfinex`]. 48 | pub mod message; 49 | 50 | /// [`Subscription`](crate::subscription::Subscription) response types and response 51 | /// [`Validator`](barter_integration::Validator) for [`Bitfinex`]. 52 | pub mod subscription; 53 | 54 | /// Public trade types for [`Bitfinex`]. 55 | pub mod trade; 56 | 57 | /// Custom [`SubscriptionValidator`](crate::subscriber::validator::SubscriptionValidator) 58 | /// implementation for [`Bitfinex`]. 59 | pub mod validator; 60 | 61 | /// [`Bitfinex`] server base url. 62 | /// 63 | /// See docs: 64 | pub const BASE_URL_BITFINEX: &str = "wss://api-pub.bitfinex.com/ws/2"; 65 | 66 | /// [`Bitfinex`] exchange. 67 | /// 68 | /// See docs: 69 | #[derive( 70 | Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, DeExchange, SerExchange, 71 | )] 72 | pub struct Bitfinex; 73 | 74 | impl Connector for Bitfinex { 75 | const ID: ExchangeId = ExchangeId::Bitfinex; 76 | type Channel = BitfinexChannel; 77 | type Market = BitfinexMarket; 78 | type Subscriber = WebSocketSubscriber; 79 | type SubValidator = BitfinexWebSocketSubValidator; 80 | type SubResponse = BitfinexPlatformEvent; 81 | 82 | fn url() -> Result { 83 | Url::parse(BASE_URL_BITFINEX).map_err(SocketError::UrlParse) 84 | } 85 | 86 | fn requests(exchange_subs: Vec>) -> Vec { 87 | exchange_subs 88 | .into_iter() 89 | .map(|ExchangeSub { channel, market }| { 90 | WsMessage::Text( 91 | json!({ 92 | "event": "subscribe", 93 | "channel": channel.as_ref(), 94 | "symbol": market.as_ref(), 95 | }) 96 | .to_string(), 97 | ) 98 | }) 99 | .collect() 100 | } 101 | } 102 | 103 | impl StreamSelector for Bitfinex 104 | where 105 | Instrument: InstrumentData, 106 | { 107 | type Stream = 108 | ExchangeWsStream>; 109 | } 110 | -------------------------------------------------------------------------------- /barter-data/src/exchange/bitfinex/trade.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | event::{MarketEvent, MarketIter}, 3 | exchange::ExchangeId, 4 | subscription::trade::PublicTrade, 5 | }; 6 | use barter_integration::{ 7 | de::{datetime_utc_from_epoch_duration, extract_next}, 8 | model::{Exchange, Side}, 9 | }; 10 | use chrono::{DateTime, Utc}; 11 | use serde::Serialize; 12 | 13 | /// [`Bitfinex`](super::Bitfinex) real-time trade message. 14 | /// 15 | /// ### Raw Payload Examples 16 | /// Format: \[ID, TIME, AMOUNT, PRICE\],
where +/- of amount indicates Side 17 | /// 18 | /// See docs: 19 | /// 20 | /// #### Side::Buy Trade 21 | /// See docs: 22 | /// ```json 23 | /// [420191,"te",[1225484398,1665452200022,0.08980641,19027.02807752]] 24 | /// ``` 25 | /// 26 | /// #### Side::Sell Trade 27 | /// See docs: 28 | /// ```json 29 | /// [420191,"te",[1225484398,1665452200022,-0.08980641,19027.02807752]] 30 | /// ``` 31 | /// 32 | /// ## Notes: 33 | /// - [`Bitfinex`](super::Bitfinex) trades subscriptions results in receiving tag="te" & tag="tu" 34 | /// trades, both of which are identical. 35 | /// - "te" trades arrive marginally faster. 36 | /// - Therefore, tag="tu" trades are filtered out and considered only as additional Heartbeats. 37 | /// 38 | /// See docs: 39 | #[derive(Clone, Copy, PartialEq, PartialOrd, Debug, Serialize)] 40 | pub struct BitfinexTrade { 41 | pub id: u64, 42 | pub time: DateTime, 43 | pub side: Side, 44 | pub price: f64, 45 | pub amount: f64, 46 | } 47 | 48 | impl From<(ExchangeId, InstrumentId, BitfinexTrade)> 49 | for MarketIter 50 | { 51 | fn from((exchange_id, instrument, trade): (ExchangeId, InstrumentId, BitfinexTrade)) -> Self { 52 | Self(vec![Ok(MarketEvent { 53 | exchange_time: trade.time, 54 | received_time: Utc::now(), 55 | exchange: Exchange::from(exchange_id), 56 | instrument, 57 | kind: PublicTrade { 58 | id: trade.id.to_string(), 59 | price: trade.price, 60 | amount: trade.amount, 61 | side: trade.side, 62 | }, 63 | })]) 64 | } 65 | } 66 | 67 | impl<'de> serde::Deserialize<'de> for BitfinexTrade { 68 | fn deserialize(deserializer: D) -> Result 69 | where 70 | D: serde::de::Deserializer<'de>, 71 | { 72 | struct SeqVisitor; 73 | 74 | impl<'de> serde::de::Visitor<'de> for SeqVisitor { 75 | type Value = BitfinexTrade; 76 | 77 | fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 78 | formatter.write_str("BitfinexTrade struct from the Bitfinex WebSocket API") 79 | } 80 | 81 | fn visit_seq( 82 | self, 83 | mut seq: SeqAccessor, 84 | ) -> Result 85 | where 86 | SeqAccessor: serde::de::SeqAccess<'de>, 87 | { 88 | // Trade: [ID, TIME, AMOUNT,PRICE] 89 | let id = extract_next(&mut seq, "id")?; 90 | let time_millis = extract_next(&mut seq, "time")?; 91 | let amount: f64 = extract_next(&mut seq, "amount")?; 92 | let price = extract_next(&mut seq, "price")?; 93 | let side = match amount.is_sign_positive() { 94 | true => Side::Buy, 95 | false => Side::Sell, 96 | }; 97 | 98 | // Ignore any additional elements or SerDe will fail 99 | // '--> Bitfinex may add fields without warning 100 | while seq.next_element::()?.is_some() {} 101 | 102 | Ok(BitfinexTrade { 103 | id, 104 | time: datetime_utc_from_epoch_duration(std::time::Duration::from_millis( 105 | time_millis, 106 | )), 107 | price, 108 | amount: amount.abs(), 109 | side, 110 | }) 111 | } 112 | } 113 | 114 | // Use Visitor implementation to deserialise the BitfinexTrade message 115 | deserializer.deserialize_seq(SeqVisitor) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /barter-data/src/exchange/bitmex/channel.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | exchange::bitmex::Bitmex, 3 | subscription::{trade::PublicTrades, Subscription}, 4 | Identifier, 5 | }; 6 | use serde::Serialize; 7 | 8 | /// Type that defines how to translate a Barter [`Subscription`] into a [`Bitmex`] 9 | /// channel to be subscribed to. 10 | /// 11 | /// See docs: 12 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize)] 13 | pub struct BitmexChannel(pub &'static str); 14 | 15 | impl BitmexChannel { 16 | /// [`Bitmex`] real-time trades channel name. 17 | /// 18 | /// See docs: 19 | pub const TRADES: Self = Self("trade"); 20 | } 21 | 22 | impl Identifier for Subscription { 23 | fn id(&self) -> BitmexChannel { 24 | BitmexChannel::TRADES 25 | } 26 | } 27 | 28 | impl AsRef for BitmexChannel { 29 | fn as_ref(&self) -> &str { 30 | self.0 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /barter-data/src/exchange/bitmex/market.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | exchange::bitmex::Bitmex, 3 | instrument::{KeyedInstrument, MarketInstrumentData}, 4 | subscription::Subscription, 5 | Identifier, 6 | }; 7 | use barter_integration::model::instrument::{symbol::Symbol, Instrument}; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | /// Type that defines how to translate a Barter [`Subscription`] into a [`Bitmex`] 11 | /// market that can be subscribed to. 12 | /// 13 | /// See docs: 14 | #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] 15 | pub struct BitmexMarket(pub String); 16 | 17 | impl Identifier for Subscription { 18 | fn id(&self) -> BitmexMarket { 19 | bitmex_market(&self.instrument.base, &self.instrument.quote) 20 | } 21 | } 22 | 23 | impl Identifier for Subscription { 24 | fn id(&self) -> BitmexMarket { 25 | bitmex_market(&self.instrument.data.base, &self.instrument.data.quote) 26 | } 27 | } 28 | 29 | impl Identifier for Subscription { 30 | fn id(&self) -> BitmexMarket { 31 | BitmexMarket(self.instrument.name_exchange.clone()) 32 | } 33 | } 34 | 35 | impl AsRef for BitmexMarket { 36 | fn as_ref(&self) -> &str { 37 | &self.0 38 | } 39 | } 40 | 41 | fn bitmex_market(base: &Symbol, quote: &Symbol) -> BitmexMarket { 42 | // Notes: 43 | // - Must be uppercase since Bitmex sends message with uppercase MARKET (eg/ XBTUSD). 44 | BitmexMarket(format!("{base}{quote}").to_uppercase()) 45 | } 46 | -------------------------------------------------------------------------------- /barter-data/src/exchange/bitmex/message.rs: -------------------------------------------------------------------------------- 1 | use crate::{exchange::bitmex::trade::BitmexTrade, Identifier}; 2 | use barter_integration::model::SubscriptionId; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// ### Raw Payload Examples 6 | /// See docs: 7 | /// #### Trade payload 8 | /// ```json 9 | /// { 10 | /// "table": "trade", 11 | /// "action": "insert", 12 | /// "data": [ 13 | /// { 14 | /// "timestamp": "2023-02-18T09:27:59.701Z", 15 | /// "symbol": "XBTUSD", 16 | /// "side": "Sell", 17 | /// "size": 200, 18 | /// "price": 24564.5, 19 | /// "tickDirection": "MinusTick", 20 | /// "trdMatchID": "31e50cb7-e005-a44e-f354-86e88dff52eb", 21 | /// "grossValue": 814184, 22 | /// "homeNotional": 0.00814184, 23 | /// "foreignNotional": 200, 24 | /// "trdType": "Regular" 25 | /// } 26 | /// ] 27 | /// } 28 | ///``` 29 | #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)] 30 | pub struct BitmexMessage { 31 | pub table: String, 32 | pub data: Vec, 33 | } 34 | 35 | impl Identifier> for BitmexTrade { 36 | fn id(&self) -> Option { 37 | self.data 38 | .first() 39 | .map(|trade| SubscriptionId(format!("{}|{}", self.table, trade.symbol))) 40 | .or(None) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /barter-data/src/exchange/bitmex/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | exchange::{ 3 | bitmex::{ 4 | channel::BitmexChannel, market::BitmexMarket, subscription::BitmexSubResponse, 5 | trade::BitmexTrade, 6 | }, 7 | subscription::ExchangeSub, 8 | Connector, ExchangeId, StreamSelector, 9 | }, 10 | instrument::InstrumentData, 11 | subscriber::{validator::WebSocketSubValidator, WebSocketSubscriber}, 12 | subscription::{trade::PublicTrades, Map}, 13 | transformer::stateless::StatelessTransformer, 14 | ExchangeWsStream, 15 | }; 16 | use barter_integration::{error::SocketError, protocol::websocket::WsMessage}; 17 | use serde::de::{Error, Unexpected}; 18 | use std::fmt::Debug; 19 | use url::Url; 20 | 21 | /// Defines the type that translates a Barter [`Subscription`](crate::subscription::Subscription) 22 | /// into an exchange [`Connector`] specific channel used for generating [`Connector::requests`]. 23 | pub mod channel; 24 | 25 | /// Defines the type that translates a Barter [`Subscription`](crate::subscription::Subscription) 26 | /// into an exchange [`Connector`] specific market used for generating [`Connector::requests`]. 27 | pub mod market; 28 | 29 | /// Generic [`BitmexMessage`](message::BitmexMessage) 30 | pub mod message; 31 | 32 | /// [`Subscription`](crate::subscription::Subscription) response type and response 33 | /// [`Validator`](barter_integration::Validator) for [`Bitmex`]. 34 | pub mod subscription; 35 | 36 | /// Public trade types for [`Bitmex`](Bitmex) 37 | pub mod trade; 38 | 39 | /// [`Bitmex`] server base url. 40 | /// 41 | /// See docs: 42 | pub const BASE_URL_BITMEX: &str = "wss://ws.bitmex.com/realtime"; 43 | 44 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] 45 | pub struct Bitmex; 46 | 47 | impl Connector for Bitmex { 48 | const ID: ExchangeId = ExchangeId::Bitmex; 49 | type Channel = BitmexChannel; 50 | type Market = BitmexMarket; 51 | type Subscriber = WebSocketSubscriber; 52 | type SubValidator = WebSocketSubValidator; 53 | type SubResponse = BitmexSubResponse; 54 | 55 | fn url() -> Result { 56 | Url::parse(BASE_URL_BITMEX).map_err(SocketError::UrlParse) 57 | } 58 | 59 | fn requests(exchange_subs: Vec>) -> Vec { 60 | let stream_names = exchange_subs 61 | .into_iter() 62 | .map(|sub| format!("{}:{}", sub.channel.as_ref(), sub.market.as_ref(),)) 63 | .collect::>(); 64 | 65 | vec![WsMessage::Text( 66 | serde_json::json!({ 67 | "op": "subscribe", 68 | "args": stream_names 69 | }) 70 | .to_string(), 71 | )] 72 | } 73 | 74 | fn expected_responses(_: &Map) -> usize { 75 | 1 76 | } 77 | } 78 | 79 | impl StreamSelector for Bitmex 80 | where 81 | Instrument: InstrumentData, 82 | { 83 | type Stream = 84 | ExchangeWsStream>; 85 | } 86 | 87 | impl<'de> serde::Deserialize<'de> for Bitmex { 88 | fn deserialize(deserializer: D) -> Result 89 | where 90 | D: serde::de::Deserializer<'de>, 91 | { 92 | let input = <&str as serde::Deserialize>::deserialize(deserializer)?; 93 | let expected = Self::ID.as_str(); 94 | 95 | if input == Self::ID.as_str() { 96 | Ok(Self) 97 | } else { 98 | Err(Error::invalid_value(Unexpected::Str(input), &expected)) 99 | } 100 | } 101 | } 102 | 103 | impl serde::Serialize for Bitmex { 104 | fn serialize(&self, serializer: S) -> Result 105 | where 106 | S: serde::ser::Serializer, 107 | { 108 | let exchange_id = Self::ID.as_str(); 109 | serializer.serialize_str(exchange_id) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /barter-data/src/exchange/bybit/channel.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | exchange::bybit::Bybit, 3 | subscription::{trade::PublicTrades, Subscription}, 4 | Identifier, 5 | }; 6 | use serde::Serialize; 7 | 8 | /// Type that defines how to translate a Barter [`Subscription`] into a [`Bybit`] 9 | /// channel to be subscribed to. 10 | /// 11 | /// See docs: 12 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize)] 13 | pub struct BybitChannel(pub &'static str); 14 | 15 | impl BybitChannel { 16 | /// [`Bybit`] real-time trades channel name. 17 | /// 18 | /// See docs: 19 | pub const TRADES: Self = Self("publicTrade"); 20 | } 21 | 22 | impl Identifier 23 | for Subscription, Instrument, PublicTrades> 24 | { 25 | fn id(&self) -> BybitChannel { 26 | BybitChannel::TRADES 27 | } 28 | } 29 | 30 | impl AsRef for BybitChannel { 31 | fn as_ref(&self) -> &str { 32 | self.0 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /barter-data/src/exchange/bybit/futures/mod.rs: -------------------------------------------------------------------------------- 1 | use super::{Bybit, ExchangeServer}; 2 | use crate::exchange::ExchangeId; 3 | 4 | /// [`BybitPerpetualsUsd`] WebSocket server base url. 5 | /// 6 | /// See docs: 7 | pub const WEBSOCKET_BASE_URL_BYBIT_PERPETUALS_USD: &str = "wss://stream.bybit.com/v5/public/linear"; 8 | 9 | /// [`Bybit`] perpetual exchange. 10 | pub type BybitPerpetualsUsd = Bybit; 11 | 12 | /// [`Bybit`] perpetual [`ExchangeServer`]. 13 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] 14 | pub struct BybitServerPerpetualsUsd; 15 | 16 | impl ExchangeServer for BybitServerPerpetualsUsd { 17 | const ID: ExchangeId = ExchangeId::BybitPerpetualsUsd; 18 | 19 | fn websocket_url() -> &'static str { 20 | WEBSOCKET_BASE_URL_BYBIT_PERPETUALS_USD 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /barter-data/src/exchange/bybit/market.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | exchange::bybit::Bybit, 3 | instrument::{KeyedInstrument, MarketInstrumentData}, 4 | subscription::Subscription, 5 | Identifier, 6 | }; 7 | use barter_integration::model::instrument::{symbol::Symbol, Instrument}; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | /// Type that defines how to translate a Barter [`Subscription`] into a [`Bybit`] 11 | /// market that can be subscribed to. 12 | /// 13 | /// See docs: 14 | #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] 15 | pub struct BybitMarket(pub String); 16 | 17 | impl Identifier for Subscription, Instrument, Kind> { 18 | fn id(&self) -> BybitMarket { 19 | bybit_market(&self.instrument.base, &self.instrument.quote) 20 | } 21 | } 22 | 23 | impl Identifier for Subscription, KeyedInstrument, Kind> { 24 | fn id(&self) -> BybitMarket { 25 | bybit_market(&self.instrument.data.base, &self.instrument.data.quote) 26 | } 27 | } 28 | 29 | impl Identifier 30 | for Subscription, MarketInstrumentData, Kind> 31 | { 32 | fn id(&self) -> BybitMarket { 33 | BybitMarket(self.instrument.name_exchange.clone()) 34 | } 35 | } 36 | 37 | impl AsRef for BybitMarket { 38 | fn as_ref(&self) -> &str { 39 | &self.0 40 | } 41 | } 42 | 43 | fn bybit_market(base: &Symbol, quote: &Symbol) -> BybitMarket { 44 | // Notes: 45 | // - Must be uppercase since Bybit sends message with uppercase MARKET (eg/ BTCUSDT). 46 | BybitMarket(format!("{base}{quote}").to_uppercase()) 47 | } 48 | -------------------------------------------------------------------------------- /barter-data/src/exchange/bybit/spot/mod.rs: -------------------------------------------------------------------------------- 1 | use super::{Bybit, ExchangeServer}; 2 | use crate::exchange::ExchangeId; 3 | 4 | /// [`BybitSpot`] WebSocket server base url. 5 | /// 6 | /// See docs: 7 | pub const WEBSOCKET_BASE_URL_BYBIT_SPOT: &str = "wss://stream.bybit.com/v5/public/spot"; 8 | 9 | /// [`Bybit`] spot exchange. 10 | pub type BybitSpot = Bybit; 11 | 12 | /// [`Bybit`] spot [`ExchangeServer`]. 13 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] 14 | pub struct BybitServerSpot; 15 | 16 | impl ExchangeServer for BybitServerSpot { 17 | const ID: ExchangeId = ExchangeId::BybitSpot; 18 | 19 | fn websocket_url() -> &'static str { 20 | WEBSOCKET_BASE_URL_BYBIT_SPOT 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /barter-data/src/exchange/coinbase/channel.rs: -------------------------------------------------------------------------------- 1 | use super::Coinbase; 2 | use crate::{ 3 | subscription::{trade::PublicTrades, Subscription}, 4 | Identifier, 5 | }; 6 | use serde::Serialize; 7 | 8 | /// Type that defines how to translate a Barter [`Subscription`] into a 9 | /// [`Coinbase`] channel to be subscribed to. 10 | /// 11 | /// See docs: 12 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize)] 13 | pub struct CoinbaseChannel(pub &'static str); 14 | 15 | impl CoinbaseChannel { 16 | /// [`Coinbase`] real-time trades channel. 17 | /// 18 | /// See docs: 19 | pub const TRADES: Self = Self("matches"); 20 | } 21 | 22 | impl Identifier for Subscription { 23 | fn id(&self) -> CoinbaseChannel { 24 | CoinbaseChannel::TRADES 25 | } 26 | } 27 | 28 | impl AsRef for CoinbaseChannel { 29 | fn as_ref(&self) -> &str { 30 | self.0 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /barter-data/src/exchange/coinbase/market.rs: -------------------------------------------------------------------------------- 1 | use super::Coinbase; 2 | use crate::{ 3 | instrument::{KeyedInstrument, MarketInstrumentData}, 4 | subscription::Subscription, 5 | Identifier, 6 | }; 7 | use barter_integration::model::instrument::{symbol::Symbol, Instrument}; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | /// Type that defines how to translate a Barter [`Subscription`] into a 11 | /// [`Coinbase`](super::Coinbase) market that can be subscribed to. 12 | /// 13 | /// See docs: 14 | #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] 15 | pub struct CoinbaseMarket(pub String); 16 | 17 | impl Identifier for Subscription { 18 | fn id(&self) -> CoinbaseMarket { 19 | coinbase_market(&self.instrument.base, &self.instrument.quote) 20 | } 21 | } 22 | 23 | impl Identifier for Subscription { 24 | fn id(&self) -> CoinbaseMarket { 25 | coinbase_market(&self.instrument.data.base, &self.instrument.data.quote) 26 | } 27 | } 28 | 29 | impl Identifier for Subscription { 30 | fn id(&self) -> CoinbaseMarket { 31 | CoinbaseMarket(self.instrument.name_exchange.clone()) 32 | } 33 | } 34 | 35 | impl AsRef for CoinbaseMarket { 36 | fn as_ref(&self) -> &str { 37 | &self.0 38 | } 39 | } 40 | 41 | fn coinbase_market(base: &Symbol, quote: &Symbol) -> CoinbaseMarket { 42 | CoinbaseMarket(format!("{base}-{quote}").to_uppercase()) 43 | } 44 | -------------------------------------------------------------------------------- /barter-data/src/exchange/coinbase/mod.rs: -------------------------------------------------------------------------------- 1 | use self::{ 2 | channel::CoinbaseChannel, market::CoinbaseMarket, subscription::CoinbaseSubResponse, 3 | trade::CoinbaseTrade, 4 | }; 5 | use crate::{ 6 | exchange::{Connector, ExchangeId, ExchangeSub, StreamSelector}, 7 | instrument::InstrumentData, 8 | subscriber::{validator::WebSocketSubValidator, WebSocketSubscriber}, 9 | subscription::trade::PublicTrades, 10 | transformer::stateless::StatelessTransformer, 11 | ExchangeWsStream, 12 | }; 13 | use barter_integration::{error::SocketError, protocol::websocket::WsMessage}; 14 | use barter_macro::{DeExchange, SerExchange}; 15 | use serde_json::json; 16 | use url::Url; 17 | 18 | /// Defines the type that translates a Barter [`Subscription`](crate::subscription::Subscription) 19 | /// into an exchange [`Connector`] specific channel used for generating [`Connector::requests`]. 20 | pub mod channel; 21 | 22 | /// Defines the type that translates a Barter [`Subscription`](crate::subscription::Subscription) 23 | /// into an exchange [`Connector`] specific market used for generating [`Connector::requests`]. 24 | pub mod market; 25 | 26 | /// [`Subscription`](crate::subscription::Subscription) response type and response 27 | /// [`Validator`](barter_integration::Validator) for [`Coinbase`]. 28 | pub mod subscription; 29 | 30 | /// Public trade types for [`Coinbase`]. 31 | pub mod trade; 32 | 33 | /// [`Coinbase`] server base url. 34 | /// 35 | /// See docs: 36 | pub const BASE_URL_COINBASE: &str = "wss://ws-feed.exchange.coinbase.com"; 37 | 38 | /// [`Coinbase`] exchange. 39 | /// 40 | /// See docs: 41 | #[derive( 42 | Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, DeExchange, SerExchange, 43 | )] 44 | pub struct Coinbase; 45 | 46 | impl Connector for Coinbase { 47 | const ID: ExchangeId = ExchangeId::Coinbase; 48 | type Channel = CoinbaseChannel; 49 | type Market = CoinbaseMarket; 50 | type Subscriber = WebSocketSubscriber; 51 | type SubValidator = WebSocketSubValidator; 52 | type SubResponse = CoinbaseSubResponse; 53 | 54 | fn url() -> Result { 55 | Url::parse(BASE_URL_COINBASE).map_err(SocketError::UrlParse) 56 | } 57 | 58 | fn requests(exchange_subs: Vec>) -> Vec { 59 | exchange_subs 60 | .into_iter() 61 | .map(|ExchangeSub { channel, market }| { 62 | WsMessage::Text( 63 | json!({ 64 | "type": "subscribe", 65 | "product_ids": [market.as_ref()], 66 | "channels": [channel.as_ref()], 67 | }) 68 | .to_string(), 69 | ) 70 | }) 71 | .collect() 72 | } 73 | } 74 | 75 | impl StreamSelector for Coinbase 76 | where 77 | Instrument: InstrumentData, 78 | { 79 | type Stream = 80 | ExchangeWsStream>; 81 | } 82 | -------------------------------------------------------------------------------- /barter-data/src/exchange/gateio/channel.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | instrument::InstrumentData, 3 | subscription::{trade::PublicTrades, Subscription}, 4 | Identifier, 5 | }; 6 | use barter_integration::model::instrument::kind::InstrumentKind; 7 | use serde::Serialize; 8 | 9 | /// Type that defines how to translate a Barter [`Subscription`] into a 10 | /// [`Gateio`](super::Gateio) channel to be subscribed to. 11 | /// 12 | /// See docs: 13 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize)] 14 | pub struct GateioChannel(pub &'static str); 15 | 16 | impl GateioChannel { 17 | /// Gateio [`InstrumentKind::Spot`] real-time trades channel. 18 | /// 19 | /// See docs: 20 | pub const SPOT_TRADES: Self = Self("spot.trades"); 21 | 22 | /// Gateio [`InstrumentKind::Future`] & [`InstrumentKind::Perpetual`] real-time trades channel. 23 | /// 24 | /// See docs: 25 | /// See docs: 26 | pub const FUTURE_TRADES: Self = Self("futures.trades"); 27 | 28 | /// Gateio [`InstrumentKind::Option`] real-time trades channel. 29 | /// 30 | /// See docs: 31 | pub const OPTION_TRADES: Self = Self("options.trades"); 32 | } 33 | 34 | impl Identifier 35 | for Subscription 36 | where 37 | Instrument: InstrumentData, 38 | { 39 | fn id(&self) -> GateioChannel { 40 | match self.instrument.kind() { 41 | InstrumentKind::Spot => GateioChannel::SPOT_TRADES, 42 | InstrumentKind::Future(_) | InstrumentKind::Perpetual => GateioChannel::FUTURE_TRADES, 43 | InstrumentKind::Option(_) => GateioChannel::OPTION_TRADES, 44 | } 45 | } 46 | } 47 | 48 | impl AsRef for GateioChannel { 49 | fn as_ref(&self) -> &str { 50 | self.0 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /barter-data/src/exchange/gateio/future/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | exchange::{ 3 | gateio::{perpetual::trade::GateioFuturesTrades, Gateio}, 4 | ExchangeId, ExchangeServer, StreamSelector, 5 | }, 6 | instrument::InstrumentData, 7 | subscription::trade::PublicTrades, 8 | transformer::stateless::StatelessTransformer, 9 | ExchangeWsStream, 10 | }; 11 | 12 | /// [`GateioFuturesUsd`] WebSocket server base url. 13 | /// 14 | /// See docs: 15 | pub const WEBSOCKET_BASE_URL_GATEIO_FUTURES_USD: &str = "wss://fx-ws.gateio.ws/v4/ws/delivery/usdt"; 16 | 17 | /// [`Gateio`] perpetual usd exchange. 18 | pub type GateioFuturesUsd = Gateio; 19 | 20 | /// [`Gateio`] perpetual usd [`ExchangeServer`]. 21 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] 22 | pub struct GateioServerFuturesUsd; 23 | 24 | impl ExchangeServer for GateioServerFuturesUsd { 25 | const ID: ExchangeId = ExchangeId::GateioFuturesUsd; 26 | 27 | fn websocket_url() -> &'static str { 28 | WEBSOCKET_BASE_URL_GATEIO_FUTURES_USD 29 | } 30 | } 31 | 32 | impl StreamSelector for GateioFuturesUsd 33 | where 34 | Instrument: InstrumentData, 35 | { 36 | type Stream = ExchangeWsStream< 37 | StatelessTransformer, 38 | >; 39 | } 40 | 41 | /// [`GateioFuturesBtc`] WebSocket server base url. 42 | /// 43 | /// See docs: 44 | pub const WEBSOCKET_BASE_URL_GATEIO_FUTURES_BTC: &str = "wss://fx-ws.gateio.ws/v4/ws/delivery/btc"; 45 | 46 | /// [`Gateio`] perpetual btc exchange. 47 | pub type GateioFuturesBtc = Gateio; 48 | 49 | /// [`Gateio`] perpetual btc [`ExchangeServer`]. 50 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] 51 | pub struct GateioServerFuturesBtc; 52 | 53 | impl ExchangeServer for GateioServerFuturesBtc { 54 | const ID: ExchangeId = ExchangeId::GateioFuturesBtc; 55 | 56 | fn websocket_url() -> &'static str { 57 | WEBSOCKET_BASE_URL_GATEIO_FUTURES_BTC 58 | } 59 | } 60 | 61 | impl StreamSelector for GateioFuturesBtc 62 | where 63 | Instrument: InstrumentData, 64 | { 65 | type Stream = ExchangeWsStream< 66 | StatelessTransformer, 67 | >; 68 | } 69 | -------------------------------------------------------------------------------- /barter-data/src/exchange/gateio/market.rs: -------------------------------------------------------------------------------- 1 | use super::Gateio; 2 | use crate::{ 3 | instrument::{KeyedInstrument, MarketInstrumentData}, 4 | subscription::Subscription, 5 | Identifier, 6 | }; 7 | use barter_integration::model::instrument::{ 8 | kind::{InstrumentKind, OptionKind}, 9 | Instrument, 10 | }; 11 | use chrono::{ 12 | format::{DelayedFormat, StrftimeItems}, 13 | DateTime, Utc, 14 | }; 15 | use serde::{Deserialize, Serialize}; 16 | 17 | /// Type that defines how to translate a Barter [`Subscription`] into a 18 | /// [`Gateio`](super::Gateio) market that can be subscribed to. 19 | /// 20 | /// See docs: 21 | #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] 22 | pub struct GateioMarket(pub String); 23 | 24 | impl Identifier for Subscription, Instrument, Kind> { 25 | fn id(&self) -> GateioMarket { 26 | gateio_market(&self.instrument) 27 | } 28 | } 29 | 30 | impl Identifier 31 | for Subscription, KeyedInstrument, Kind> 32 | { 33 | fn id(&self) -> GateioMarket { 34 | gateio_market(&self.instrument.data) 35 | } 36 | } 37 | 38 | impl Identifier 39 | for Subscription, MarketInstrumentData, Kind> 40 | { 41 | fn id(&self) -> GateioMarket { 42 | GateioMarket(self.instrument.name_exchange.clone()) 43 | } 44 | } 45 | 46 | impl AsRef for GateioMarket { 47 | fn as_ref(&self) -> &str { 48 | &self.0 49 | } 50 | } 51 | 52 | fn gateio_market(instrument: &Instrument) -> GateioMarket { 53 | use InstrumentKind::*; 54 | let Instrument { base, quote, kind } = instrument; 55 | 56 | GateioMarket( 57 | match kind { 58 | Spot | Perpetual => format!("{base}_{quote}"), 59 | Future(future) => { 60 | format!("{base}_{quote}_QUARTERLY_{}", format_expiry(future.expiry)) 61 | } 62 | Option(option) => format!( 63 | "{base}_{quote}-{}-{}-{}", 64 | format_expiry(option.expiry), 65 | option.strike, 66 | match option.kind { 67 | OptionKind::Call => "C", 68 | OptionKind::Put => "P", 69 | }, 70 | ), 71 | } 72 | .to_uppercase(), 73 | ) 74 | } 75 | 76 | /// Format the expiry DateTime to be Gateio API compatible. 77 | /// 78 | /// eg/ "20241231" (31st of December 2024) 79 | /// 80 | /// See docs: 81 | fn format_expiry<'a>(expiry: DateTime) -> DelayedFormat> { 82 | expiry.date_naive().format("%Y%m%d") 83 | } 84 | -------------------------------------------------------------------------------- /barter-data/src/exchange/gateio/message.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// [`Gateio`](super::Gateio) WebSocket message. 4 | /// 5 | /// ### Raw Payload Examples 6 | /// #### Subscription Trades Success 7 | /// See docs: 8 | /// ```json 9 | /// { 10 | /// "time": 1606292218, 11 | /// "time_ms": 1606292218231, 12 | /// "channel": "spot.trades", 13 | /// "event": "subscribe", 14 | /// "result": { 15 | /// "status": "success" 16 | /// } 17 | /// } 18 | /// ``` 19 | /// 20 | /// #### Subscription Trades Failure 21 | /// See docs: 22 | /// ```json 23 | /// { 24 | /// "time": 1606292218, 25 | /// "time_ms": 1606292218231, 26 | /// "channel": "spot.trades", 27 | /// "event": "subscribe", 28 | /// "error":{ 29 | /// "code":2, 30 | /// "message":"unknown currency pair GIBBERISH_USD" 31 | /// }, 32 | /// "result": null, 33 | /// } 34 | /// ``` 35 | /// 36 | /// #### Spot Trade 37 | /// See docs: 38 | /// ```json 39 | /// { 40 | /// "time": 1606292218, 41 | /// "time_ms": 1606292218231, 42 | /// "channel": "spot.trades", 43 | /// "event": "update", 44 | /// "result": { 45 | /// "id": 309143071, 46 | /// "create_time": 1606292218, 47 | /// "create_time_ms": "1606292218213.4578", 48 | /// "side": "sell", 49 | /// "currency_pair": "GT_USDT", 50 | /// "amount": "16.4700000000", 51 | /// "price": "0.4705000000" 52 | /// } 53 | /// } 54 | /// ``` 55 | /// 56 | /// See docs: 57 | #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] 58 | pub struct GateioMessage { 59 | pub channel: String, 60 | pub error: Option, 61 | #[serde(rename = "result")] 62 | pub data: T, 63 | } 64 | 65 | /// [`Gateio`](super::Gateio) WebSocket error message. 66 | /// 67 | /// See docs: 68 | #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] 69 | pub struct GateioError { 70 | pub code: u8, 71 | pub message: String, 72 | } 73 | -------------------------------------------------------------------------------- /barter-data/src/exchange/gateio/option/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | exchange::{ 3 | gateio::{perpetual::trade::GateioFuturesTrades, Gateio}, 4 | ExchangeId, ExchangeServer, StreamSelector, 5 | }, 6 | instrument::InstrumentData, 7 | subscription::trade::PublicTrades, 8 | transformer::stateless::StatelessTransformer, 9 | ExchangeWsStream, 10 | }; 11 | 12 | /// [`GateioOptions`] WebSocket server base url. 13 | /// 14 | /// See docs: 15 | pub const WEBSOCKET_BASE_URL_GATEIO_OPTIONS_USD: &str = "wss://op-ws.gateio.live/v4/ws"; 16 | 17 | /// [`Gateio`] options exchange. 18 | pub type GateioOptions = Gateio; 19 | 20 | /// [`Gateio`] options [`ExchangeServer`]. 21 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] 22 | pub struct GateioServerOptions; 23 | 24 | impl ExchangeServer for GateioServerOptions { 25 | const ID: ExchangeId = ExchangeId::GateioOptions; 26 | 27 | fn websocket_url() -> &'static str { 28 | WEBSOCKET_BASE_URL_GATEIO_OPTIONS_USD 29 | } 30 | } 31 | 32 | impl StreamSelector for GateioOptions 33 | where 34 | Instrument: InstrumentData, 35 | { 36 | type Stream = ExchangeWsStream< 37 | StatelessTransformer, 38 | >; 39 | } 40 | -------------------------------------------------------------------------------- /barter-data/src/exchange/gateio/perpetual/mod.rs: -------------------------------------------------------------------------------- 1 | use self::trade::GateioFuturesTrades; 2 | use super::Gateio; 3 | use crate::{ 4 | exchange::{ExchangeId, ExchangeServer, StreamSelector}, 5 | instrument::InstrumentData, 6 | subscription::trade::PublicTrades, 7 | transformer::stateless::StatelessTransformer, 8 | ExchangeWsStream, 9 | }; 10 | 11 | /// Public trades types. 12 | pub mod trade; 13 | 14 | /// [`GateioPerpetualsUsd`] WebSocket server base url. 15 | /// 16 | /// See docs: 17 | pub const WEBSOCKET_BASE_URL_GATEIO_PERPETUALS_USD: &str = "wss://fx-ws.gateio.ws/v4/ws/usdt"; 18 | 19 | /// [`Gateio`] perpetual usd exchange. 20 | pub type GateioPerpetualsUsd = Gateio; 21 | 22 | /// [`Gateio`] perpetual usd [`ExchangeServer`]. 23 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] 24 | pub struct GateioServerPerpetualsUsd; 25 | 26 | impl ExchangeServer for GateioServerPerpetualsUsd { 27 | const ID: ExchangeId = ExchangeId::GateioPerpetualsUsd; 28 | 29 | fn websocket_url() -> &'static str { 30 | WEBSOCKET_BASE_URL_GATEIO_PERPETUALS_USD 31 | } 32 | } 33 | 34 | impl StreamSelector for GateioPerpetualsUsd 35 | where 36 | Instrument: InstrumentData, 37 | { 38 | type Stream = ExchangeWsStream< 39 | StatelessTransformer, 40 | >; 41 | } 42 | 43 | /// [`GateioPerpetualsBtc`] WebSocket server base url. 44 | /// 45 | /// See docs: 46 | pub const WEBSOCKET_BASE_URL_GATEIO_PERPETUALS_BTC: &str = "wss://fx-ws.gateio.ws/v4/ws/btc"; 47 | 48 | /// [`Gateio`] perpetual btc exchange. 49 | pub type GateioPerpetualsBtc = Gateio; 50 | 51 | /// [`Gateio`] perpetual btc [`ExchangeServer`]. 52 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] 53 | pub struct GateioServerPerpetualsBtc; 54 | 55 | impl ExchangeServer for GateioServerPerpetualsBtc { 56 | const ID: ExchangeId = ExchangeId::GateioPerpetualsBtc; 57 | 58 | fn websocket_url() -> &'static str { 59 | WEBSOCKET_BASE_URL_GATEIO_PERPETUALS_BTC 60 | } 61 | } 62 | 63 | impl StreamSelector for GateioPerpetualsBtc 64 | where 65 | Instrument: InstrumentData, 66 | { 67 | type Stream = ExchangeWsStream< 68 | StatelessTransformer, 69 | >; 70 | } 71 | -------------------------------------------------------------------------------- /barter-data/src/exchange/gateio/spot/mod.rs: -------------------------------------------------------------------------------- 1 | use self::trade::GateioSpotTrade; 2 | use super::Gateio; 3 | use crate::{ 4 | exchange::{ExchangeId, ExchangeServer, StreamSelector}, 5 | instrument::InstrumentData, 6 | subscription::trade::PublicTrades, 7 | transformer::stateless::StatelessTransformer, 8 | ExchangeWsStream, 9 | }; 10 | use barter_macro::{DeExchange, SerExchange}; 11 | 12 | /// Public trades types. 13 | pub mod trade; 14 | 15 | /// [`GateioSpot`] WebSocket server base url. 16 | /// 17 | /// See docs: 18 | pub const WEBSOCKET_BASE_URL_GATEIO_SPOT: &str = "wss://api.gateio.ws/ws/v4/"; 19 | 20 | /// [`Gateio`] spot exchange. 21 | pub type GateioSpot = Gateio; 22 | 23 | /// [`Gateio`] spot [`ExchangeServer`]. 24 | #[derive( 25 | Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, DeExchange, SerExchange, 26 | )] 27 | pub struct GateioServerSpot; 28 | 29 | impl ExchangeServer for GateioServerSpot { 30 | const ID: ExchangeId = ExchangeId::GateioSpot; 31 | 32 | fn websocket_url() -> &'static str { 33 | WEBSOCKET_BASE_URL_GATEIO_SPOT 34 | } 35 | } 36 | 37 | impl StreamSelector for GateioSpot 38 | where 39 | Instrument: InstrumentData, 40 | { 41 | type Stream = 42 | ExchangeWsStream>; 43 | } 44 | -------------------------------------------------------------------------------- /barter-data/src/exchange/gateio/spot/trade.rs: -------------------------------------------------------------------------------- 1 | use super::super::message::GateioMessage; 2 | use crate::{ 3 | event::{MarketEvent, MarketIter}, 4 | exchange::{ExchangeId, ExchangeSub}, 5 | subscription::trade::PublicTrade, 6 | Identifier, 7 | }; 8 | use barter_integration::model::{Exchange, Side, SubscriptionId}; 9 | use chrono::{DateTime, Utc}; 10 | use serde::{Deserialize, Serialize}; 11 | 12 | /// Terse type alias for an [`GateioSpot`](super::GateioSpot) real-time trades WebSocket message. 13 | pub type GateioSpotTrade = GateioMessage; 14 | 15 | /// [`GateioSpot`](super::GateioSpot) real-time trade WebSocket message. 16 | /// 17 | /// ### Raw Payload Examples 18 | /// See docs: 19 | /// ```json 20 | /// { 21 | /// "id": 309143071, 22 | /// "create_time": 1606292218, 23 | /// "create_time_ms": "1606292218213.4578", 24 | /// "side": "sell", 25 | /// "currency_pair": "GT_USDT", 26 | /// "amount": "16.4700000000", 27 | /// "price": "0.4705000000" 28 | /// } 29 | /// ``` 30 | #[derive(Clone, PartialEq, PartialOrd, Debug, Deserialize, Serialize)] 31 | pub struct GateioSpotTradeInner { 32 | #[serde(rename = "currency_pair")] 33 | pub market: String, 34 | #[serde( 35 | rename = "create_time_ms", 36 | deserialize_with = "barter_integration::de::de_str_f64_epoch_ms_as_datetime_utc" 37 | )] 38 | pub time: DateTime, 39 | pub id: u64, 40 | #[serde(deserialize_with = "barter_integration::de::de_str")] 41 | pub price: f64, 42 | 43 | #[serde(alias = "size", deserialize_with = "barter_integration::de::de_str")] 44 | pub amount: f64, 45 | 46 | /// Taker [`Side`] of the trade. 47 | pub side: Side, 48 | } 49 | 50 | impl Identifier> for GateioSpotTrade { 51 | fn id(&self) -> Option { 52 | Some(ExchangeSub::from((&self.channel, &self.data.market)).id()) 53 | } 54 | } 55 | 56 | impl From<(ExchangeId, InstrumentId, GateioSpotTrade)> 57 | for MarketIter 58 | { 59 | fn from((exchange_id, instrument, trade): (ExchangeId, InstrumentId, GateioSpotTrade)) -> Self { 60 | Self(vec![Ok(MarketEvent { 61 | exchange_time: trade.data.time, 62 | received_time: Utc::now(), 63 | exchange: Exchange::from(exchange_id), 64 | instrument, 65 | kind: PublicTrade { 66 | id: trade.data.id.to_string(), 67 | price: trade.data.price, 68 | amount: trade.data.amount, 69 | side: trade.data.side, 70 | }, 71 | })]) 72 | } 73 | } 74 | 75 | #[cfg(test)] 76 | mod tests { 77 | use super::*; 78 | 79 | mod de { 80 | use super::*; 81 | 82 | #[test] 83 | fn test_gateio_message_futures_trade() { 84 | let input = r#" 85 | { 86 | "time": 1606292218, 87 | "time_ms": 1606292218231, 88 | "channel": "spot.trades", 89 | "event": "update", 90 | "result": { 91 | "id": 309143071, 92 | "create_time": 1606292218, 93 | "create_time_ms": "1606292218213.4578", 94 | "side": "sell", 95 | "currency_pair": "GT_USDT", 96 | "amount": "16.4700000000", 97 | "price": "0.4705000000" 98 | } 99 | } 100 | "#; 101 | serde_json::from_str::(input).unwrap(); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /barter-data/src/exchange/kraken/book/mod.rs: -------------------------------------------------------------------------------- 1 | /// Level 1 OrderBook types (top of book). 2 | pub mod l1; 3 | -------------------------------------------------------------------------------- /barter-data/src/exchange/kraken/channel.rs: -------------------------------------------------------------------------------- 1 | use super::Kraken; 2 | use crate::{ 3 | subscription::{book::OrderBooksL1, trade::PublicTrades, Subscription}, 4 | Identifier, 5 | }; 6 | use serde::Serialize; 7 | 8 | /// Type that defines how to translate a Barter [`Subscription`] into a 9 | /// [`Kraken`](super::Kraken) channel to be subscribed to. 10 | /// 11 | /// See docs: 12 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize)] 13 | pub struct KrakenChannel(pub &'static str); 14 | 15 | impl KrakenChannel { 16 | /// [`Kraken`] real-time trades channel name. 17 | /// 18 | /// See docs: 19 | pub const TRADES: Self = Self("trade"); 20 | 21 | /// [`Kraken`] real-time OrderBook Level1 (top of book) channel name. 22 | /// 23 | /// See docs: 24 | pub const ORDER_BOOK_L1: Self = Self("spread"); 25 | } 26 | 27 | impl Identifier for Subscription { 28 | fn id(&self) -> KrakenChannel { 29 | KrakenChannel::TRADES 30 | } 31 | } 32 | 33 | impl Identifier for Subscription { 34 | fn id(&self) -> KrakenChannel { 35 | KrakenChannel::ORDER_BOOK_L1 36 | } 37 | } 38 | 39 | impl AsRef for KrakenChannel { 40 | fn as_ref(&self) -> &str { 41 | self.0 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /barter-data/src/exchange/kraken/market.rs: -------------------------------------------------------------------------------- 1 | use super::Kraken; 2 | use crate::{ 3 | instrument::{KeyedInstrument, MarketInstrumentData}, 4 | subscription::Subscription, 5 | Identifier, 6 | }; 7 | use barter_integration::model::instrument::{symbol::Symbol, Instrument}; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | /// Type that defines how to translate a Barter [`Subscription`] into a 11 | /// [`Kraken`] market that can be subscribed to. 12 | /// 13 | /// See docs: 14 | #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] 15 | pub struct KrakenMarket(pub String); 16 | 17 | impl Identifier for Subscription { 18 | fn id(&self) -> KrakenMarket { 19 | kraken_market(&self.instrument.base, &self.instrument.quote) 20 | } 21 | } 22 | 23 | impl Identifier for Subscription { 24 | fn id(&self) -> KrakenMarket { 25 | kraken_market(&self.instrument.data.base, &self.instrument.data.quote) 26 | } 27 | } 28 | 29 | impl Identifier for Subscription { 30 | fn id(&self) -> KrakenMarket { 31 | KrakenMarket(self.instrument.name_exchange.clone()) 32 | } 33 | } 34 | 35 | impl AsRef for KrakenMarket { 36 | fn as_ref(&self) -> &str { 37 | &self.0 38 | } 39 | } 40 | 41 | fn kraken_market(base: &Symbol, quote: &Symbol) -> KrakenMarket { 42 | KrakenMarket(format!("{base}/{quote}").to_uppercase()) 43 | } 44 | -------------------------------------------------------------------------------- /barter-data/src/exchange/kraken/mod.rs: -------------------------------------------------------------------------------- 1 | use self::{ 2 | book::l1::KrakenOrderBookL1, channel::KrakenChannel, market::KrakenMarket, 3 | message::KrakenMessage, subscription::KrakenSubResponse, trade::KrakenTrades, 4 | }; 5 | use crate::{ 6 | exchange::{Connector, ExchangeId, ExchangeSub, StreamSelector}, 7 | instrument::InstrumentData, 8 | subscriber::{validator::WebSocketSubValidator, WebSocketSubscriber}, 9 | subscription::{book::OrderBooksL1, trade::PublicTrades}, 10 | transformer::stateless::StatelessTransformer, 11 | ExchangeWsStream, 12 | }; 13 | use barter_integration::{error::SocketError, protocol::websocket::WsMessage}; 14 | use barter_macro::{DeExchange, SerExchange}; 15 | use serde_json::json; 16 | use url::Url; 17 | 18 | /// Order book types for [`Kraken`] 19 | pub mod book; 20 | 21 | /// Defines the type that translates a Barter [`Subscription`](crate::subscription::Subscription) 22 | /// into an exchange [`Connector`] specific channel used for generating [`Connector::requests`]. 23 | pub mod channel; 24 | 25 | /// Defines the type that translates a Barter [`Subscription`](crate::subscription::Subscription) 26 | /// into an exchange [`Connector`] specific market used for generating [`Connector::requests`]. 27 | pub mod market; 28 | 29 | /// [`KrakenMessage`](message::KrakenMessage) type for [`Kraken`]. 30 | pub mod message; 31 | 32 | /// [`Subscription`](crate::subscription::Subscription) response type and response 33 | /// [`Validator`](barter_integration) for [`Kraken`]. 34 | pub mod subscription; 35 | 36 | /// Public trade types for [`Kraken`]. 37 | pub mod trade; 38 | 39 | /// [`Kraken`] server base url. 40 | /// 41 | /// See docs: 42 | pub const BASE_URL_KRAKEN: &str = "wss://ws.kraken.com/"; 43 | 44 | /// [`Kraken`] exchange. 45 | /// 46 | /// See docs: 47 | #[derive( 48 | Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, DeExchange, SerExchange, 49 | )] 50 | pub struct Kraken; 51 | 52 | impl Connector for Kraken { 53 | const ID: ExchangeId = ExchangeId::Kraken; 54 | type Channel = KrakenChannel; 55 | type Market = KrakenMarket; 56 | type Subscriber = WebSocketSubscriber; 57 | type SubValidator = WebSocketSubValidator; 58 | type SubResponse = KrakenSubResponse; 59 | 60 | fn url() -> Result { 61 | Url::parse(BASE_URL_KRAKEN).map_err(SocketError::UrlParse) 62 | } 63 | 64 | fn requests(exchange_subs: Vec>) -> Vec { 65 | exchange_subs 66 | .into_iter() 67 | .map(|ExchangeSub { channel, market }| { 68 | WsMessage::Text( 69 | json!({ 70 | "event": "subscribe", 71 | "pair": [market.as_ref()], 72 | "subscription": { 73 | "name": channel.as_ref() 74 | } 75 | }) 76 | .to_string(), 77 | ) 78 | }) 79 | .collect() 80 | } 81 | } 82 | 83 | impl StreamSelector for Kraken 84 | where 85 | Instrument: InstrumentData, 86 | { 87 | type Stream = 88 | ExchangeWsStream>; 89 | } 90 | 91 | impl StreamSelector for Kraken 92 | where 93 | Instrument: InstrumentData, 94 | { 95 | type Stream = ExchangeWsStream< 96 | StatelessTransformer, 97 | >; 98 | } 99 | -------------------------------------------------------------------------------- /barter-data/src/exchange/okx/channel.rs: -------------------------------------------------------------------------------- 1 | use super::Okx; 2 | use crate::{ 3 | subscription::{trade::PublicTrades, Subscription}, 4 | Identifier, 5 | }; 6 | use serde::Serialize; 7 | 8 | /// Type that defines how to translate a Barter [`Subscription`] into a 9 | /// [`Okx`] channel to be subscribed to. 10 | /// 11 | /// See docs: 12 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize)] 13 | pub struct OkxChannel(pub &'static str); 14 | 15 | impl OkxChannel { 16 | /// [`Okx`] real-time trades channel. 17 | /// 18 | /// See docs: 19 | pub const TRADES: Self = Self("trades"); 20 | } 21 | 22 | impl Identifier for Subscription { 23 | fn id(&self) -> OkxChannel { 24 | OkxChannel::TRADES 25 | } 26 | } 27 | 28 | impl AsRef for OkxChannel { 29 | fn as_ref(&self) -> &str { 30 | self.0 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /barter-data/src/exchange/okx/market.rs: -------------------------------------------------------------------------------- 1 | use super::Okx; 2 | use crate::{ 3 | instrument::{KeyedInstrument, MarketInstrumentData}, 4 | subscription::Subscription, 5 | Identifier, 6 | }; 7 | use barter_integration::model::instrument::{ 8 | kind::{InstrumentKind, OptionKind}, 9 | Instrument, 10 | }; 11 | use chrono::{ 12 | format::{DelayedFormat, StrftimeItems}, 13 | DateTime, Utc, 14 | }; 15 | use serde::{Deserialize, Serialize}; 16 | 17 | /// Type that defines how to translate a Barter [`Subscription`] into a 18 | /// [`Okx`] market that can be subscribed to. 19 | /// 20 | /// See docs: 21 | #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] 22 | pub struct OkxMarket(pub String); 23 | 24 | impl Identifier for Subscription { 25 | fn id(&self) -> OkxMarket { 26 | okx_market(&self.instrument) 27 | } 28 | } 29 | 30 | impl Identifier for Subscription { 31 | fn id(&self) -> OkxMarket { 32 | okx_market(&self.instrument.data) 33 | } 34 | } 35 | 36 | impl Identifier for Subscription { 37 | fn id(&self) -> OkxMarket { 38 | OkxMarket(self.instrument.name_exchange.clone()) 39 | } 40 | } 41 | 42 | impl AsRef for OkxMarket { 43 | fn as_ref(&self) -> &str { 44 | &self.0 45 | } 46 | } 47 | 48 | fn okx_market(instrument: &Instrument) -> OkxMarket { 49 | use InstrumentKind::*; 50 | let Instrument { base, quote, kind } = instrument; 51 | 52 | OkxMarket(match kind { 53 | Spot => format!("{base}-{quote}").to_uppercase(), 54 | Future(future) => format!("{base}-{quote}-{}", format_expiry(future.expiry)).to_uppercase(), 55 | Perpetual => format!("{base}-{quote}-SWAP").to_uppercase(), 56 | Option(option) => format!( 57 | "{base}-{quote}-{}-{}-{}", 58 | format_expiry(option.expiry), 59 | option.strike, 60 | match option.kind { 61 | OptionKind::Call => "C", 62 | OptionKind::Put => "P", 63 | }, 64 | ) 65 | .to_uppercase(), 66 | }) 67 | } 68 | 69 | /// Format the expiry DateTime to be Okx API compatible. 70 | /// 71 | /// eg/ "230526" (26th of May 2023) 72 | /// 73 | /// See docs: 74 | fn format_expiry<'a>(expiry: DateTime) -> DelayedFormat> { 75 | expiry.date_naive().format("%g%m%d") 76 | } 77 | -------------------------------------------------------------------------------- /barter-data/src/exchange/okx/mod.rs: -------------------------------------------------------------------------------- 1 | use self::{ 2 | channel::OkxChannel, market::OkxMarket, subscription::OkxSubResponse, trade::OkxTrades, 3 | }; 4 | use crate::{ 5 | exchange::{Connector, ExchangeId, ExchangeSub, PingInterval, StreamSelector}, 6 | instrument::InstrumentData, 7 | subscriber::{validator::WebSocketSubValidator, WebSocketSubscriber}, 8 | subscription::trade::PublicTrades, 9 | transformer::stateless::StatelessTransformer, 10 | ExchangeWsStream, 11 | }; 12 | use barter_integration::{error::SocketError, protocol::websocket::WsMessage}; 13 | use barter_macro::{DeExchange, SerExchange}; 14 | use serde_json::json; 15 | use std::time::Duration; 16 | use url::Url; 17 | 18 | /// Defines the type that translates a Barter [`Subscription`](crate::subscription::Subscription) 19 | /// into an exchange [`Connector`] specific channel used for generating [`Connector::requests`]. 20 | pub mod channel; 21 | 22 | /// Defines the type that translates a Barter [`Subscription`](crate::subscription::Subscription) 23 | /// into an exchange [`Connector`] specific market used for generating [`Connector::requests`]. 24 | pub mod market; 25 | 26 | /// [`Subscription`](crate::subscription::Subscription) response type and response 27 | /// [`Validator`](barter_integration::Validator) for [`Okx`]. 28 | pub mod subscription; 29 | 30 | /// Public trade types for [`Okx`]. 31 | pub mod trade; 32 | 33 | /// [`Okx`] server base url. 34 | /// 35 | /// See docs: 36 | pub const BASE_URL_OKX: &str = "wss://wsaws.okx.com:8443/ws/v5/public"; 37 | 38 | /// [`Okx`] server [`PingInterval`] duration. 39 | /// 40 | /// See docs: 41 | pub const PING_INTERVAL_OKX: Duration = Duration::from_secs(29); 42 | 43 | /// [`Okx`] exchange. 44 | /// 45 | /// See docs: 46 | #[derive( 47 | Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, DeExchange, SerExchange, 48 | )] 49 | pub struct Okx; 50 | 51 | impl Connector for Okx { 52 | const ID: ExchangeId = ExchangeId::Okx; 53 | type Channel = OkxChannel; 54 | type Market = OkxMarket; 55 | type Subscriber = WebSocketSubscriber; 56 | type SubValidator = WebSocketSubValidator; 57 | type SubResponse = OkxSubResponse; 58 | 59 | fn url() -> Result { 60 | Url::parse(BASE_URL_OKX).map_err(SocketError::UrlParse) 61 | } 62 | 63 | fn ping_interval() -> Option { 64 | Some(PingInterval { 65 | interval: tokio::time::interval(PING_INTERVAL_OKX), 66 | ping: || WsMessage::text("ping"), 67 | }) 68 | } 69 | 70 | fn requests(exchange_subs: Vec>) -> Vec { 71 | vec![WsMessage::Text( 72 | json!({ 73 | "op": "subscribe", 74 | "args": &exchange_subs, 75 | }) 76 | .to_string(), 77 | )] 78 | } 79 | } 80 | 81 | impl StreamSelector for Okx 82 | where 83 | Instrument: InstrumentData, 84 | { 85 | type Stream = 86 | ExchangeWsStream>; 87 | } 88 | -------------------------------------------------------------------------------- /barter-data/src/exchange/subscription.rs: -------------------------------------------------------------------------------- 1 | use crate::{subscription::Subscription, Identifier}; 2 | use barter_integration::model::SubscriptionId; 3 | use serde::Deserialize; 4 | 5 | /// Defines an exchange specific market and channel combination used by an exchange 6 | /// [`Connector`](super::Connector) to build the 7 | /// [`WsMessage`](barter_integration::protocol::websocket::WsMessage) subscription payloads to 8 | /// send to the exchange server. 9 | /// 10 | /// ### Examples 11 | /// #### Binance OrderBooksL2 12 | /// ```json 13 | /// ExchangeSub { 14 | /// channel: BinanceChannel("@depth@100ms"), 15 | /// market: BinanceMarket("btcusdt"), 16 | /// } 17 | /// ``` 18 | /// #### Kraken PublicTrades 19 | /// ```json 20 | /// ExchangeSub { 21 | /// channel: KrakenChannel("trade"), 22 | /// market: KrakenChannel("BTC/USDT") 23 | /// } 24 | /// ``` 25 | #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize)] 26 | pub struct ExchangeSub { 27 | /// Type that defines how to translate a Barter [`Subscription`] into an exchange specific 28 | /// channel to be subscribed to. 29 | /// 30 | /// ### Examples 31 | /// - [`BinanceChannel("@depth@100ms")`](super::binance::channel::BinanceChannel) 32 | /// - [`KrakenChannel("trade")`](super::kraken::channel::KrakenChannel) 33 | pub channel: Channel, 34 | 35 | /// Type that defines how to translate a Barter [`Subscription`] into an exchange specific 36 | /// market that can be subscribed to. 37 | /// 38 | /// ### Examples 39 | /// - [`BinanceMarket("btcusdt")`](super::binance::market::BinanceMarket) 40 | /// - [`KrakenMarket("BTC/USDT")`](super::kraken::market::KrakenMarket) 41 | pub market: Market, 42 | } 43 | 44 | impl Identifier for ExchangeSub 45 | where 46 | Channel: AsRef, 47 | Market: AsRef, 48 | { 49 | fn id(&self) -> SubscriptionId { 50 | SubscriptionId::from(format!( 51 | "{}|{}", 52 | self.channel.as_ref(), 53 | self.market.as_ref() 54 | )) 55 | } 56 | } 57 | 58 | impl ExchangeSub 59 | where 60 | Channel: AsRef, 61 | Market: AsRef, 62 | { 63 | /// Construct a new exchange specific [`Self`] with the Barter [`Subscription`] provided. 64 | pub fn new(sub: &Subscription) -> Self 65 | where 66 | Subscription: Identifier + Identifier, 67 | { 68 | Self { 69 | channel: sub.id(), 70 | market: sub.id(), 71 | } 72 | } 73 | } 74 | 75 | impl From<(Channel, Market)> for ExchangeSub 76 | where 77 | Channel: AsRef, 78 | Market: AsRef, 79 | { 80 | fn from((channel, market): (Channel, Market)) -> Self { 81 | Self { channel, market } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /barter-data/src/instrument.rs: -------------------------------------------------------------------------------- 1 | use barter_integration::model::instrument::{kind::InstrumentKind, Instrument}; 2 | use derive_more::{Constructor, Display}; 3 | use serde::{Deserialize, Serialize}; 4 | use std::fmt::Debug; 5 | 6 | /// Concise unique identifier for an instrument. Used to key 7 | /// [MarketEvents](crate::event::MarketEvent) in a memory efficient way. 8 | #[derive( 9 | Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Display, 10 | )] 11 | pub struct InstrumentId(pub u64); 12 | 13 | /// Instrument related data that defines an associated unique `Id`. 14 | /// 15 | /// Verbose `InstrumentData` is often used to subscribe to market data feeds, but it's unique `Id` 16 | /// can then be used to key consumed [MarketEvents](crate::event::MarketEvent), significantly reducing 17 | /// duplication in the case of complex instruments (eg/ options). 18 | pub trait InstrumentData 19 | where 20 | Self: Clone + Debug + Send + Sync, 21 | { 22 | type Id: Debug + Clone + Send + Sync; 23 | fn id(&self) -> &Self::Id; 24 | fn kind(&self) -> InstrumentKind; 25 | } 26 | 27 | #[derive( 28 | Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Constructor, 29 | )] 30 | pub struct KeyedInstrument { 31 | pub id: Id, 32 | pub data: Instrument, 33 | } 34 | 35 | impl InstrumentData for KeyedInstrument 36 | where 37 | Id: Debug + Clone + Send + Sync, 38 | { 39 | type Id = Id; 40 | 41 | fn id(&self) -> &Self::Id { 42 | &self.id 43 | } 44 | 45 | fn kind(&self) -> InstrumentKind { 46 | self.data.kind 47 | } 48 | } 49 | 50 | impl AsRef for KeyedInstrument { 51 | fn as_ref(&self) -> &Instrument { 52 | &self.data 53 | } 54 | } 55 | 56 | impl InstrumentData for Instrument { 57 | type Id = Self; 58 | 59 | fn id(&self) -> &Self::Id { 60 | self 61 | } 62 | 63 | fn kind(&self) -> InstrumentKind { 64 | self.kind 65 | } 66 | } 67 | 68 | #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)] 69 | pub struct MarketInstrumentData { 70 | pub id: InstrumentId, 71 | pub name_exchange: String, 72 | pub kind: InstrumentKind, 73 | } 74 | 75 | impl InstrumentData for MarketInstrumentData { 76 | type Id = InstrumentId; 77 | 78 | fn id(&self) -> &Self::Id { 79 | &self.id 80 | } 81 | 82 | fn kind(&self) -> InstrumentKind { 83 | self.kind 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /barter-data/src/streams/mod.rs: -------------------------------------------------------------------------------- 1 | use self::builder::{multi::MultiStreamBuilder, StreamBuilder}; 2 | use crate::{exchange::ExchangeId, subscription::SubscriptionKind}; 3 | use std::collections::HashMap; 4 | use tokio::sync::mpsc; 5 | use tokio_stream::{wrappers::UnboundedReceiverStream, StreamMap}; 6 | 7 | /// Defines the [`StreamBuilder`](builder::StreamBuilder) and 8 | /// [`MultiStreamBuilder`](builder::multi::MultiStreamBuilder) APIs for ergonomically initialising 9 | /// [`MarketStream`](super::MarketStream) [`Streams`]. 10 | pub mod builder; 11 | 12 | /// Central consumer loop functionality used by the [`StreamBuilder`](builder::StreamBuilder) to 13 | /// to drive a re-connecting [`MarketStream`](super::MarketStream). 14 | pub mod consumer; 15 | 16 | /// Ergonomic collection of exchange [`MarketEvent`](crate::event::MarketEvent) receivers. 17 | #[derive(Debug)] 18 | pub struct Streams { 19 | pub streams: HashMap>, 20 | } 21 | 22 | impl Streams { 23 | /// Construct a [`StreamBuilder`] for configuring new 24 | /// [`MarketEvent`](crate::event::MarketEvent) [`Streams`]. 25 | pub fn builder() -> StreamBuilder 26 | where 27 | Kind: SubscriptionKind, 28 | { 29 | StreamBuilder::::new() 30 | } 31 | 32 | /// Construct a [`MultiStreamBuilder`] for configuring new 33 | /// [`MarketEvent`](crate::event::MarketEvent) [`Streams`]. 34 | pub fn builder_multi() -> MultiStreamBuilder { 35 | MultiStreamBuilder::::new() 36 | } 37 | 38 | /// Remove an exchange [`mpsc::UnboundedReceiver`] from the [`Streams`] `HashMap`. 39 | pub fn select(&mut self, exchange: ExchangeId) -> Option> { 40 | self.streams.remove(&exchange) 41 | } 42 | 43 | /// Join all exchange [`mpsc::UnboundedReceiver`] streams into a unified 44 | /// [`mpsc::UnboundedReceiver`]. 45 | pub async fn join(self) -> mpsc::UnboundedReceiver 46 | where 47 | T: Send + 'static, 48 | { 49 | let (joined_tx, joined_rx) = mpsc::unbounded_channel(); 50 | 51 | for mut exchange_rx in self.streams.into_values() { 52 | let joined_tx = joined_tx.clone(); 53 | tokio::spawn(async move { 54 | while let Some(event) = exchange_rx.recv().await { 55 | let _ = joined_tx.send(event); 56 | } 57 | }); 58 | } 59 | 60 | joined_rx 61 | } 62 | 63 | /// Join all exchange [`mpsc::UnboundedReceiver`] streams into a unified [`StreamMap`]. 64 | pub async fn join_map(self) -> StreamMap> { 65 | self.streams 66 | .into_iter() 67 | .fold(StreamMap::new(), |mut map, (exchange, rx)| { 68 | map.insert(exchange, UnboundedReceiverStream::new(rx)); 69 | map 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /barter-data/src/subscriber/mapper.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | exchange::{subscription::ExchangeSub, Connector}, 3 | instrument::InstrumentData, 4 | subscription::{Map, Subscription, SubscriptionKind, SubscriptionMeta}, 5 | Identifier, 6 | }; 7 | use barter_integration::model::SubscriptionId; 8 | use serde::{Deserialize, Serialize}; 9 | use std::collections::HashMap; 10 | 11 | /// Defines how to map a collection of Barter [`Subscription`]s into exchange specific 12 | /// [`SubscriptionMeta`], containing subscription payloads that are sent to the exchange. 13 | pub trait SubscriptionMapper { 14 | fn map( 15 | subscriptions: &[Subscription], 16 | ) -> SubscriptionMeta 17 | where 18 | Exchange: Connector, 19 | Instrument: InstrumentData, 20 | Kind: SubscriptionKind, 21 | Subscription: 22 | Identifier + Identifier; 23 | } 24 | 25 | /// Standard [`SubscriptionMapper`] for 26 | /// [`WebSocket`](barter_integration::protocol::websocket::WebSocket)s suitable for most exchanges. 27 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] 28 | pub struct WebSocketSubMapper; 29 | 30 | impl SubscriptionMapper for WebSocketSubMapper { 31 | fn map( 32 | subscriptions: &[Subscription], 33 | ) -> SubscriptionMeta 34 | where 35 | Exchange: Connector, 36 | Kind: SubscriptionKind, 37 | Instrument: InstrumentData, 38 | Subscription: 39 | Identifier + Identifier, 40 | ExchangeSub: Identifier, 41 | { 42 | // Allocate SubscriptionIds HashMap to track identifiers for each actioned Subscription 43 | let mut instrument_map = Map(HashMap::with_capacity(subscriptions.len())); 44 | 45 | // Map Barter Subscriptions to exchange specific subscriptions 46 | let exchange_subs = subscriptions 47 | .iter() 48 | .map(|subscription| { 49 | // Translate Barter Subscription to exchange specific subscription 50 | let exchange_sub = ExchangeSub::new(subscription); 51 | 52 | // Determine the SubscriptionId associated with this exchange specific subscription 53 | let subscription_id = exchange_sub.id(); 54 | 55 | // Use ExchangeSub SubscriptionId as the link to this Barter Subscription 56 | instrument_map 57 | .0 58 | .insert(subscription_id, subscription.instrument.id().clone()); 59 | 60 | exchange_sub 61 | }) 62 | .collect::>>(); 63 | 64 | // Construct WebSocket message subscriptions requests 65 | let subscriptions = Exchange::requests(exchange_subs); 66 | 67 | SubscriptionMeta { 68 | instrument_map, 69 | subscriptions, 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /barter-data/src/subscriber/mod.rs: -------------------------------------------------------------------------------- 1 | use self::{ 2 | mapper::{SubscriptionMapper, WebSocketSubMapper}, 3 | validator::SubscriptionValidator, 4 | }; 5 | use crate::{ 6 | exchange::Connector, 7 | instrument::InstrumentData, 8 | subscription::{Map, Subscription, SubscriptionKind, SubscriptionMeta}, 9 | Identifier, 10 | }; 11 | use async_trait::async_trait; 12 | use barter_integration::{ 13 | error::SocketError, 14 | protocol::websocket::{connect, WebSocket}, 15 | }; 16 | use futures::SinkExt; 17 | use serde::{Deserialize, Serialize}; 18 | use tracing::{debug, info}; 19 | 20 | /// [`SubscriptionMapper`] implementations defining how to map a 21 | /// collection of Barter [`Subscription`]s into exchange specific [`SubscriptionMeta`]. 22 | pub mod mapper; 23 | 24 | /// [`SubscriptionValidator`] implementations defining how to 25 | /// validate actioned [`Subscription`]s were successful. 26 | pub mod validator; 27 | 28 | /// Defines how to connect to a socket and subscribe to market data streams. 29 | #[async_trait] 30 | pub trait Subscriber { 31 | type SubMapper: SubscriptionMapper; 32 | 33 | async fn subscribe( 34 | subscriptions: &[Subscription], 35 | ) -> Result<(WebSocket, Map), SocketError> 36 | where 37 | Exchange: Connector + Send + Sync, 38 | Kind: SubscriptionKind + Send + Sync, 39 | Instrument: InstrumentData, 40 | Subscription: 41 | Identifier + Identifier; 42 | } 43 | 44 | /// Standard [`Subscriber`] for [`WebSocket`]s suitable for most exchanges. 45 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] 46 | pub struct WebSocketSubscriber; 47 | 48 | #[async_trait] 49 | impl Subscriber for WebSocketSubscriber { 50 | type SubMapper = WebSocketSubMapper; 51 | 52 | async fn subscribe( 53 | subscriptions: &[Subscription], 54 | ) -> Result<(WebSocket, Map), SocketError> 55 | where 56 | Exchange: Connector + Send + Sync, 57 | Kind: SubscriptionKind + Send + Sync, 58 | Instrument: InstrumentData, 59 | Subscription: 60 | Identifier + Identifier, 61 | { 62 | // Define variables for logging ergonomics 63 | let exchange = Exchange::ID; 64 | let url = Exchange::url()?; 65 | debug!(%exchange, %url, ?subscriptions, "subscribing to WebSocket"); 66 | 67 | // Connect to exchange 68 | let mut websocket = connect(url).await?; 69 | debug!(%exchange, ?subscriptions, "connected to WebSocket"); 70 | 71 | // Map &[Subscription] to SubscriptionMeta 72 | let SubscriptionMeta { 73 | instrument_map, 74 | subscriptions, 75 | } = Self::SubMapper::map::(subscriptions); 76 | 77 | // Send Subscriptions over WebSocket 78 | for subscription in subscriptions { 79 | debug!(%exchange, payload = ?subscription, "sending exchange subscription"); 80 | websocket.send(subscription).await?; 81 | } 82 | 83 | // Validate Subscription responses 84 | let map = Exchange::SubValidator::validate::( 85 | instrument_map, 86 | &mut websocket, 87 | ) 88 | .await?; 89 | 90 | info!(%exchange, "subscribed to WebSocket"); 91 | Ok((websocket, map)) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /barter-data/src/subscription/candle.rs: -------------------------------------------------------------------------------- 1 | use super::SubscriptionKind; 2 | use chrono::{DateTime, Utc}; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// Barter [`Subscription`](super::Subscription) [`SubscriptionKind`] that yields [`Candle`] 6 | /// [`MarketEvent`](crate::event::MarketEvent) events. 7 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] 8 | pub struct Candles; 9 | 10 | impl SubscriptionKind for Candles { 11 | type Event = Candle; 12 | } 13 | 14 | /// Normalised Barter OHLCV [`Candle`] model. 15 | #[derive(Copy, Clone, PartialEq, PartialOrd, Debug, Deserialize, Serialize)] 16 | pub struct Candle { 17 | pub close_time: DateTime, 18 | pub open: f64, 19 | pub high: f64, 20 | pub low: f64, 21 | pub close: f64, 22 | pub volume: f64, 23 | pub trade_count: u64, 24 | } 25 | -------------------------------------------------------------------------------- /barter-data/src/subscription/liquidation.rs: -------------------------------------------------------------------------------- 1 | use super::SubscriptionKind; 2 | use barter_integration::model::Side; 3 | use chrono::{DateTime, Utc}; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | /// Barter [`Subscription`](super::Subscription) [`SubscriptionKind`] that yields [`Liquidation`] 7 | /// [`MarketEvent`](crate::event::MarketEvent) events. 8 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] 9 | pub struct Liquidations; 10 | 11 | impl SubscriptionKind for Liquidations { 12 | type Event = Liquidation; 13 | } 14 | 15 | /// Normalised Barter [`Liquidation`] model. 16 | #[derive(Clone, Copy, PartialEq, PartialOrd, Debug, Deserialize, Serialize)] 17 | pub struct Liquidation { 18 | pub side: Side, 19 | pub price: f64, 20 | pub quantity: f64, 21 | pub time: DateTime, 22 | } 23 | -------------------------------------------------------------------------------- /barter-data/src/subscription/trade.rs: -------------------------------------------------------------------------------- 1 | use super::SubscriptionKind; 2 | use barter_integration::model::Side; 3 | use barter_macro::{DeSubKind, SerSubKind}; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | /// Barter [`Subscription`](super::Subscription) [`SubscriptionKind`] that yields [`PublicTrade`] 7 | /// [`MarketEvent`](crate::event::MarketEvent) events. 8 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, DeSubKind, SerSubKind)] 9 | pub struct PublicTrades; 10 | 11 | impl SubscriptionKind for PublicTrades { 12 | type Event = PublicTrade; 13 | } 14 | 15 | /// Normalised Barter [`PublicTrade`] model. 16 | #[derive(Clone, PartialEq, PartialOrd, Debug, Deserialize, Serialize)] 17 | pub struct PublicTrade { 18 | pub id: String, 19 | pub price: f64, 20 | pub amount: f64, 21 | pub side: Side, 22 | } 23 | -------------------------------------------------------------------------------- /barter-data/src/transformer/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | error::DataError, 3 | event::MarketEvent, 4 | subscription::{Map, SubscriptionKind}, 5 | }; 6 | use async_trait::async_trait; 7 | use barter_integration::{protocol::websocket::WsMessage, Transformer}; 8 | use tokio::sync::mpsc; 9 | 10 | /// Generic OrderBook [`ExchangeTransformer`]s. 11 | pub mod book; 12 | 13 | /// Generic stateless [`ExchangeTransformer`] often used for transforming 14 | /// [`PublicTrades`](crate::subscription::trade::PublicTrades) streams. 15 | pub mod stateless; 16 | 17 | /// Defines how to construct a [`Transformer`] used by [`MarketStream`](super::MarketStream)s to 18 | /// translate exchange specific types to normalised Barter types. 19 | #[async_trait] 20 | pub trait ExchangeTransformer 21 | where 22 | Self: Transformer, Error = DataError> + Sized, 23 | Kind: SubscriptionKind, 24 | { 25 | /// Construct a new [`Self`]. 26 | /// 27 | /// The [`mpsc::UnboundedSender`] can be used by [`Self`] to send messages back to the exchange. 28 | async fn new( 29 | ws_sink_tx: mpsc::UnboundedSender, 30 | instrument_map: Map, 31 | ) -> Result; 32 | } 33 | -------------------------------------------------------------------------------- /barter-data/src/transformer/stateless.rs: -------------------------------------------------------------------------------- 1 | use super::ExchangeTransformer; 2 | use crate::{ 3 | error::DataError, 4 | event::{MarketEvent, MarketIter}, 5 | exchange::{Connector, ExchangeId}, 6 | subscription::{Map, SubscriptionKind}, 7 | Identifier, 8 | }; 9 | use async_trait::async_trait; 10 | use barter_integration::{model::SubscriptionId, protocol::websocket::WsMessage, Transformer}; 11 | use serde::{Deserialize, Serialize}; 12 | use std::marker::PhantomData; 13 | use tokio::sync::mpsc; 14 | 15 | /// Standard generic stateless [`ExchangeTransformer`] to translate exchange specific types into 16 | /// normalised Barter types. Often used with 17 | /// [`PublicTrades`](crate::subscription::trade::PublicTrades) or 18 | /// [`OrderBooksL1`](crate::subscription::book::OrderBooksL1) streams. 19 | #[derive(Clone, Eq, PartialEq, Debug, Serialize)] 20 | pub struct StatelessTransformer { 21 | instrument_map: Map, 22 | phantom: PhantomData<(Exchange, Kind, Input)>, 23 | } 24 | 25 | #[async_trait] 26 | impl ExchangeTransformer 27 | for StatelessTransformer 28 | where 29 | Exchange: Connector + Send, 30 | InstrumentId: Clone + Send, 31 | Kind: SubscriptionKind + Send, 32 | Input: Identifier> + for<'de> Deserialize<'de>, 33 | MarketIter: From<(ExchangeId, InstrumentId, Input)>, 34 | { 35 | async fn new( 36 | _: mpsc::UnboundedSender, 37 | instrument_map: Map, 38 | ) -> Result { 39 | Ok(Self { 40 | instrument_map, 41 | phantom: PhantomData, 42 | }) 43 | } 44 | } 45 | 46 | impl Transformer 47 | for StatelessTransformer 48 | where 49 | Exchange: Connector, 50 | InstrumentId: Clone, 51 | Kind: SubscriptionKind, 52 | Input: Identifier> + for<'de> Deserialize<'de>, 53 | MarketIter: From<(ExchangeId, InstrumentId, Input)>, 54 | { 55 | type Error = DataError; 56 | type Input = Input; 57 | type Output = MarketEvent; 58 | type OutputIter = Vec>; 59 | 60 | fn transform(&mut self, input: Self::Input) -> Self::OutputIter { 61 | // Determine if the message has an identifiable SubscriptionId 62 | let subscription_id = match input.id() { 63 | Some(subscription_id) => subscription_id, 64 | None => return vec![], 65 | }; 66 | 67 | // Find Instrument associated with Input and transform 68 | match self.instrument_map.find(&subscription_id) { 69 | Ok(instrument) => { 70 | MarketIter::::from(( 71 | Exchange::ID, 72 | instrument.clone(), 73 | input, 74 | )) 75 | .0 76 | } 77 | Err(unidentifiable) => vec![Err(DataError::Socket(unidentifiable))], 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /barter-execution/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "barter-execution" 3 | version = "0.3.0" 4 | edition = "2021" 5 | authors = ["Just A Stream <93921983+just-a-stream@users.noreply.github.com>"] 6 | license = "MIT" 7 | documentation = "https://docs.rs/barter-execution/" 8 | repository = "https://github.com/barter-rs/barter-rs" 9 | readme = "README.md" 10 | description = "High-performance and normalised trading interface capable of executing across many financial venues. Also provides a feature rich simulated exchange." 11 | keywords = ["trading", "backtesting", "crypto", "stocks", "investment"] 12 | categories = ["accessibility", "simulation"] 13 | 14 | 15 | [dependencies] 16 | # Barter Ecosystem 17 | barter-integration = { path = "../barter-integration", version = "0.7.3" } 18 | barter-data = { path = "../barter-data", version = "0.8.1" } 19 | 20 | # Logging 21 | tracing = { workspace = true } 22 | 23 | # Async 24 | tokio = { workspace = true, features = ["sync", "macros", "rt-multi-thread"] } 25 | tokio-stream = { workspace = true, features = ["sync"] } 26 | tokio-tungstenite = { workspace = true, features = ["rustls-tls-webpki-roots"] } 27 | futures = { workspace = true } 28 | async-trait = { workspace = true } 29 | pin-project = { workspace = true } 30 | 31 | # Error 32 | thiserror = { workspace = true } 33 | 34 | # SerDe 35 | serde = { workspace = true, features = ["derive"] } 36 | serde_json = { workspace = true } 37 | 38 | # Protocol 39 | reqwest = { workspace = true, features = ["rustls-tls", "json"] } 40 | 41 | # Data Structures 42 | parking_lot = { workspace = true } 43 | 44 | # Misc 45 | uuid = { workspace = true, features = ["v4", "serde"]} 46 | chrono = { workspace = true, features = ["serde"]} 47 | #num-traits = "0.2.15" 48 | -------------------------------------------------------------------------------- /barter-execution/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Barter-Execution Contributors 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /barter-execution/README.md: -------------------------------------------------------------------------------- 1 | # Barter-Execution 2 | High-performance and normalised trading interface capable of executing across many financial venues. Also provides 3 | a feature rich simulated exchange to assist with backtesting and dry-trading. 4 | 5 | **It is:** 6 | * **Easy**: ExecutionClient trait provides a unified and simple language for interacting with exchanges. 7 | * **Normalised**: Allow your strategy to communicate with every real or simulated exchange using the same interface. 8 | * **Extensible**: Barter-Execution is highly extensible, making it easy to contribute by adding new exchange integrations! 9 | 10 | **See: [`Barter`], [`Barter-Integration`], [`Barter-Data`]** 11 | 12 | [![Crates.io][crates-badge]][crates-url] 13 | [![MIT licensed][mit-badge]][mit-url] 14 | [![Build Status][actions-badge]][actions-url] 15 | [![Discord chat][discord-badge]][discord-url] 16 | 17 | [crates-badge]: https://img.shields.io/crates/v/barter-execution.svg 18 | [crates-url]: https://crates.io/crates/barter-execution 19 | 20 | [mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg 21 | [mit-url]: https://gitlab.com/open-source-keir/financial-modelling/trading/barter-execution-rs/-/blob/main/LICENCE 22 | 23 | [actions-badge]: https://gitlab.com/open-source-keir/financial-modelling/trading/barter-execution-rs/badges/-/blob/main/pipeline.svg 24 | [actions-url]: https://gitlab.com/open-source-keir/financial-modelling/trading/barter-execution-rs/-/commits/main 25 | 26 | [discord-badge]: https://img.shields.io/discord/910237311332151317.svg?logo=discord&style=flat-square 27 | [discord-url]: https://discord.gg/wE7RqhnQMV 28 | 29 | [API Documentation] | [Chat] 30 | 31 | [`Barter`]: https://crates.io/crates/barter 32 | [`Barter-Integration`]: https://crates.io/crates/barter-integration 33 | [`Barter-Data`]: https://crates.io/crates/barter-data 34 | [API Documentation]: https://docs.rs/barter-execution/latest/barter_execution 35 | [Chat]: https://discord.gg/wE7RqhnQMV 36 | 37 | ## Overview 38 | High-performance and normalised trading interface capable of executing across many financial venues. Also provides 39 | a feature rich simulated exchange to assist with backtesting and dry-trading. Communicate with an exchange by 40 | initialising it's associated `ExecutionClient` instance. 41 | 42 | 43 | ## Example 44 | For now, see `tests/simulated_exchange.rs` for a taste. 45 | 46 | ## Getting Help 47 | Firstly, see if the answer to your question can be found in the [API Documentation]. If the answer is not there, I'd be 48 | happy to help to [Chat] and try answer your question via Discord. 49 | 50 | ## Contributing 51 | Thanks for your help in improving the Barter ecosystem! Please do get in touch on the discord to discuss 52 | development, new features, and the future roadmap. 53 | * **Implement the `ExecutionClient` trait to integrate a new exchange.** 54 | 55 | ## Related Projects 56 | In addition to the Barter-Execution crate, the Barter project also maintains: 57 | * [`Barter`]: High-performance, extensible & modular trading components with batteries-included. Contains a 58 | pre-built trading Engine that can serve as a live-trading or backtesting system. 59 | * [`Barter-Integration`]: High-performance, low-level framework for composing flexible web integrations. 60 | * [`Barter-Data`]: High-performance WebSocket integration library for streaming public market data from leading 61 | cryptocurrency exchanges. 62 | 63 | ## Roadmap 64 | * Improve sophistication of the SimulatedExchange. 65 | * `OrderKind::Market` & `OrderKind::ImmediateOrCancel` execution support. 66 | * Enable many clients to use the exchange simultaneously, pathing the way for simulated order books generated by 67 | many market archetype actors. 68 | * Add many more `ExecutionClient` implementations for exchanges. 69 | 70 | ## Licence 71 | This project is licensed under the [MIT license]. 72 | 73 | [MIT license]: https://gitlab.com/open-source-keir/financial-modelling/trading/barter-execution-rs/-/blob/main/LICENSE 74 | 75 | ### Contribution 76 | Unless you explicitly state otherwise, any contribution intentionally submitted 77 | for inclusion in Barter-Data by you, shall be licensed as MIT, without any additional 78 | terms or conditions. 79 | -------------------------------------------------------------------------------- /barter-execution/src/error.rs: -------------------------------------------------------------------------------- 1 | use crate::model::{order::OrderKind, ClientOrderId}; 2 | use barter_integration::model::instrument::symbol::Symbol; 3 | use serde::{Deserialize, Serialize}; 4 | use thiserror::Error; 5 | 6 | #[derive(Error, PartialEq, Eq, PartialOrd, Debug, Clone, Deserialize, Serialize)] 7 | pub enum ExecutionError { 8 | #[error("failed to build component due to missing attributes: {0}")] 9 | BuilderIncomplete(String), 10 | 11 | #[error("SimulatedExchange error: {0}")] 12 | Simulated(String), 13 | 14 | #[error("Balance for symbol {0} insufficient to open order")] 15 | InsufficientBalance(Symbol), 16 | 17 | #[error("failed to find Order with ClientOrderId: {0}")] 18 | OrderNotFound(ClientOrderId), 19 | 20 | #[error("failed to open Order due to unsupported OrderKind: {0}")] 21 | UnsupportedOrderKind(OrderKind), 22 | } 23 | -------------------------------------------------------------------------------- /barter-execution/src/execution/binance/mod.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /barter-execution/src/execution/ftx/mod.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /barter-execution/src/execution/mod.rs: -------------------------------------------------------------------------------- 1 | /// `Binance` & `BinanceFuturesUsd` [`ExecutionClient`](crate::ExecutionClient) implementations. 2 | pub mod binance; 3 | 4 | /// `Ftx` [`ExecutionClient`](crate::ExecutionClient) implementation. 5 | pub mod ftx; 6 | -------------------------------------------------------------------------------- /barter-execution/src/model/mod.rs: -------------------------------------------------------------------------------- 1 | use self::{ 2 | balance::SymbolBalance, 3 | order::{Cancelled, Open, Order}, 4 | trade::Trade, 5 | }; 6 | use barter_integration::model::Exchange; 7 | use chrono::{DateTime, Utc}; 8 | use serde::{Deserialize, Serialize}; 9 | use std::fmt::Formatter; 10 | use uuid::Uuid; 11 | 12 | pub mod balance; 13 | pub mod order; 14 | pub mod trade; 15 | 16 | /// Normalised Barter [`AccountEvent`] containing metadata about the included 17 | /// [`AccountEventKind`] variant. Produced by [`ExecutionClients`](crate::ExecutionClient). 18 | #[derive(Clone, Debug, Deserialize, Serialize)] 19 | pub struct AccountEvent { 20 | pub received_time: DateTime, 21 | pub exchange: Exchange, 22 | pub kind: AccountEventKind, 23 | } 24 | 25 | /// Defines the type of Barter [`AccountEvent`]. 26 | #[derive(Clone, Debug, Deserialize, Serialize)] 27 | pub enum AccountEventKind { 28 | // HTTP Only 29 | OrdersOpen(Vec>), 30 | OrdersNew(Vec>), 31 | OrdersCancelled(Vec>), 32 | 33 | // WebSocket Only 34 | Balance(SymbolBalance), 35 | Trade(Trade), 36 | 37 | // HTTP & WebSocket 38 | Balances(Vec), 39 | } 40 | 41 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] 42 | pub struct ClientOrderId(pub Uuid); 43 | 44 | impl std::fmt::Display for ClientOrderId { 45 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 46 | write!(f, "{}", self.0) 47 | } 48 | } 49 | 50 | #[derive(Clone, Copy, Debug)] 51 | pub enum ClientStatus { 52 | Connected, 53 | CancelOnly, 54 | Disconnected, 55 | } 56 | -------------------------------------------------------------------------------- /barter-execution/src/model/trade.rs: -------------------------------------------------------------------------------- 1 | use super::order::OrderId; 2 | use barter_integration::model::{ 3 | instrument::{symbol::Symbol, Instrument}, 4 | Side, 5 | }; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | /// Normalised Barter private [`Trade`] model. 9 | #[derive(Clone, PartialEq, PartialOrd, Debug, Deserialize, Serialize)] 10 | pub struct Trade { 11 | pub id: TradeId, 12 | pub order_id: OrderId, 13 | pub instrument: Instrument, 14 | pub side: Side, 15 | pub price: f64, 16 | pub quantity: f64, 17 | pub fees: SymbolFees, 18 | } 19 | 20 | /// Private [`Trade`] identifier generated by an exchange. Cannot assume this is unique across each 21 | /// [`Exchange`](barter_integration::model::Exchange), 22 | /// [`Market`](barter_integration::model::Market), or 23 | /// [`Instrument`](barter_integration::model::Instrument). 24 | #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] 25 | pub struct TradeId(pub String); 26 | 27 | impl From for TradeId 28 | where 29 | S: Into, 30 | { 31 | fn from(id: S) -> Self { 32 | Self(id.into()) 33 | } 34 | } 35 | 36 | /// [`Trade`] fees denominated in a [`Symbol`]. 37 | #[derive(Clone, PartialEq, PartialOrd, Debug, Deserialize, Serialize)] 38 | pub struct SymbolFees { 39 | pub symbol: Symbol, 40 | pub fees: f64, 41 | } 42 | 43 | impl SymbolFees { 44 | /// Construct a new [`SymbolFees`]. 45 | pub fn new(symbol: S, fees: f64) -> Self 46 | where 47 | S: Into, 48 | { 49 | Self { 50 | symbol: symbol.into(), 51 | fees, 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /barter-execution/src/simulated/exchange/mod.rs: -------------------------------------------------------------------------------- 1 | use super::{exchange::account::ClientAccount, SimulatedEvent}; 2 | use crate::ExecutionError; 3 | use tokio::sync::mpsc; 4 | 5 | /// [`SimulatedExchange`] account balances, open orders, fees, and latency. 6 | pub mod account; 7 | 8 | /// [`SimulatedExchange`] that responds to [`SimulatedEvent`]s. 9 | #[derive(Debug)] 10 | pub struct SimulatedExchange { 11 | pub event_simulated_rx: mpsc::UnboundedReceiver, 12 | pub account: ClientAccount, 13 | } 14 | 15 | impl SimulatedExchange { 16 | /// Construct a [`ExchangeBuilder`] for configuring a new [`SimulatedExchange`]. 17 | pub fn builder() -> ExchangeBuilder { 18 | ExchangeBuilder::new() 19 | } 20 | 21 | /// Run the [`SimulatedExchange`] by responding to [`SimulatedEvent`]s. 22 | pub async fn run(mut self) { 23 | while let Some(event) = self.event_simulated_rx.recv().await { 24 | match event { 25 | SimulatedEvent::FetchOrdersOpen(response_tx) => { 26 | self.account.fetch_orders_open(response_tx) 27 | } 28 | SimulatedEvent::FetchBalances(response_tx) => { 29 | self.account.fetch_balances(response_tx) 30 | } 31 | SimulatedEvent::OpenOrders((open_requests, response_tx)) => { 32 | self.account.open_orders(open_requests, response_tx) 33 | } 34 | SimulatedEvent::CancelOrders((cancel_requests, response_tx)) => { 35 | self.account.cancel_orders(cancel_requests, response_tx) 36 | } 37 | SimulatedEvent::CancelOrdersAll(response_tx) => { 38 | self.account.cancel_orders_all(response_tx) 39 | } 40 | SimulatedEvent::MarketTrade((instrument, trade)) => { 41 | self.account.match_orders(instrument, trade) 42 | } 43 | } 44 | } 45 | } 46 | } 47 | 48 | #[derive(Debug, Default)] 49 | pub struct ExchangeBuilder { 50 | event_simulated_rx: Option>, 51 | account: Option, 52 | } 53 | 54 | impl ExchangeBuilder { 55 | fn new() -> Self { 56 | Self { 57 | ..Default::default() 58 | } 59 | } 60 | 61 | pub fn event_simulated_rx(self, value: mpsc::UnboundedReceiver) -> Self { 62 | Self { 63 | event_simulated_rx: Some(value), 64 | ..self 65 | } 66 | } 67 | 68 | pub fn account(self, value: ClientAccount) -> Self { 69 | Self { 70 | account: Some(value), 71 | ..self 72 | } 73 | } 74 | 75 | pub fn build(self) -> Result { 76 | Ok(SimulatedExchange { 77 | event_simulated_rx: self.event_simulated_rx.ok_or_else(|| { 78 | ExecutionError::BuilderIncomplete("event_simulated_rx".to_string()) 79 | })?, 80 | account: self 81 | .account 82 | .ok_or_else(|| ExecutionError::BuilderIncomplete("account".to_string()))?, 83 | }) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /barter-execution/src/simulated/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{Cancelled, ExecutionError, Open, Order, RequestCancel, RequestOpen, SymbolBalance}; 2 | use barter_data::subscription::trade::PublicTrade; 3 | use barter_integration::model::instrument::Instrument; 4 | use tokio::sync::oneshot; 5 | 6 | /// Simulated Exchange using public trade `Streams` to model available market liquidity. Liquidity 7 | /// is then used to match to open client orders. 8 | pub mod exchange; 9 | 10 | /// Simulated [`ExecutionClient`](crate::ExecutionClient) implementation that integrates with the 11 | /// Barter [`SimulatedExchange`](exchange::SimulatedExchange). 12 | pub mod execution; 13 | 14 | /// Events used to communicate with the Barter [`SimulatedExchange`](exchange::SimulatedExchange). 15 | /// 16 | /// Two main types of [`SimulatedEvent`]: 17 | /// 1. Request sent from the [`SimulatedExecution`](execution::SimulatedExecution) 18 | /// [`ExecutionClient`](crate::ExecutionClient). 19 | /// 2. Market events used to model available liquidity and trigger matches with open client orders. 20 | #[derive(Debug)] 21 | pub enum SimulatedEvent { 22 | FetchOrdersOpen(oneshot::Sender>, ExecutionError>>), 23 | FetchBalances(oneshot::Sender, ExecutionError>>), 24 | OpenOrders( 25 | ( 26 | Vec>, 27 | oneshot::Sender, ExecutionError>>>, 28 | ), 29 | ), 30 | CancelOrders( 31 | ( 32 | Vec>, 33 | oneshot::Sender, ExecutionError>>>, 34 | ), 35 | ), 36 | CancelOrdersAll(oneshot::Sender>, ExecutionError>>), 37 | MarketTrade((Instrument, PublicTrade)), 38 | } 39 | -------------------------------------------------------------------------------- /barter-integration/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "barter-integration" 3 | version = "0.7.3" 4 | authors = ["JustAStream"] 5 | edition = "2021" 6 | license = "MIT" 7 | documentation = "https://docs.rs/barter-integration/" 8 | repository = "https://github.com/barter-rs/barter-rs" 9 | readme = "README.md" 10 | description = "Low-level framework for composing flexible web integrations, especially with financial exchanges" 11 | keywords = ["trading", "backtesting", "crypto", "stocks", "investment"] 12 | categories = ["accessibility", "simulation"] 13 | 14 | [dev-dependencies] 15 | rust_decimal_macros = { workspace = true } 16 | 17 | [dependencies] 18 | # Logging 19 | tracing = { workspace = true } 20 | 21 | # SerDe 22 | serde = { workspace = true, features = ["derive"] } 23 | serde_json = { workspace = true } 24 | serde_qs = { workspace = true } 25 | serde_urlencoded = { workspace = true } 26 | 27 | # Error 28 | thiserror = { workspace = true } 29 | 30 | # Async 31 | tokio = { workspace = true, features = ["net", "sync", "macros", "rt-multi-thread"] } 32 | futures = { workspace = true } 33 | async-trait = { workspace = true } 34 | pin-project = { workspace = true } 35 | 36 | # Protocol 37 | tokio-tungstenite = { workspace = true, features = ["rustls-tls-webpki-roots"] } 38 | reqwest = { workspace = true, features = ["json"] } 39 | url = { workspace = true } 40 | 41 | # Cryptographic Signatures 42 | hmac = { workspace = true } 43 | sha2 = { workspace = true } 44 | hex = { workspace = true } 45 | base64 = { workspace = true } 46 | 47 | # Misc 48 | chrono = { workspace = true, features = ["serde"] } 49 | bytes = { workspace = true } 50 | rust_decimal = { workspace = true } -------------------------------------------------------------------------------- /barter-integration/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Barter-Integration Contributors 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /barter-integration/examples/simple_websocket_integration.rs: -------------------------------------------------------------------------------- 1 | use barter_integration::{ 2 | error::SocketError, 3 | protocol::websocket::{WebSocket, WebSocketParser, WsMessage}, 4 | ExchangeStream, Transformer, 5 | }; 6 | use futures::{SinkExt, StreamExt}; 7 | use serde::{de, Deserialize}; 8 | use serde_json::json; 9 | use std::str::FromStr; 10 | use tokio_tungstenite::connect_async; 11 | use tracing::debug; 12 | 13 | // Convenient type alias for an `ExchangeStream` utilising a tungstenite `WebSocket` 14 | type ExchangeWsStream = ExchangeStream; 15 | 16 | // Communicative type alias for what the VolumeSum the Transformer is generating 17 | type VolumeSum = f64; 18 | 19 | #[derive(Deserialize)] 20 | #[serde(untagged, rename_all = "camelCase")] 21 | enum BinanceMessage { 22 | SubResponse { 23 | result: Option>, 24 | id: u32, 25 | }, 26 | Trade { 27 | #[serde(rename = "q", deserialize_with = "de_str")] 28 | quantity: f64, 29 | }, 30 | } 31 | 32 | struct StatefulTransformer { 33 | sum_of_volume: VolumeSum, 34 | } 35 | 36 | impl Transformer for StatefulTransformer { 37 | type Error = SocketError; 38 | type Input = BinanceMessage; 39 | type Output = VolumeSum; 40 | type OutputIter = Vec>; 41 | 42 | fn transform(&mut self, input: Self::Input) -> Self::OutputIter { 43 | // Add new input Trade quantity to sum 44 | match input { 45 | BinanceMessage::SubResponse { result, id } => { 46 | debug!("Received SubResponse for {}: {:?}", id, result); 47 | // Don't care about this for the example 48 | } 49 | BinanceMessage::Trade { quantity, .. } => { 50 | // Add new Trade volume to internal state VolumeSum 51 | self.sum_of_volume += quantity; 52 | } 53 | }; 54 | 55 | // Return IntoIterator of length 1 containing the running sum of volume 56 | vec![Ok(self.sum_of_volume)] 57 | } 58 | } 59 | 60 | /// See Barter-Data for a comprehensive real-life example, as well as code you can use out of the 61 | /// box to collect real-time public market data from many exchanges. 62 | #[tokio::main] 63 | async fn main() { 64 | // Establish Sink/Stream communication with desired WebSocket server 65 | let mut binance_conn = connect_async("wss://fstream.binance.com/ws/") 66 | .await 67 | .map(|(ws_conn, _)| ws_conn) 68 | .expect("failed to connect"); 69 | 70 | // Send something over the socket (eg/ Binance trades subscription) 71 | binance_conn 72 | .send(WsMessage::Text( 73 | json!({"method": "SUBSCRIBE","params": ["btcusdt@aggTrade"],"id": 1}).to_string(), 74 | )) 75 | .await 76 | .expect("failed to send WsMessage over socket"); 77 | 78 | // Instantiate some arbitrary Transformer to apply to data parsed from the WebSocket protocol 79 | let transformer = StatefulTransformer { sum_of_volume: 0.0 }; 80 | 81 | // ExchangeWsStream includes pre-defined WebSocket Sink/Stream & WebSocket StreamParser 82 | let mut ws_stream = ExchangeWsStream::new(binance_conn, transformer); 83 | 84 | // Receive a stream of your desired Output data model from the ExchangeStream 85 | while let Some(volume_result) = ws_stream.next().await { 86 | match volume_result { 87 | Ok(cumulative_volume) => { 88 | // Do something with your data 89 | println!("{cumulative_volume:?}"); 90 | } 91 | Err(error) => { 92 | // React to any errors produced by the internal transformation 93 | eprintln!("{error}") 94 | } 95 | } 96 | } 97 | } 98 | 99 | /// Deserialize a `String` as the desired type. 100 | fn de_str<'de, D, T>(deserializer: D) -> Result 101 | where 102 | D: de::Deserializer<'de>, 103 | T: FromStr, 104 | T::Err: std::fmt::Display, 105 | { 106 | let data: String = Deserialize::deserialize(deserializer)?; 107 | data.parse::().map_err(de::Error::custom) 108 | } 109 | -------------------------------------------------------------------------------- /barter-integration/src/de.rs: -------------------------------------------------------------------------------- 1 | /// Determine the `DateTime` from the provided `Duration` since the epoch. 2 | pub fn datetime_utc_from_epoch_duration( 3 | duration: std::time::Duration, 4 | ) -> chrono::DateTime { 5 | chrono::DateTime::::from(std::time::UNIX_EPOCH + duration) 6 | } 7 | 8 | /// Deserialize a `String` as the desired type. 9 | pub fn de_str<'de, D, T>(deserializer: D) -> Result 10 | where 11 | D: serde::de::Deserializer<'de>, 12 | T: std::str::FromStr, 13 | T::Err: std::fmt::Display, 14 | { 15 | let data: &str = serde::de::Deserialize::deserialize(deserializer)?; 16 | data.parse::().map_err(serde::de::Error::custom) 17 | } 18 | 19 | /// Deserialize a `u64` milliseconds value as `DateTime`. 20 | pub fn de_u64_epoch_ms_as_datetime_utc<'de, D>( 21 | deserializer: D, 22 | ) -> Result, D::Error> 23 | where 24 | D: serde::de::Deserializer<'de>, 25 | { 26 | serde::de::Deserialize::deserialize(deserializer).map(|epoch_ms| { 27 | datetime_utc_from_epoch_duration(std::time::Duration::from_millis(epoch_ms)) 28 | }) 29 | } 30 | 31 | /// Deserialize a &str "u64" milliseconds value as `DateTime`. 32 | pub fn de_str_u64_epoch_ms_as_datetime_utc<'de, D>( 33 | deserializer: D, 34 | ) -> Result, D::Error> 35 | where 36 | D: serde::de::Deserializer<'de>, 37 | { 38 | de_str(deserializer).map(|epoch_ms| { 39 | datetime_utc_from_epoch_duration(std::time::Duration::from_millis(epoch_ms)) 40 | }) 41 | } 42 | 43 | /// Deserialize a &str "f64" milliseconds value as `DateTime`. 44 | pub fn de_str_f64_epoch_ms_as_datetime_utc<'de, D>( 45 | deserializer: D, 46 | ) -> Result, D::Error> 47 | where 48 | D: serde::de::Deserializer<'de>, 49 | { 50 | de_str(deserializer).map(|epoch_ms: f64| { 51 | datetime_utc_from_epoch_duration(std::time::Duration::from_millis(epoch_ms as u64)) 52 | }) 53 | } 54 | 55 | /// Deserialize a &str "f64" seconds value as `DateTime`. 56 | pub fn de_str_f64_epoch_s_as_datetime_utc<'de, D>( 57 | deserializer: D, 58 | ) -> Result, D::Error> 59 | where 60 | D: serde::de::Deserializer<'de>, 61 | { 62 | de_str(deserializer).map(|epoch_s: f64| { 63 | datetime_utc_from_epoch_duration(std::time::Duration::from_secs_f64(epoch_s)) 64 | }) 65 | } 66 | 67 | /// Assists deserialisation of sequences by attempting to extract & parse the next element in the 68 | /// provided sequence. 69 | /// 70 | /// A [`serde::de::Error`] is returned if the element does not exist, or it cannot 71 | /// be deserialized into the `Target` type inferred. 72 | /// 73 | /// Example sequence: ["20180.30000","0.00010000","1661978265.280067","s","l",""] 74 | pub fn extract_next<'de, SeqAccessor, Target>( 75 | sequence: &mut SeqAccessor, 76 | name: &'static str, 77 | ) -> Result 78 | where 79 | SeqAccessor: serde::de::SeqAccess<'de>, 80 | Target: serde::de::DeserializeOwned, 81 | { 82 | sequence 83 | .next_element::()? 84 | .ok_or_else(|| serde::de::Error::missing_field(name)) 85 | } 86 | 87 | /// Serialize a generic element T as a `Vec`. 88 | pub fn se_element_to_vector(element: T, serializer: S) -> Result 89 | where 90 | S: serde::Serializer, 91 | T: serde::Serialize, 92 | { 93 | use serde::ser::SerializeSeq; 94 | 95 | let mut sequence = serializer.serialize_seq(Some(1))?; 96 | sequence.serialize_element(&element)?; 97 | sequence.end() 98 | } 99 | -------------------------------------------------------------------------------- /barter-integration/src/error.rs: -------------------------------------------------------------------------------- 1 | use crate::model::SubscriptionId; 2 | use reqwest::Error; 3 | use thiserror::Error; 4 | 5 | /// All socket IO related errors generated in `barter-integration`. 6 | #[derive(Debug, Error)] 7 | pub enum SocketError { 8 | #[error("Sink error")] 9 | Sink, 10 | 11 | #[error("Deserialising JSON error: {error} for payload: {payload}")] 12 | Deserialise { 13 | error: serde_json::Error, 14 | payload: String, 15 | }, 16 | 17 | #[error("Deserialising JSON error: {error} for binary payload: {payload:?}")] 18 | DeserialiseBinary { 19 | error: serde_json::Error, 20 | payload: Vec, 21 | }, 22 | 23 | #[error("Serialising JSON error: {0}")] 24 | Serialise(serde_json::Error), 25 | 26 | #[error("SerDe Query String serialisation error: {0}")] 27 | QueryParams(#[from] serde_qs::Error), 28 | 29 | #[error("SerDe url encoding serialisation error: {0}")] 30 | UrlEncoded(#[from] serde_urlencoded::ser::Error), 31 | 32 | #[error("error parsing Url: {0}")] 33 | UrlParse(#[from] url::ParseError), 34 | 35 | #[error("error subscribing to resources over the socket: {0}")] 36 | Subscribe(String), 37 | 38 | #[error("ExchangeStream terminated with closing frame: {0}")] 39 | Terminated(String), 40 | 41 | #[error("{entity} does not support: {item}")] 42 | Unsupported { entity: &'static str, item: String }, 43 | 44 | #[error("WebSocket error: {0}")] 45 | WebSocket(#[from] tokio_tungstenite::tungstenite::Error), 46 | 47 | #[error("HTTP error: {0}")] 48 | Http(reqwest::Error), 49 | 50 | #[error("HTTP request timed out")] 51 | HttpTimeout(reqwest::Error), 52 | 53 | /// REST http response error 54 | #[error("HTTP response (status={0}) error: {1}")] 55 | HttpResponse(reqwest::StatusCode, String), 56 | 57 | #[error("consumed unidentifiable message: {0}")] 58 | Unidentifiable(SubscriptionId), 59 | 60 | #[error("consumed error message from exchange: {0}")] 61 | Exchange(String), 62 | } 63 | 64 | impl From for SocketError { 65 | fn from(error: Error) -> Self { 66 | match error { 67 | error if error.is_timeout() => SocketError::HttpTimeout(error), 68 | error => SocketError::Http(error), 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /barter-integration/src/metric.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Clone, PartialOrd, PartialEq, Serialize)] 4 | pub struct Metric { 5 | /// Metric name. 6 | pub name: &'static str, 7 | 8 | /// Milliseconds since the Unix epoch. 9 | pub time: u64, 10 | 11 | /// Key-Value pairs to categorise the Metric. 12 | pub tags: Vec, 13 | 14 | /// Observed measurements. 15 | pub fields: Vec, 16 | } 17 | 18 | #[derive(Debug, Clone, Serialize, Ord, PartialOrd, Eq, PartialEq)] 19 | pub struct Tag { 20 | pub key: &'static str, 21 | pub value: String, 22 | } 23 | 24 | #[derive(Debug, Clone, PartialOrd, PartialEq, Serialize)] 25 | pub struct Field { 26 | pub key: &'static str, 27 | pub value: Value, 28 | } 29 | 30 | #[derive(Debug, Clone, PartialOrd, PartialEq, Deserialize, Serialize)] 31 | pub enum Value { 32 | Float(f64), 33 | Int(i64), 34 | UInt(u64), 35 | Bool(bool), 36 | String(String), 37 | } 38 | 39 | impl From<(&'static str, S)> for Tag 40 | where 41 | S: Into, 42 | { 43 | fn from((key, value): (&'static str, S)) -> Self { 44 | Self::new(key, value) 45 | } 46 | } 47 | 48 | impl Tag { 49 | pub fn new(key: &'static str, value: S) -> Self 50 | where 51 | S: Into, 52 | { 53 | Self { 54 | key, 55 | value: value.into(), 56 | } 57 | } 58 | } 59 | 60 | impl From<(&'static str, S)> for Field 61 | where 62 | S: Into, 63 | { 64 | fn from((key, value): (&'static str, S)) -> Self { 65 | Self::new(key, value) 66 | } 67 | } 68 | 69 | impl Field { 70 | pub fn new(key: &'static str, value: S) -> Self 71 | where 72 | S: Into, 73 | { 74 | Self { 75 | key, 76 | value: value.into(), 77 | } 78 | } 79 | } 80 | 81 | impl From for Value { 82 | fn from(value: f64) -> Self { 83 | Self::Float(value) 84 | } 85 | } 86 | 87 | impl From for Value { 88 | fn from(value: i64) -> Self { 89 | Self::Int(value) 90 | } 91 | } 92 | 93 | impl From for Value { 94 | fn from(value: u64) -> Self { 95 | Self::UInt(value) 96 | } 97 | } 98 | 99 | impl From for Value { 100 | fn from(value: bool) -> Self { 101 | Self::Bool(value) 102 | } 103 | } 104 | 105 | impl From for Value { 106 | fn from(value: String) -> Self { 107 | Self::String(value) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /barter-integration/src/model/instrument/kind.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use rust_decimal::Decimal; 3 | use serde::{Deserialize, Serialize}; 4 | use std::fmt::{Display, Formatter}; 5 | 6 | /// Defines the type of [`Instrument`](super::Instrument) which is being traded on a 7 | /// given `base_quote` market. 8 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] 9 | #[serde(rename_all = "snake_case")] 10 | pub enum InstrumentKind { 11 | Spot, 12 | Future(FutureContract), 13 | Perpetual, 14 | Option(OptionContract), 15 | } 16 | 17 | impl Default for InstrumentKind { 18 | fn default() -> Self { 19 | Self::Spot 20 | } 21 | } 22 | 23 | impl Display for InstrumentKind { 24 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 25 | write!( 26 | f, 27 | "{}", 28 | match self { 29 | InstrumentKind::Spot => "spot".to_string(), 30 | InstrumentKind::Future(future) => 31 | format!("future_{}-UTC", future.expiry.date_naive()), 32 | InstrumentKind::Perpetual => "perpetual".to_string(), 33 | InstrumentKind::Option(option) => format!( 34 | "option_{}_{}_{}-UTC_{}", 35 | option.kind, 36 | option.exercise, 37 | option.expiry.date_naive(), 38 | option.strike, 39 | ), 40 | } 41 | ) 42 | } 43 | } 44 | 45 | /// Configuration of an [`InstrumentKind::Future`] contract. 46 | #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Deserialize, Serialize)] 47 | pub struct FutureContract { 48 | #[serde(with = "chrono::serde::ts_milliseconds")] 49 | pub expiry: DateTime, 50 | } 51 | 52 | /// Configuration of an [`InstrumentKind::Option`] contract. 53 | #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Deserialize, Serialize)] 54 | pub struct OptionContract { 55 | pub kind: OptionKind, 56 | pub exercise: OptionExercise, 57 | #[serde(with = "chrono::serde::ts_milliseconds")] 58 | pub expiry: DateTime, 59 | pub strike: Decimal, 60 | } 61 | 62 | /// [`OptionContract`] kind - Put or Call. 63 | #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Deserialize, Serialize)] 64 | #[serde(rename_all = "lowercase")] 65 | pub enum OptionKind { 66 | #[serde(alias = "CALL", alias = "Call")] 67 | Call, 68 | #[serde(alias = "PUT", alias = "Put")] 69 | Put, 70 | } 71 | 72 | impl Display for OptionKind { 73 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 74 | write!( 75 | f, 76 | "{}", 77 | match self { 78 | OptionKind::Call => "call", 79 | OptionKind::Put => "put", 80 | } 81 | ) 82 | } 83 | } 84 | 85 | /// [`OptionContract`] exercise style. 86 | #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Deserialize, Serialize)] 87 | #[serde(rename_all = "lowercase")] 88 | pub enum OptionExercise { 89 | #[serde(alias = "AMERICAN", alias = "American")] 90 | American, 91 | #[serde(alias = "BERMUDAN", alias = "Bermudan")] 92 | Bermudan, 93 | #[serde(alias = "EUROPEAN", alias = "European")] 94 | European, 95 | } 96 | 97 | impl Display for OptionExercise { 98 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 99 | write!( 100 | f, 101 | "{}", 102 | match self { 103 | OptionExercise::American => "american", 104 | OptionExercise::Bermudan => "bermudan", 105 | OptionExercise::European => "european", 106 | } 107 | ) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /barter-integration/src/model/instrument/symbol.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Deserializer, Serialize}; 2 | use std::fmt::{Debug, Display, Formatter}; 3 | 4 | /// Barter new type representing a currency symbol `String` identifier. 5 | /// 6 | /// eg/ "btc", "eth", "usdt", etc 7 | #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize)] 8 | pub struct Symbol(String); 9 | 10 | impl Debug for Symbol { 11 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 12 | write!(f, "{}", self.0) 13 | } 14 | } 15 | 16 | impl Display for Symbol { 17 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 18 | write!(f, "{}", self.0) 19 | } 20 | } 21 | 22 | impl AsRef for Symbol { 23 | fn as_ref(&self) -> &str { 24 | &self.0 25 | } 26 | } 27 | 28 | impl<'de> Deserialize<'de> for Symbol { 29 | fn deserialize(deserializer: D) -> Result 30 | where 31 | D: Deserializer<'de>, 32 | { 33 | String::deserialize(deserializer).map(Symbol::new) 34 | } 35 | } 36 | 37 | impl From for Symbol 38 | where 39 | S: Into, 40 | { 41 | fn from(input: S) -> Self { 42 | Symbol::new(input) 43 | } 44 | } 45 | 46 | impl Symbol { 47 | /// Construct a new [`Symbol`] new type using the provided `Into` value. 48 | pub fn new(input: S) -> Self 49 | where 50 | S: Into, 51 | { 52 | Self(input.into().to_lowercase()) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /barter-integration/src/protocol/http/mod.rs: -------------------------------------------------------------------------------- 1 | use self::rest::RestRequest; 2 | use crate::error::SocketError; 3 | use reqwest::StatusCode; 4 | use serde::de::DeserializeOwned; 5 | use tracing::error; 6 | 7 | /// Defines an abstract [`RestRequest`] that can be executed by a fully 8 | /// configurable [`RestClient`](rest::client::RestClient). 9 | pub mod rest; 10 | 11 | /// Defines a configurable [`RequestSigner`](private::RequestSigner) that signs Http 12 | /// [`RestRequest`] using API specific logic. 13 | pub mod private; 14 | 15 | /// Defines a default [`BuildStrategy`] that builds a non-authenticated Http 16 | /// [`RestRequest`] with no headers. 17 | pub mod public; 18 | 19 | /// [`RestRequest`] build strategy for the API being interacted with. 20 | /// 21 | /// An API that requires authenticated [`RestRequest`]s will likely utilise the configurable 22 | /// [`RequestSigner`](private::RequestSigner) to sign the requests before building. 23 | /// 24 | /// An API that requires no authentication may just add mandatory `reqwest` headers to the 25 | /// [`RestRequest`] before building. 26 | pub trait BuildStrategy { 27 | /// Use a [`RestRequest`] and [`reqwest::RequestBuilder`] to construct a [`reqwest::Request`] 28 | /// that is ready for executing. 29 | /// 30 | /// It is expected that any signing or performed during this method, or the addition of any 31 | /// `reqwest` headers. 32 | fn build( 33 | &self, 34 | request: Request, 35 | builder: reqwest::RequestBuilder, 36 | ) -> Result 37 | where 38 | Request: RestRequest; 39 | } 40 | 41 | /// Utilised by a [`RestClient`](rest::client::RestClient) to deserialise 42 | /// [`RestRequest::Response`], and upon failure parses API errors 43 | /// returned from the server. 44 | pub trait HttpParser { 45 | type ApiError: DeserializeOwned; 46 | type OutputError: From; 47 | 48 | /// Attempt to parse a [`StatusCode`] & bytes payload into a deserialisable `Response`. 49 | fn parse( 50 | &self, 51 | status: StatusCode, 52 | payload: &[u8], 53 | ) -> Result 54 | where 55 | Response: DeserializeOwned, 56 | { 57 | // Attempt to deserialise reqwest::Response bytes into Ok(Response) 58 | let parse_ok_error = match serde_json::from_slice::(payload) { 59 | Ok(response) => return Ok(response), 60 | Err(serde_error) => serde_error, 61 | }; 62 | 63 | // Attempt to deserialise API Error if Ok(Response) deserialisation failed 64 | let parse_api_error_error = match serde_json::from_slice::(payload) { 65 | Ok(api_error) => return Err(self.parse_api_error(status, api_error)), 66 | Err(serde_error) => serde_error, 67 | }; 68 | 69 | // Log errors if failed to deserialise reqwest::Response into Response or API Self::Error 70 | error!( 71 | status_code = ?status, 72 | ?parse_ok_error, 73 | ?parse_api_error_error, 74 | response_body = %String::from_utf8_lossy(payload), 75 | "error deserializing HTTP response" 76 | ); 77 | 78 | Err(Self::OutputError::from(SocketError::DeserialiseBinary { 79 | error: parse_ok_error, 80 | payload: payload.to_vec(), 81 | })) 82 | } 83 | 84 | /// If [`parse`](Self::parse) fails to deserialise the `Ok(Response)`, this function parses 85 | /// to parse the API [`Self::ApiError`] associated with the response. 86 | fn parse_api_error(&self, status: StatusCode, error: Self::ApiError) -> Self::OutputError; 87 | } 88 | -------------------------------------------------------------------------------- /barter-integration/src/protocol/http/private/encoder.rs: -------------------------------------------------------------------------------- 1 | use base64::Engine; 2 | 3 | /// Encodes bytes data. 4 | pub trait Encoder { 5 | /// Encodes the bytes data into some `String` format. 6 | fn encode(&self, data: Bytes) -> String 7 | where 8 | Bytes: AsRef<[u8]>; 9 | } 10 | 11 | /// Encodes bytes data as a hex `String` using lowercase characters. 12 | #[derive(Debug, Copy, Clone)] 13 | pub struct HexEncoder; 14 | 15 | impl Encoder for HexEncoder { 16 | fn encode(&self, data: Bytes) -> String 17 | where 18 | Bytes: AsRef<[u8]>, 19 | { 20 | hex::encode(data) 21 | } 22 | } 23 | 24 | /// Encodes bytes data as a base64 `String`. 25 | #[derive(Debug, Copy, Clone)] 26 | pub struct Base64Encoder; 27 | 28 | impl Encoder for Base64Encoder { 29 | fn encode(&self, data: Bytes) -> String 30 | where 31 | Bytes: AsRef<[u8]>, 32 | { 33 | base64::engine::general_purpose::STANDARD.encode(data) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /barter-integration/src/protocol/http/public/mod.rs: -------------------------------------------------------------------------------- 1 | use super::BuildStrategy; 2 | use crate::error::SocketError; 3 | 4 | /// [`RestRequest`](super::RestRequest) [`BuildStrategy`] that builds a non-authenticated Http request with no headers. 5 | #[derive(Debug, Copy, Clone)] 6 | pub struct PublicNoHeaders; 7 | 8 | impl BuildStrategy for PublicNoHeaders { 9 | fn build( 10 | &self, 11 | _: Request, 12 | builder: reqwest::RequestBuilder, 13 | ) -> Result { 14 | builder.build().map_err(SocketError::from) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /barter-integration/src/protocol/http/rest/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::{de::DeserializeOwned, Serialize}; 2 | use std::time::Duration; 3 | 4 | /// Configurable [`client::RestClient`] capable of executing signed [`RestRequest`]s and parsing 5 | /// responses. 6 | pub mod client; 7 | 8 | /// Default Http [`reqwest::Request`] timeout Duration. 9 | const DEFAULT_HTTP_REQUEST_TIMEOUT: Duration = Duration::from_secs(5); 10 | 11 | /// Http REST request that can be executed by a [`RestClient`](self::client::RestClient). 12 | pub trait RestRequest { 13 | /// Expected response type if this request was successful. 14 | type Response: DeserializeOwned; 15 | 16 | /// Serialisable query parameters type - use unit struct () if not required for this request. 17 | type QueryParams: Serialize; 18 | 19 | /// Serialisable Body type - use unit struct () if not required for this request. 20 | type Body: Serialize; 21 | 22 | /// Additional [`Url`](url::Url) path to the resource. 23 | fn path(&self) -> std::borrow::Cow<'static, str>; 24 | 25 | /// Http [`reqwest::Method`] of this request. 26 | fn method() -> reqwest::Method; 27 | 28 | /// Optional query parameters for this request. 29 | fn query_params(&self) -> Option<&Self::QueryParams> { 30 | None 31 | } 32 | 33 | /// Optional Body for this request. 34 | fn body(&self) -> Option<&Self::Body> { 35 | None 36 | } 37 | 38 | /// Http request timeout [`Duration`]. 39 | fn timeout() -> Duration { 40 | DEFAULT_HTTP_REQUEST_TIMEOUT 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /barter-integration/src/protocol/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::SocketError; 2 | use futures::Stream; 3 | use serde::de::DeserializeOwned; 4 | 5 | /// Contains useful `WebSocket` type aliases and a default `WebSocket` implementation of a 6 | /// [`StreamParser`]. 7 | pub mod websocket; 8 | 9 | /// Contains HTTP client capable of executing signed & unsigned requests, as well as an associated 10 | /// exchange oriented HTTP request. 11 | pub mod http; 12 | 13 | /// `StreamParser`s are capable of parsing the input messages from a given stream protocol 14 | /// (eg/ WebSocket, Financial Information eXchange (FIX), etc.) and deserialising into an `Output`. 15 | pub trait StreamParser { 16 | type Stream: Stream; 17 | type Message; 18 | type Error; 19 | 20 | fn parse( 21 | input: Result, 22 | ) -> Option> 23 | where 24 | Output: DeserializeOwned; 25 | } 26 | -------------------------------------------------------------------------------- /barter-macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "barter-macro" 3 | version = "0.1.1" 4 | authors = ["JustAStream"] 5 | edition = "2021" 6 | license = "MIT" 7 | documentation = "https://docs.rs/barter-macro/" 8 | repository = "https://github.com/barter-rs/barter-rs" 9 | description = "Barter ecosystem macros" 10 | keywords = ["trading", "backtesting", "crypto", "stocks", "investment"] 11 | categories = ["accessibility", "simulation"] 12 | 13 | [lib] 14 | proc-macro = true 15 | 16 | [dependencies] 17 | # Macro 18 | proc-macro2 = "1.0.49" 19 | syn = "1.0.107" 20 | quote = "1.0.23" 21 | 22 | # Misc 23 | convert_case = "0.6.0" -------------------------------------------------------------------------------- /barter-macro/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Barter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /barter-macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use convert_case::{Boundary, Case, Casing}; 4 | use proc_macro::TokenStream; 5 | use quote::quote; 6 | use syn::DeriveInput; 7 | 8 | #[proc_macro_derive(DeExchange)] 9 | pub fn de_exchange_derive(input: TokenStream) -> TokenStream { 10 | // Parse Rust code abstract syntax tree with Syn from TokenStream -> DeriveInput 11 | let ast: DeriveInput = 12 | syn::parse(input).expect("de_exchange_derive() failed to parse input TokenStream"); 13 | 14 | // Determine exchange name 15 | let exchange = &ast.ident; 16 | 17 | let generated = quote! { 18 | impl<'de> serde::Deserialize<'de> for #exchange { 19 | fn deserialize(deserializer: D) -> Result 20 | where 21 | D: serde::de::Deserializer<'de> 22 | { 23 | let input = ::deserialize(deserializer)?; 24 | let expected = #exchange::ID.as_str(); 25 | 26 | if input.as_str() == expected { 27 | Ok(Self::default()) 28 | } else { 29 | Err(serde::de::Error::invalid_value( 30 | serde::de::Unexpected::Str(input.as_str()), 31 | &expected 32 | )) 33 | } 34 | } 35 | } 36 | }; 37 | 38 | TokenStream::from(generated) 39 | } 40 | 41 | #[proc_macro_derive(SerExchange)] 42 | pub fn ser_exchange_derive(input: TokenStream) -> TokenStream { 43 | // Parse Rust code abstract syntax tree with Syn from TokenStream -> DeriveInput 44 | let ast: DeriveInput = 45 | syn::parse(input).expect("ser_exchange_derive() failed to parse input TokenStream"); 46 | 47 | // Determine Exchange 48 | let exchange = &ast.ident; 49 | 50 | let generated = quote! { 51 | impl serde::Serialize for #exchange { 52 | fn serialize(&self, serializer: S) -> Result 53 | where 54 | S: serde::ser::Serializer, 55 | { 56 | let exchange_id = #exchange::ID.as_str(); 57 | serializer.serialize_str(exchange_id) 58 | } 59 | } 60 | }; 61 | 62 | TokenStream::from(generated) 63 | } 64 | 65 | #[proc_macro_derive(DeSubKind)] 66 | pub fn de_sub_kind_derive(input: TokenStream) -> TokenStream { 67 | // Parse Rust code abstract syntax tree with Syn from TokenStream -> DeriveInput 68 | let ast: DeriveInput = 69 | syn::parse(input).expect("de_sub_kind_derive() failed to parse input TokenStream"); 70 | 71 | // Determine SubKind name 72 | let sub_kind = &ast.ident; 73 | 74 | let expected_sub_kind = sub_kind 75 | .to_string() 76 | .from_case(Case::Pascal) 77 | .without_boundaries(&Boundary::letter_digit()) 78 | .to_case(Case::Snake); 79 | 80 | let generated = quote! { 81 | impl<'de> serde::Deserialize<'de> for #sub_kind { 82 | fn deserialize(deserializer: D) -> Result 83 | where 84 | D: serde::de::Deserializer<'de> 85 | { 86 | let input = ::deserialize(deserializer)?; 87 | 88 | if input == #expected_sub_kind { 89 | Ok(Self) 90 | } else { 91 | Err(serde::de::Error::invalid_value( 92 | serde::de::Unexpected::Str(input.as_str()), 93 | &#expected_sub_kind 94 | )) 95 | } 96 | } 97 | } 98 | }; 99 | 100 | TokenStream::from(generated) 101 | } 102 | 103 | #[proc_macro_derive(SerSubKind)] 104 | pub fn ser_sub_kind_derive(input: TokenStream) -> TokenStream { 105 | // Parse Rust code abstract syntax tree with Syn from TokenStream -> DeriveInput 106 | let ast: DeriveInput = 107 | syn::parse(input).expect("ser_sub_kind_derive() failed to parse input TokenStream"); 108 | 109 | // Determine SubKind name 110 | let sub_kind = &ast.ident; 111 | let sub_kind_string = sub_kind.to_string().to_case(Case::Snake); 112 | let sub_kind_str = sub_kind_string.as_str(); 113 | 114 | let generated = quote! { 115 | impl serde::Serialize for #sub_kind { 116 | fn serialize(&self, serializer: S) -> Result 117 | where 118 | S: serde::ser::Serializer, 119 | { 120 | serializer.serialize_str(#sub_kind_str) 121 | } 122 | } 123 | }; 124 | 125 | TokenStream::from(generated) 126 | } 127 | -------------------------------------------------------------------------------- /barter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "barter" 3 | version = "0.8.15" 4 | authors = ["Just A Stream <93921983+just-a-stream@users.noreply.github.com>"] 5 | edition = "2021" 6 | license = "MIT" 7 | documentation = "https://docs.rs/barter/" 8 | repository = "https://github.com/barter-rs/barter-rs" 9 | readme = "README.md" 10 | description = "Framework for building event-driven live-trading & backtesting engines" 11 | keywords = ["trading", "backtesting", "crypto", "stocks", "investment"] 12 | categories = ["accessibility", "simulation"] 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [dependencies] 17 | # Barter Ecosystem 18 | barter-data = { path = "../barter-data", version = "0.8.1"} 19 | barter-integration = { path = "../barter-integration", version = "0.7.3" } 20 | 21 | # Logging 22 | tracing = { workspace = true } 23 | 24 | # Async 25 | tokio = { workspace = true, features = ["sync"] } 26 | tokio-stream = { workspace = true, features = ["sync"] } 27 | futures = { workspace = true } 28 | async-trait = { workspace = true } 29 | 30 | # Error 31 | thiserror = { workspace = true } 32 | 33 | # SerDe 34 | serde = { workspace = true, features = ["derive"] } 35 | serde_json = { workspace = true } 36 | 37 | # Persistence 38 | redis = "0.25.4" 39 | 40 | # Strategy 41 | ta = { workspace = true } 42 | 43 | # Misc 44 | uuid = { workspace = true, features = ["v4", "serde"] } 45 | chrono = { workspace = true, features = ["serde"]} 46 | parking_lot = { workspace = true } 47 | prettytable-rs = "0.10.0" 48 | -------------------------------------------------------------------------------- /barter/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Barter Contributors 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /barter/examples/data/candles_1h.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "start_time": "2022-04-05 20:00:00.000000000 UTC", 4 | "close_time": "2022-04-05 21:00:00.000000000 UTC", 5 | "open": 1000.0, 6 | "high": 1100.0, 7 | "low": 900.0, 8 | "close": 1050.0, 9 | "volume": 1000000000.0, 10 | "trade_count": 100 11 | }, 12 | { 13 | "start_time": "2022-04-05 21:00:00.000000000 UTC", 14 | "close_time": "2022-04-05 22:00:00.000000000 UTC", 15 | "open": 1050.0, 16 | "high": 1100.0, 17 | "low": 800.0, 18 | "close": 1060.0, 19 | "volume": 1000000000.0, 20 | "trade_count": 50 21 | }, 22 | { 23 | "start_time": "2022-04-05 22:00:00.000000000 UTC", 24 | "close_time": "2022-04-05 23:00:00.000000000 UTC", 25 | "open": 1060.0, 26 | "high": 1200.0, 27 | "low": 800.0, 28 | "close": 1200.0, 29 | "volume": 1000000000.0, 30 | "trade_count": 200 31 | }, 32 | { 33 | "start_time": "2022-04-05 23:00:00.000000000 UTC", 34 | "close_time": "2022-04-06 00:00:00.000000000 UTC", 35 | "open": 1200.0, 36 | "high": 1200.0, 37 | "low": 1100.0, 38 | "close": 1300.0, 39 | "volume": 1000000000.0, 40 | "trade_count": 500 41 | } 42 | ] -------------------------------------------------------------------------------- /barter/src/data/error.rs: -------------------------------------------------------------------------------- 1 | use barter_integration::error::SocketError; 2 | use thiserror::Error; 3 | 4 | /// All errors generated in the barter::data module. 5 | #[derive(Error, Debug)] 6 | pub enum DataError { 7 | #[error("Invalid builder attributes provided")] 8 | BuilderAttributesInvalid, 9 | 10 | #[error("Failed to build struct due to missing attributes: {0}")] 11 | BuilderIncomplete(&'static str), 12 | 13 | #[error("Socket: {0}")] 14 | Socket(#[from] SocketError), 15 | 16 | #[error("Barter-Data: {0}")] 17 | Data(#[from] barter_data::error::DataError), 18 | } 19 | -------------------------------------------------------------------------------- /barter/src/data/historical.rs: -------------------------------------------------------------------------------- 1 | use crate::data::{Feed, MarketGenerator}; 2 | 3 | /// Historical [`Feed`] of market events. 4 | #[derive(Debug)] 5 | pub struct MarketFeed 6 | where 7 | Iter: Iterator, 8 | { 9 | pub market_iterator: Iter, 10 | } 11 | 12 | impl MarketGenerator for MarketFeed 13 | where 14 | Iter: Iterator, 15 | { 16 | fn next(&mut self) -> Feed { 17 | self.market_iterator 18 | .next() 19 | .map_or(Feed::Finished, Feed::Next) 20 | } 21 | } 22 | 23 | impl MarketFeed 24 | where 25 | Iter: Iterator, 26 | { 27 | /// Construct a historical [`MarketFeed`] that yields market events from the `IntoIterator` 28 | /// provided. 29 | pub fn new(market_iterator: IntoIter) -> Self 30 | where 31 | IntoIter: IntoIterator, 32 | { 33 | Self { 34 | market_iterator: market_iterator.into_iter(), 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /barter/src/data/live.rs: -------------------------------------------------------------------------------- 1 | use super::{Feed, MarketGenerator}; 2 | use tokio::sync::mpsc; 3 | 4 | /// Live [`Feed`] of market events. 5 | #[derive(Debug)] 6 | pub struct MarketFeed { 7 | pub market_rx: mpsc::UnboundedReceiver, 8 | } 9 | 10 | impl MarketGenerator for MarketFeed { 11 | fn next(&mut self) -> Feed { 12 | loop { 13 | match self.market_rx.try_recv() { 14 | Ok(event) => break Feed::Next(event), 15 | Err(mpsc::error::TryRecvError::Empty) => continue, 16 | Err(mpsc::error::TryRecvError::Disconnected) => break Feed::Finished, 17 | } 18 | } 19 | } 20 | } 21 | 22 | impl MarketFeed { 23 | /// Initialises a live [`MarketFeed`] that yields market `Event`s from the provided 24 | /// [`mpsc::UnboundedReceiver`]. 25 | /// 26 | /// Recommended use with the `Barter-Data` [`Streams`](barter_data::streams::Streams): 27 | /// 1. Initialise a [`Streams`](barter_data::streams::Streams) using the 28 | /// [`StreamBuilder`](barter_data::streams::builder::StreamBuilder) or 29 | /// [`MultiStreamBuilder`](barter_data::streams::builder::multi::MultiStreamBuilder). 30 | /// 2. Use [`Streams::join`](barter_data::streams::Streams::join) to join all exchange 31 | /// [`mpsc::UnboundedReceiver`] streams into a unified [`mpsc::UnboundedReceiver`]. 32 | /// 3. Construct [`Self`] with the unified [`mpsc::UnboundedReceiver`]. 33 | pub fn new(market_rx: mpsc::UnboundedReceiver) -> Self { 34 | Self { market_rx } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /barter/src/data/mod.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | /// Barter data module specific errors. 5 | pub mod error; 6 | 7 | /// Live market event feed for dry-trading & live-trading. 8 | pub mod live; 9 | 10 | /// Historical market event feed for backtesting. 11 | pub mod historical; 12 | 13 | /// Generates the next `Event`. Acts as the system heartbeat. 14 | pub trait MarketGenerator { 15 | /// Return the next market `Event`. 16 | fn next(&mut self) -> Feed; 17 | } 18 | 19 | /// Communicates the state of the [`Feed`] as well as the next event. 20 | #[derive(Clone, Eq, PartialEq, PartialOrd, Debug, Deserialize, Serialize)] 21 | pub enum Feed { 22 | Next(Event), 23 | Unhealthy, 24 | Finished, 25 | } 26 | 27 | /// Metadata detailing the [`Candle`](barter_data::subscription::candle::Candle) or 28 | /// [`Trade`](barter_data::subscription::trade::PublicTrade) close price & it's associated 29 | /// timestamp. Used to propagate key market information in downstream Events. 30 | #[derive(Copy, Clone, PartialEq, PartialOrd, Debug, Deserialize, Serialize)] 31 | pub struct MarketMeta { 32 | /// Close value from the source market event. 33 | pub close: f64, 34 | /// Exchange timestamp from the source market event. 35 | pub time: DateTime, 36 | } 37 | 38 | impl Default for MarketMeta { 39 | fn default() -> Self { 40 | Self { 41 | close: 100.0, 42 | time: Utc::now(), 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /barter/src/engine/error.rs: -------------------------------------------------------------------------------- 1 | use crate::portfolio::repository::error::RepositoryError; 2 | use thiserror::Error; 3 | 4 | /// All errors generated in barter-engine. 5 | #[derive(Error, Debug)] 6 | pub enum EngineError { 7 | #[error("Failed to build struct due to missing attributes: {0}")] 8 | BuilderIncomplete(&'static str), 9 | 10 | #[error("Failed to interact with repository")] 11 | RepositoryInteractionError(#[from] RepositoryError), 12 | } 13 | -------------------------------------------------------------------------------- /barter/src/event.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | execution::FillEvent, 3 | portfolio::{ 4 | position::{Position, PositionExit, PositionUpdate}, 5 | Balance, OrderEvent, 6 | }, 7 | strategy::{Signal, SignalForceExit}, 8 | }; 9 | use barter_data::event::{DataKind, MarketEvent}; 10 | use barter_integration::model::instrument::Instrument; 11 | use serde::{Deserialize, Serialize}; 12 | use std::fmt::Debug; 13 | use tokio::sync::mpsc; 14 | use tracing::warn; 15 | 16 | /// Events that occur when bartering. [`MarketEvent`], [`Signal`], [`OrderEvent`], and 17 | /// [`FillEvent`] are vital to the [`Trader`](crate::engine::trader::Trader) event loop, dictating 18 | /// the trading sequence. The [`PositionExit`] Event is a representation of work done by the 19 | /// system, and is useful for analysing performance & reconciliations. 20 | #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] 21 | pub enum Event { 22 | Market(MarketEvent), 23 | Signal(Signal), 24 | SignalForceExit(SignalForceExit), 25 | OrderNew(OrderEvent), 26 | OrderUpdate, 27 | Fill(FillEvent), 28 | PositionNew(Position), 29 | PositionUpdate(PositionUpdate), 30 | PositionExit(PositionExit), 31 | Balance(Balance), 32 | } 33 | 34 | /// Message transmitter for sending Barter messages to downstream consumers. 35 | pub trait MessageTransmitter { 36 | /// Attempts to send a message to an external message subscriber. 37 | fn send(&mut self, message: Message); 38 | 39 | /// Attempts to send many messages to an external message subscriber. 40 | fn send_many(&mut self, messages: Vec); 41 | } 42 | 43 | /// Transmitter for sending Barter [`Event`]s to an external sink. Useful for event-sourcing, 44 | /// real-time dashboards & general monitoring. 45 | #[derive(Debug, Clone)] 46 | pub struct EventTx { 47 | /// Flag to communicate if the external [`Event`] receiver has been dropped. 48 | receiver_dropped: bool, 49 | /// [`Event`] channel transmitter to send [`Event`]s to an external sink. 50 | event_tx: mpsc::UnboundedSender, 51 | } 52 | 53 | impl MessageTransmitter for EventTx { 54 | fn send(&mut self, message: Event) { 55 | if self.receiver_dropped { 56 | return; 57 | } 58 | 59 | if self.event_tx.send(message).is_err() { 60 | warn!( 61 | action = "setting receiver_dropped = true", 62 | why = "event receiver dropped", 63 | "cannot send Events" 64 | ); 65 | self.receiver_dropped = true; 66 | } 67 | } 68 | 69 | fn send_many(&mut self, messages: Vec) { 70 | if self.receiver_dropped { 71 | return; 72 | } 73 | 74 | messages.into_iter().for_each(|message| { 75 | let _ = self.event_tx.send(message); 76 | }) 77 | } 78 | } 79 | 80 | impl EventTx { 81 | /// Constructs a new [`EventTx`] instance using the provided channel transmitter. 82 | pub fn new(event_tx: mpsc::UnboundedSender) -> Self { 83 | Self { 84 | receiver_dropped: false, 85 | event_tx, 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /barter/src/execution/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | /// All errors generated in the barter::execution module. 4 | #[derive(Error, Copy, Clone, Debug)] 5 | pub enum ExecutionError { 6 | #[error("Failed to build struct due to missing attributes: {0}")] 7 | BuilderIncomplete(&'static str), 8 | } 9 | -------------------------------------------------------------------------------- /barter/src/portfolio/error.rs: -------------------------------------------------------------------------------- 1 | use crate::portfolio::repository::error::RepositoryError; 2 | use thiserror::Error; 3 | 4 | /// All errors generated in the barter::portfolio module. 5 | #[derive(Error, Debug)] 6 | pub enum PortfolioError { 7 | #[error("Failed to build struct due to missing attributes: {0}")] 8 | BuilderIncomplete(&'static str), 9 | 10 | #[error("Failed to parse Position entry Side due to ambiguous fill quantity & Decision.")] 11 | ParseEntrySide, 12 | 13 | #[error("Cannot exit Position with an entry decision FillEvent.")] 14 | CannotEnterPositionWithExitFill, 15 | 16 | #[error("Cannot exit Position with an entry decision FillEvent.")] 17 | CannotExitPositionWithEntryFill, 18 | 19 | #[error("Cannot generate PositionExit from Position that has not been exited")] 20 | PositionExit, 21 | 22 | #[error("Failed to interact with repository")] 23 | RepositoryInteraction(#[from] RepositoryError), 24 | } 25 | -------------------------------------------------------------------------------- /barter/src/portfolio/repository/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | /// All errors generated in the barter::portfolio::repository module. 4 | #[derive(Error, Debug)] 5 | pub enum RepositoryError { 6 | #[error("Failed to deserialize/serialize JSON due to: {0}")] 7 | JsonSerDeError(#[from] serde_json::Error), 8 | 9 | #[error("Failed to write data to the repository")] 10 | WriteError, 11 | 12 | #[error("Failed to read data from the repository")] 13 | ReadError, 14 | 15 | #[error("Failed to delete data from the repository")] 16 | DeleteError, 17 | 18 | #[error("Failed to retrieve expected data due to it not being present")] 19 | ExpectedDataNotPresentError, 20 | } 21 | -------------------------------------------------------------------------------- /barter/src/portfolio/repository/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::portfolio::{ 2 | position::{Position, PositionId}, 3 | repository::error::RepositoryError, 4 | Balance, 5 | }; 6 | use barter_integration::model::{Market, MarketId}; 7 | use uuid::Uuid; 8 | 9 | /// Barter repository module specific errors. 10 | pub mod error; 11 | 12 | /// In-Memory repository for convenient state keeping. No fault tolerant guarantees. 13 | pub mod in_memory; 14 | 15 | /// Redis repository for state keeping. 16 | pub mod redis; 17 | 18 | /// Handles the reading & writing of a [`Position`] to/from the persistence layer. 19 | pub trait PositionHandler { 20 | /// Upsert the open [`Position`] using it's [`PositionId`]. 21 | fn set_open_position(&mut self, position: Position) -> Result<(), RepositoryError>; 22 | 23 | /// Get an open [`Position`] using the [`PositionId`] provided. 24 | fn get_open_position( 25 | &mut self, 26 | position_id: &PositionId, 27 | ) -> Result, RepositoryError>; 28 | 29 | /// Get all open [`Position`]s associated with a Portfolio. 30 | fn get_open_positions<'a, Markets: Iterator>( 31 | &mut self, 32 | engine_id: Uuid, 33 | markets: Markets, 34 | ) -> Result, RepositoryError>; 35 | 36 | /// Remove the [`Position`] at the [`PositionId`]. 37 | fn remove_position( 38 | &mut self, 39 | position_id: &PositionId, 40 | ) -> Result, RepositoryError>; 41 | 42 | /// Append an exited [`Position`] to the Portfolio's exited position list. 43 | fn set_exited_position( 44 | &mut self, 45 | engine_id: Uuid, 46 | position: Position, 47 | ) -> Result<(), RepositoryError>; 48 | 49 | /// Get every exited [`Position`] associated with the engine_id. 50 | fn get_exited_positions(&mut self, engine_id: Uuid) -> Result, RepositoryError>; 51 | } 52 | 53 | /// Handles the reading & writing of a Portfolio's current balance to/from the persistence layer. 54 | pub trait BalanceHandler { 55 | /// Upsert the Portfolio [`Balance`] at the engine_id. 56 | fn set_balance(&mut self, engine_id: Uuid, balance: Balance) -> Result<(), RepositoryError>; 57 | /// Get the Portfolio [`Balance`] using the engine_id provided. 58 | fn get_balance(&mut self, engine_id: Uuid) -> Result; 59 | } 60 | 61 | /// Handles the reading & writing of a Portfolio's statistics for each of it's 62 | /// markets, where each market is represented by a [`MarketId`]. 63 | pub trait StatisticHandler { 64 | /// Upsert the market statistics at the [`MarketId`] provided. 65 | fn set_statistics( 66 | &mut self, 67 | market_id: MarketId, 68 | statistic: Statistic, 69 | ) -> Result<(), RepositoryError>; 70 | /// Get the market statistics using the [`MarketId`] provided. 71 | fn get_statistics(&mut self, market_id: &MarketId) -> Result; 72 | } 73 | 74 | /// Communicates a String represents a unique identifier for all a Portfolio's exited [`Position`]s. 75 | /// Used to append new exited [`Position`]s to the entry in the [`PositionHandler`]. 76 | pub type ExitedPositionsId = String; 77 | 78 | /// Returns the unique identifier for a Portfolio's exited [`Position`]s, given an engine_id. 79 | pub fn determine_exited_positions_id(engine_id: Uuid) -> ExitedPositionsId { 80 | format!("positions_exited_{}", engine_id) 81 | } 82 | -------------------------------------------------------------------------------- /barter/src/portfolio/risk.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::portfolio::{OrderEvent, OrderType}; 4 | 5 | /// Evaluates the risk associated with an [`OrderEvent`] to determine if it should be actioned. It 6 | /// can also amend the order (eg/ [`OrderType`]) to better fit the risk strategy required for 7 | /// profitability. 8 | pub trait OrderEvaluator { 9 | const DEFAULT_ORDER_TYPE: OrderType; 10 | 11 | /// May return an amended [`OrderEvent`] if the associated risk is appropriate. Returns `None` 12 | /// if the risk is too high. 13 | fn evaluate_order(&self, order: OrderEvent) -> Option; 14 | } 15 | 16 | /// Default risk manager that implements [`OrderEvaluator`]. 17 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] 18 | pub struct DefaultRisk {} 19 | 20 | impl OrderEvaluator for DefaultRisk { 21 | const DEFAULT_ORDER_TYPE: OrderType = OrderType::Market; 22 | 23 | fn evaluate_order(&self, mut order: OrderEvent) -> Option { 24 | if self.risk_too_high(&order) { 25 | return None; 26 | } 27 | order.order_type = DefaultRisk::DEFAULT_ORDER_TYPE; 28 | Some(order) 29 | } 30 | } 31 | 32 | impl DefaultRisk { 33 | fn risk_too_high(&self, _: &OrderEvent) -> bool { 34 | false 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /barter/src/statistic/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | /// All errors generated in the barter::statistic module. 4 | #[derive(Error, Copy, Clone, Debug)] 5 | pub enum StatisticError { 6 | #[error("Failed to build struct due to missing attributes: {0}")] 7 | BuilderIncomplete(&'static str), 8 | 9 | #[error("Failed to build struct due to insufficient metrics provided")] 10 | BuilderNoMetricsProvided, 11 | } 12 | -------------------------------------------------------------------------------- /barter/src/statistic/mod.rs: -------------------------------------------------------------------------------- 1 | use chrono::Duration; 2 | use serde::{Deserialize, Deserializer, Serializer}; 3 | 4 | pub mod algorithm; 5 | pub mod dispersion; 6 | pub mod error; 7 | pub mod metric; 8 | pub mod summary; 9 | 10 | /// Serialize a [`Duration`] into a `u64` representing the associated seconds. 11 | pub fn se_duration_as_secs(duration: &Duration, serializer: S) -> Result 12 | where 13 | S: Serializer, 14 | { 15 | serializer.serialize_i64(duration.num_seconds()) 16 | } 17 | 18 | /// Deserialize a number representing seconds into a [`Duration`] 19 | pub fn de_duration_from_secs<'de, D>(deserializer: D) -> Result 20 | where 21 | D: Deserializer<'de>, 22 | { 23 | let seconds: i64 = Deserialize::deserialize(deserializer)?; 24 | Ok(Duration::seconds(seconds)) 25 | } 26 | -------------------------------------------------------------------------------- /barter/src/statistic/summary/drawdown.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | portfolio::position::Position, 3 | statistic::{ 4 | metric::{ 5 | drawdown::{AvgDrawdown, Drawdown, MaxDrawdown}, 6 | EquityPoint, 7 | }, 8 | summary::{PositionSummariser, TableBuilder}, 9 | }, 10 | }; 11 | use prettytable::Row; 12 | use serde::{Deserialize, Serialize}; 13 | 14 | #[derive(Copy, Clone, PartialEq, PartialOrd, Debug, Deserialize, Serialize)] 15 | pub struct DrawdownSummary { 16 | pub current_drawdown: Drawdown, 17 | pub avg_drawdown: AvgDrawdown, 18 | pub max_drawdown: MaxDrawdown, 19 | } 20 | 21 | impl PositionSummariser for DrawdownSummary { 22 | fn update(&mut self, position: &Position) { 23 | // Only update DrawdownSummary with closed Positions 24 | let equity_point = match position.meta.exit_balance { 25 | None => return, 26 | Some(exit_balance) => EquityPoint::from(exit_balance), 27 | }; 28 | 29 | // Updates 30 | if let Some(ended_drawdown) = self.current_drawdown.update(equity_point) { 31 | self.avg_drawdown.update(&ended_drawdown); 32 | self.max_drawdown.update(&ended_drawdown); 33 | } 34 | } 35 | } 36 | 37 | impl TableBuilder for DrawdownSummary { 38 | fn titles(&self) -> Row { 39 | row![ 40 | "Max Drawdown", 41 | "Max Drawdown Days", 42 | "Avg. Drawdown", 43 | "Avg. Drawdown Days", 44 | ] 45 | } 46 | 47 | fn row(&self) -> Row { 48 | row![ 49 | format!("{:.3}", self.max_drawdown.drawdown.drawdown), 50 | self.max_drawdown.drawdown.duration.num_days().to_string(), 51 | format!("{:.3}", self.avg_drawdown.mean_drawdown), 52 | self.avg_drawdown.mean_duration.num_days().to_string(), 53 | ] 54 | } 55 | } 56 | 57 | impl DrawdownSummary { 58 | pub fn new(starting_equity: f64) -> Self { 59 | Self { 60 | current_drawdown: Drawdown::init(starting_equity), 61 | avg_drawdown: AvgDrawdown::init(), 62 | max_drawdown: MaxDrawdown::init(), 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /barter/src/statistic/summary/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod data; 2 | pub mod drawdown; 3 | pub mod pnl; 4 | pub mod trading; 5 | 6 | use crate::portfolio::position::Position; 7 | use prettytable::{Cell, Row, Table}; 8 | 9 | pub trait Initialiser { 10 | type Config: Copy; 11 | fn init(config: Self::Config) -> Self; 12 | } 13 | 14 | pub trait PositionSummariser: Copy { 15 | fn update(&mut self, position: &Position); 16 | fn generate_summary(&mut self, positions: &[Position]) { 17 | for position in positions.iter() { 18 | self.update(position) 19 | } 20 | } 21 | } 22 | 23 | pub trait TableBuilder { 24 | fn titles(&self) -> Row; 25 | fn row(&self) -> Row; 26 | fn table(&self, id_cell: &str) -> Table { 27 | let mut table = Table::new(); 28 | 29 | let mut titles = self.titles(); 30 | titles.insert_cell(0, Cell::new("")); 31 | table.set_titles(titles); 32 | 33 | let mut row = self.row(); 34 | row.insert_cell(0, Cell::new(id_cell)); 35 | table.add_row(row); 36 | 37 | table 38 | } 39 | fn table_with(&self, id_cell: &str, another: (T, &str)) -> Table { 40 | let mut table = Table::new(); 41 | 42 | let mut titles = self.titles(); 43 | titles.insert_cell(0, Cell::new("")); 44 | table.set_titles(titles); 45 | 46 | let mut first_row = self.row(); 47 | first_row.insert_cell(0, Cell::new(id_cell)); 48 | table.add_row(first_row); 49 | 50 | let mut another_row = another.0.row(); 51 | another_row.insert_cell(0, Cell::new(another.1)); 52 | table.add_row(another_row); 53 | 54 | table 55 | } 56 | } 57 | 58 | pub fn combine(builders: Iter) -> Table 59 | where 60 | Iter: IntoIterator, 61 | T: TableBuilder, 62 | { 63 | builders 64 | .into_iter() 65 | .enumerate() 66 | .fold(Table::new(), |mut table, (index, (id, builder))| { 67 | // Set Table titles using the first builder 68 | if index == 0 { 69 | let mut titles = builder.titles(); 70 | titles.insert_cell(0, Cell::new("")); 71 | table.set_titles(titles); 72 | } 73 | 74 | // Add rows for each builder 75 | let mut row = builder.row(); 76 | row.insert_cell(0, Cell::new(&id)); 77 | table.add_row(row); 78 | 79 | table 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /barter/src/statistic/summary/trading.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | portfolio::position::Position, 3 | statistic::{ 4 | metric::ratio::{CalmarRatio, Ratio, SharpeRatio, SortinoRatio}, 5 | summary::{ 6 | drawdown::DrawdownSummary, pnl::PnLReturnSummary, Initialiser, PositionSummariser, 7 | TableBuilder, 8 | }, 9 | }, 10 | }; 11 | use chrono::{DateTime, Duration, Utc}; 12 | use prettytable::{Cell, Row}; 13 | use serde::{Deserialize, Serialize}; 14 | 15 | /// Configuration for initialising a [`TradingSummary`] via the init() constructor method. 16 | #[derive(Copy, Clone, PartialEq, PartialOrd, Debug, Deserialize, Serialize)] 17 | pub struct Config { 18 | pub starting_equity: f64, 19 | pub trading_days_per_year: usize, 20 | pub risk_free_return: f64, 21 | } 22 | 23 | #[derive(Copy, Clone, PartialEq, PartialOrd, Debug, Deserialize, Serialize)] 24 | pub struct TradingSummary { 25 | pub pnl_returns: PnLReturnSummary, 26 | pub drawdown: DrawdownSummary, 27 | pub tear_sheet: TearSheet, 28 | } 29 | 30 | impl Initialiser for TradingSummary { 31 | type Config = Config; 32 | 33 | fn init(config: Self::Config) -> Self { 34 | Self { 35 | pnl_returns: PnLReturnSummary::new(), 36 | drawdown: DrawdownSummary::new(config.starting_equity), 37 | tear_sheet: TearSheet::new(config.risk_free_return), 38 | } 39 | } 40 | } 41 | 42 | impl PositionSummariser for TradingSummary { 43 | fn update(&mut self, position: &Position) { 44 | self.pnl_returns.update(position); 45 | self.drawdown.update(position); 46 | self.tear_sheet.update(&self.pnl_returns, &self.drawdown); 47 | } 48 | } 49 | 50 | impl TableBuilder for TradingSummary { 51 | fn titles(&self) -> Row { 52 | let mut titles = Vec::::new(); 53 | 54 | for title in &self.pnl_returns.titles() { 55 | titles.push(title.clone()) 56 | } 57 | 58 | for title in &self.tear_sheet.titles() { 59 | titles.push(title.clone()) 60 | } 61 | 62 | for title in &self.drawdown.titles() { 63 | titles.push(title.clone()) 64 | } 65 | 66 | Row::new(titles) 67 | } 68 | 69 | fn row(&self) -> Row { 70 | let mut cells = Vec::::new(); 71 | 72 | for cell in &self.pnl_returns.row() { 73 | cells.push(cell.clone()) 74 | } 75 | 76 | for cell in &self.tear_sheet.row() { 77 | cells.push(cell.clone()) 78 | } 79 | 80 | for cell in &self.drawdown.row() { 81 | cells.push(cell.clone()) 82 | } 83 | 84 | Row::new(cells) 85 | } 86 | } 87 | 88 | #[derive(Copy, Clone, PartialEq, PartialOrd, Debug, Deserialize, Serialize)] 89 | pub struct TearSheet { 90 | pub sharpe_ratio: SharpeRatio, 91 | pub sortino_ratio: SortinoRatio, 92 | pub calmar_ratio: CalmarRatio, 93 | } 94 | 95 | impl TearSheet { 96 | pub fn new(risk_free_return: f64) -> Self { 97 | Self { 98 | sharpe_ratio: SharpeRatio::init(risk_free_return), 99 | sortino_ratio: SortinoRatio::init(risk_free_return), 100 | calmar_ratio: CalmarRatio::init(risk_free_return), 101 | } 102 | } 103 | 104 | pub fn update(&mut self, pnl_returns: &PnLReturnSummary, drawdown: &DrawdownSummary) { 105 | self.sharpe_ratio.update(pnl_returns); 106 | self.sortino_ratio.update(pnl_returns); 107 | self.calmar_ratio 108 | .update(pnl_returns, drawdown.max_drawdown.drawdown.drawdown); 109 | } 110 | } 111 | 112 | impl TableBuilder for TearSheet { 113 | fn titles(&self) -> Row { 114 | row!["Sharpe Ratio", "Sortino Ratio", "Calmar Ratio"] 115 | } 116 | 117 | fn row(&self) -> Row { 118 | row![ 119 | format!("{:.3}", self.sharpe_ratio.daily()), 120 | format!("{:.3}", self.sortino_ratio.daily()), 121 | format!("{:.3}", self.calmar_ratio.daily()), 122 | ] 123 | } 124 | } 125 | 126 | pub fn calculate_trading_duration(start_time: &DateTime, position: &Position) -> Duration { 127 | match position.meta.exit_balance { 128 | None => { 129 | // Since Position is not exited, estimate duration w/ last_update_time 130 | position.meta.update_time.signed_duration_since(*start_time) 131 | } 132 | Some(exit_balance) => exit_balance.time.signed_duration_since(*start_time), 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /barter/src/strategy/example.rs: -------------------------------------------------------------------------------- 1 | use super::{Decision, Signal, SignalGenerator, SignalStrength}; 2 | use crate::data::MarketMeta; 3 | use barter_data::event::{DataKind, MarketEvent}; 4 | use barter_integration::model::instrument::Instrument; 5 | use chrono::Utc; 6 | use serde::{Deserialize, Serialize}; 7 | use std::collections::HashMap; 8 | use ta::{indicators::RelativeStrengthIndex, Next}; 9 | 10 | /// Configuration for constructing a [`RSIStrategy`] via the new() constructor method. 11 | #[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Debug, Deserialize, Serialize)] 12 | pub struct Config { 13 | pub rsi_period: usize, 14 | } 15 | 16 | #[derive(Clone, Debug)] 17 | /// Example RSI based strategy that implements [`SignalGenerator`]. 18 | pub struct RSIStrategy { 19 | rsi: RelativeStrengthIndex, 20 | } 21 | 22 | impl SignalGenerator for RSIStrategy { 23 | fn generate_signal(&mut self, market: &MarketEvent) -> Option { 24 | // Check if it's a MarketEvent with a candle 25 | let candle_close = match &market.kind { 26 | DataKind::Candle(candle) => candle.close, 27 | _ => return None, 28 | }; 29 | 30 | // Calculate the next RSI value using the new MarketEvent Candle data 31 | let rsi = self.rsi.next(candle_close); 32 | 33 | // Generate advisory signals map 34 | let signals = RSIStrategy::generate_signals_map(rsi); 35 | 36 | // If signals map is empty, return no SignalEvent 37 | if signals.is_empty() { 38 | return None; 39 | } 40 | 41 | Some(Signal { 42 | time: Utc::now(), 43 | exchange: market.exchange.clone(), 44 | instrument: market.instrument.clone(), 45 | market_meta: MarketMeta { 46 | close: candle_close, 47 | time: market.exchange_time, 48 | }, 49 | signals, 50 | }) 51 | } 52 | } 53 | 54 | impl RSIStrategy { 55 | /// Constructs a new [`RSIStrategy`] component using the provided configuration struct. 56 | pub fn new(config: Config) -> Self { 57 | let rsi_indicator = RelativeStrengthIndex::new(config.rsi_period) 58 | .expect("Failed to construct RSI indicator"); 59 | 60 | Self { rsi: rsi_indicator } 61 | } 62 | 63 | /// Given the latest RSI value for a symbol, generates a map containing the [`SignalStrength`] for 64 | /// [`Decision`] under consideration. 65 | fn generate_signals_map(rsi: f64) -> HashMap { 66 | let mut signals = HashMap::with_capacity(4); 67 | if rsi < 40.0 { 68 | signals.insert(Decision::Long, RSIStrategy::calculate_signal_strength()); 69 | } 70 | if rsi > 60.0 { 71 | signals.insert( 72 | Decision::CloseLong, 73 | RSIStrategy::calculate_signal_strength(), 74 | ); 75 | } 76 | if rsi > 60.0 { 77 | signals.insert(Decision::Short, RSIStrategy::calculate_signal_strength()); 78 | } 79 | if rsi < 40.0 { 80 | signals.insert( 81 | Decision::CloseShort, 82 | RSIStrategy::calculate_signal_strength(), 83 | ); 84 | } 85 | signals 86 | } 87 | 88 | /// Calculates the [`SignalStrength`] of a particular [`Decision`]. 89 | fn calculate_signal_strength() -> SignalStrength { 90 | SignalStrength(1.0) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | imports_granularity = "crate" --------------------------------------------------------------------------------