├── .cargo └── config.toml ├── .dockerignore ├── .github └── workflows │ ├── build.yaml │ └── lint.yaml ├── .gitignore ├── .gitmodules ├── .markdownlint.yaml ├── .pre-commit-config.yaml ├── .prettierrc ├── .tokeignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── README.md ├── TODO.md ├── crates ├── bin │ ├── bt.rs │ ├── crawler.rs │ └── main.rs ├── rsquant-bt │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── rsquant-core │ ├── Cargo.toml │ ├── migration │ │ ├── Cargo.toml │ │ ├── README.md │ │ └── src │ │ │ ├── lib.rs │ │ │ ├── m20240621_165714_create_table.rs │ │ │ └── main.rs │ ├── src │ │ ├── actor │ │ │ ├── binan_api.rs │ │ │ ├── frontend.rs │ │ │ ├── mod.rs │ │ │ ├── send_email.rs │ │ │ └── strategy.rs │ │ ├── api │ │ │ ├── basic │ │ │ │ ├── enum_def.rs │ │ │ │ ├── filters.rs │ │ │ │ └── mod.rs │ │ │ ├── credential │ │ │ │ └── mod.rs │ │ │ ├── mod.rs │ │ │ └── req │ │ │ │ ├── api_impl.rs │ │ │ │ ├── mod.rs │ │ │ │ └── send_req.rs │ │ ├── db │ │ │ ├── mod.rs │ │ │ └── service.rs │ │ ├── entity │ │ │ ├── mod.rs │ │ │ ├── order.rs │ │ │ └── side.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── manager.rs │ │ ├── message.rs │ │ ├── model │ │ │ ├── account │ │ │ │ ├── account_info.rs │ │ │ │ ├── coin_info.rs │ │ │ │ └── mod.rs │ │ │ ├── market │ │ │ │ ├── kline.rs │ │ │ │ ├── mod.rs │ │ │ │ └── ticker_price.rs │ │ │ ├── mod.rs │ │ │ └── trade │ │ │ │ ├── mod.rs │ │ │ │ └── order.rs │ │ ├── monitor.rs │ │ ├── trade │ │ │ ├── indicator │ │ │ │ ├── data_item.rs │ │ │ │ ├── ema.rs │ │ │ │ ├── macd.rs │ │ │ │ ├── mod.rs │ │ │ │ └── rsi.rs │ │ │ ├── macros.rs │ │ │ ├── mod.rs │ │ │ └── strategy │ │ │ │ ├── common_macd_and_rsi.rs │ │ │ │ ├── double_ema.rs │ │ │ │ ├── mod.rs │ │ │ │ └── rsi_and_double_ema.rs │ │ └── util │ │ │ ├── config.rs │ │ │ ├── constants.rs │ │ │ ├── email.rs │ │ │ ├── env.rs │ │ │ ├── log.rs │ │ │ ├── mod.rs │ │ │ └── time │ │ │ ├── converter.rs │ │ │ ├── current.rs │ │ │ ├── mod.rs │ │ │ └── timezone.rs │ └── template │ │ └── email │ │ └── monitor.html ├── rsquant-derive │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── rsquant-tool │ ├── Cargo.toml │ └── src │ └── lib.rs ├── deps └── binan_spot │ ├── Cargo.toml │ └── src │ ├── http │ ├── credentials.rs │ ├── error.rs │ ├── method.rs │ ├── mod.rs │ └── request.rs │ ├── hyper │ ├── client.rs │ ├── error.rs │ ├── mod.rs │ └── response.rs │ ├── isolated_margin_stream │ ├── close_listen_key.rs │ ├── mod.rs │ ├── new_listen_key.rs │ └── renew_listen_key.rs │ ├── lib.rs │ ├── margin │ ├── bnb_burn_status.rs │ ├── isolated_margin_account.rs │ ├── isolated_margin_account_limit.rs │ ├── isolated_margin_all_symbols.rs │ ├── isolated_margin_disable_account.rs │ ├── isolated_margin_enable_account.rs │ ├── isolated_margin_fee_data.rs │ ├── isolated_margin_symbol.rs │ ├── isolated_margin_tier_data.rs │ ├── isolated_margin_transfer.rs │ ├── isolated_margin_transfer_history.rs │ ├── margin_account.rs │ ├── margin_all_assets.rs │ ├── margin_all_oco_order.rs │ ├── margin_all_orders.rs │ ├── margin_all_pairs.rs │ ├── margin_asset.rs │ ├── margin_borrow.rs │ ├── margin_cancel_oco_order.rs │ ├── margin_cancel_open_orders.rs │ ├── margin_cancel_order.rs │ ├── margin_dustlog.rs │ ├── margin_fee_data.rs │ ├── margin_force_liquidation_record.rs │ ├── margin_interest_history.rs │ ├── margin_interest_rate_history.rs │ ├── margin_loan_record.rs │ ├── margin_max_borrowable.rs │ ├── margin_max_transferable.rs │ ├── margin_my_trades.rs │ ├── margin_new_oco_order.rs │ ├── margin_new_order.rs │ ├── margin_oco_order.rs │ ├── margin_open_oco_order.rs │ ├── margin_open_orders.rs │ ├── margin_order.rs │ ├── margin_order_count_usage.rs │ ├── margin_pair.rs │ ├── margin_price_index.rs │ ├── margin_repay.rs │ ├── margin_repay_record.rs │ ├── margin_transfer.rs │ ├── margin_transfer_history.rs │ ├── mod.rs │ └── toggle_bnb_burn.rs │ ├── margin_stream │ ├── close_listen_key.rs │ ├── mod.rs │ ├── new_listen_key.rs │ └── renew_listen_key.rs │ ├── market │ ├── agg_trades.rs │ ├── avg_price.rs │ ├── book_ticker.rs │ ├── depth.rs │ ├── exchange_info.rs │ ├── historical_trades.rs │ ├── klines.rs │ ├── mod.rs │ ├── ping.rs │ ├── rolling_window_price_change_statistics.rs │ ├── ticker_price.rs │ ├── ticker_twenty_four_hr.rs │ ├── time.rs │ └── trades.rs │ ├── market_stream │ ├── agg_trade.rs │ ├── book_ticker.rs │ ├── diff_depth.rs │ ├── kline.rs │ ├── mini_ticker.rs │ ├── mod.rs │ ├── partial_depth.rs │ ├── rolling_window_ticker.rs │ ├── ticker.rs │ └── trade.rs │ ├── stream │ ├── close_listen_key.rs │ ├── mod.rs │ ├── new_listen_key.rs │ └── renew_listen_key.rs │ ├── tokio_tungstenite.rs │ ├── trade │ ├── account.rs │ ├── all_orders.rs │ ├── cancel_an_existing_order_and_send_a_new_order.rs │ ├── cancel_oco_order.rs │ ├── cancel_open_orders.rs │ ├── cancel_order.rs │ ├── get_oco_order.rs │ ├── get_oco_orders.rs │ ├── get_open_oco_orders.rs │ ├── get_order.rs │ ├── mod.rs │ ├── my_trades.rs │ ├── new_oco_order.rs │ ├── new_order.rs │ ├── new_order_test.rs │ ├── open_orders.rs │ ├── order.rs │ └── order_limit_usage.rs │ ├── tungstenite.rs │ ├── ureq │ ├── client.rs │ ├── error.rs │ ├── mod.rs │ └── response.rs │ ├── user_data_stream │ ├── mod.rs │ └── user_data.rs │ ├── utils.rs │ ├── wallet │ ├── account_snapshot.rs │ ├── account_status.rs │ ├── api_key_permission.rs │ ├── api_trading_status.rs │ ├── asset_detail.rs │ ├── asset_dividend_record.rs │ ├── coin_info.rs │ ├── deposit_address.rs │ ├── deposit_history.rs │ ├── disable_fast_withdraw.rs │ ├── dust_log.rs │ ├── dust_transfer.rs │ ├── dustable_assets.rs │ ├── enable_fast_withdraw.rs │ ├── funding_wallet.rs │ ├── mod.rs │ ├── system_status.rs │ ├── trade_fee.rs │ ├── universal_transfer.rs │ ├── universal_transfer_history.rs │ ├── user_asset.rs │ ├── withdraw.rs │ └── withdraw_history.rs │ └── websocket.rs ├── docker-compose.yml ├── docs └── assets │ ├── exec-flow.png │ ├── project-structure.png │ └── quant-trader-avatar.png ├── rsquant.sh ├── rust-toolchain.toml ├── rustfmt.toml ├── scripts ├── check.sh ├── database.sh ├── docker.sh ├── rust.sh └── web.sh └── web ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── src ├── assets │ └── favicon.ico ├── components │ ├── Calculator.tsx │ └── SymbolCard.tsx ├── index.css ├── index.tsx ├── logo.svg ├── pages │ ├── GreetPage.tsx │ ├── Home.tsx │ └── SubscribeTicker.tsx ├── types.ts └── utils │ └── constants.ts ├── tailwind.config.js ├── tsconfig.json └── vite.config.ts /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | # [target.x86_64-unknown-linux-musl] 2 | # rustflags = [ 3 | # "-C", 4 | # "linker=clang", 5 | # "-C", 6 | # "link-arg=-fuse-ld=mold", 7 | # "-C", 8 | # "force-frame-pointers=yes", 9 | # ] 10 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | *.db 2 | *.pdb 3 | **/*.rs.bk 4 | *.sublime-* 5 | 6 | core.* 7 | nohup.out 8 | Cargo.lock 9 | 10 | debug/ 11 | target/ 12 | database/ 13 | log/ 14 | 15 | .vscode/ 16 | .idea/ 17 | .venv 18 | __pycache__ 19 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | tags: 7 | - "v[0-9]+.[0-9]+.[0-9]+" 8 | pull_request: 9 | branches: [master, dev, fix-**, feat-**] 10 | workflow_run: 11 | workflows: [Lint] 12 | types: 13 | - completed 14 | 15 | jobs: 16 | build-rs: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | with: 22 | submodules: true 23 | 24 | - name: Download build image 25 | run: docker pull clux/muslrust:nightly 26 | 27 | - name: Build 28 | run: docker run -v $PWD:/volume --rm -t clux/muslrust:nightly bash -c 'cargo build --release --target x86_64-unknown-linux-musl' 29 | 30 | - name: Package asset 31 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') 32 | shell: bash 33 | run: | 34 | echo "Tag : ${{ github.ref_name }}" 35 | cp $PWD/target/x86_64-unknown-linux-musl/release/rsquant $PWD 36 | tar cvJf rsquant-${{ github.ref_name }}.tar.xz rsquant 37 | realpath ${{ github.workspace }}/rsquant-${{ github.ref_name }}.tar.xz 38 | env: 39 | GITHUB_TOKEN: ${{ github.TOKEN }} 40 | 41 | - name: Upload artifact 42 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') 43 | uses: actions/upload-artifact@v4 44 | with: 45 | name: rsquant-${{ github.ref_name }}.tar.xz 46 | path: ${{ github.workspace }}/rsquant-${{ github.ref_name }}.tar.xz 47 | overwrite: true 48 | 49 | release: 50 | if: startsWith(github.ref, 'refs/tags/v') 51 | needs: build-rs 52 | runs-on: ubuntu-latest 53 | 54 | steps: 55 | - uses: actions/checkout@v4 56 | with: 57 | submodules: true 58 | 59 | - name: Download artifact 60 | uses: actions/download-artifact@v4 61 | with: 62 | name: rsquant-${{ github.ref_name }}.tar.xz 63 | 64 | - name: Create release 65 | uses: actions/create-release@v1 66 | env: 67 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 68 | with: 69 | tag_name: ${{ github.ref }} 70 | release_name: Release rsquant ${{ github.ref_name }} 71 | draft: false 72 | prerelease: false 73 | 74 | - name: Upload asset to release 75 | uses: svenstaro/upload-release-action@v2 76 | with: 77 | repo_token: ${{ secrets.GITHUB_TOKEN }} 78 | file: rsquant-${{ github.ref_name }}.tar.xz 79 | asset_name: rsquant-${{ github.ref_name }}.tar.xz 80 | tag: ${{ github.ref }} 81 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | pre-commit: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v3 11 | 12 | - uses: actions/setup-python@v3 13 | 14 | - uses: pre-commit/action@v3.0.0 15 | 16 | check-rs: 17 | needs: pre-commit 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | with: 23 | submodules: true 24 | 25 | - name: Download build image 26 | run: docker pull clux/muslrust:nightly 27 | 28 | - name: Test 29 | run: docker run -v $PWD:/volume --rm -t clux/muslrust:nightly bash -c 'cargo test' 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Rust 2 | *.pdb 3 | **/*.rs.bk 4 | 5 | debug/ 6 | target/ 7 | 8 | ### IDE config 9 | .vscode/ 10 | .idea/ 11 | 12 | *.sublime-* 13 | 14 | ### Project 15 | log 16 | .venv 17 | __pycache__ 18 | poetry.lock 19 | rsquant.json 20 | .env 21 | 22 | deps/binan_spot_examples 23 | 24 | ### Build and Run 25 | core.* 26 | nohup.out 27 | 28 | .gdb* 29 | 30 | ### Documentations 31 | *.excalidraw 32 | 33 | ### Web 34 | 35 | # dependencies 36 | node_modules 37 | .pnp 38 | .pnp.js 39 | pnpm-lock.yaml 40 | 41 | # testing 42 | coverage 43 | 44 | # next.js 45 | .next/ 46 | out/ 47 | 48 | # production 49 | build 50 | _build 51 | dist 52 | 53 | # misc 54 | .DS_Store 55 | *.pem 56 | 57 | # debug 58 | npm-debug.log* 59 | yarn-debug.log* 60 | yarn-error.log* 61 | 62 | # local env files 63 | .env*.local 64 | 65 | # vercel 66 | .vercel 67 | 68 | # typescript 69 | *.tsbuildinfo 70 | next-env.d.ts 71 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deps/barter-rs"] 2 | path = deps/barter-rs 3 | url = git@github.com:hnlcf/barter-rs.git 4 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | default: true 2 | MD013: false 3 | MD024: 4 | siblings_only: true 5 | MD029: 6 | style: ordered 7 | MD033: false 8 | MD041: false 9 | MD046: false 10 | MD049: false 11 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | ci: 2 | autofix_commit_msg: "style(pre-commit): autofix" 3 | 4 | repos: 5 | - repo: https://github.com/pre-commit/pre-commit-hooks 6 | rev: v4.4.0 7 | hooks: 8 | - id: check-json 9 | - id: check-merge-conflict 10 | - id: check-toml 11 | - id: check-xml 12 | - id: check-yaml 13 | args: [--unsafe] 14 | - id: detect-private-key 15 | exclude: .rs$ 16 | - id: end-of-file-fixer 17 | - id: mixed-line-ending 18 | - id: trailing-whitespace 19 | args: [--markdown-linebreak-ext=md] 20 | 21 | - repo: https://github.com/igorshubovych/markdownlint-cli 22 | rev: v0.33.0 23 | hooks: 24 | - id: markdownlint 25 | args: [-c, .markdownlint.yaml, --fix] 26 | 27 | - repo: https://github.com/pre-commit/mirrors-prettier 28 | rev: v3.0.0-alpha.6 29 | hooks: 30 | - id: prettier 31 | 32 | - repo: https://github.com/shellcheck-py/shellcheck-py 33 | rev: v0.9.0.2 34 | hooks: 35 | - id: shellcheck 36 | args: [-e, SC1091] 37 | 38 | - repo: https://github.com/scop/pre-commit-shfmt 39 | rev: v3.6.0-2 40 | hooks: 41 | - id: shfmt 42 | args: [-w, -s, -i=4] 43 | 44 | - repo: https://github.com/pycqa/isort 45 | rev: 5.12.0 46 | hooks: 47 | - id: isort 48 | 49 | - repo: https://github.com/psf/black 50 | rev: 23.3.0 51 | hooks: 52 | - id: black 53 | args: [--line-length=100] 54 | 55 | - repo: local 56 | hooks: 57 | - id: rustfmt 58 | name: rustfmt 59 | description: Check if all files follow the rustfmt style 60 | entry: cargo fmt --all -- --check --color always 61 | language: system 62 | pass_filenames: false 63 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "printWidth": 120, 5 | "semi": false, 6 | "singleQuote": false 7 | } 8 | -------------------------------------------------------------------------------- /.tokeignore: -------------------------------------------------------------------------------- 1 | binan_spot* 2 | 3 | *.toml 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.0.1 - Undefined 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rsquant" 3 | version = "0.1.5" 4 | edition = "2021" 5 | authors = ["Changfeng Lou"] 6 | 7 | 8 | [[bin]] 9 | bench = false 10 | path = "crates/bin/main.rs" 11 | name = "rsquant" 12 | 13 | [[bin]] 14 | bench = false 15 | path = "crates/bin/crawler.rs" 16 | name = "crawler" 17 | 18 | [[bin]] 19 | bench = false 20 | path = "crates/bin/bt.rs" 21 | name = "backtest" 22 | 23 | [workspace] 24 | members = [ 25 | "deps/binan_spot", 26 | # "deps/binan_spot_examples", 27 | "crates/rsquant-core", 28 | "crates/rsquant-core/migration", 29 | "crates/rsquant-derive", 30 | "crates/rsquant-tool", 31 | "crates/rsquant-bt", 32 | ] 33 | 34 | [workspace.dependencies] 35 | log = "0.4.18" 36 | tracing = "0.1.40" 37 | chrono = "0.4.26" 38 | serde = { version = "1.0.156", features = ["derive"] } 39 | serde_json = "1.0.94" 40 | thiserror = "1.0.56" 41 | actix = { version = "0.13.1", features = ["macros"] } 42 | actix-web = { version = "4.8.0", features = [ 43 | "http2", 44 | "cookies", 45 | "compress-zstd", 46 | # "experimental-io-uring", 47 | ] } 48 | tokio = { version = "1.38.0", features = ["time", "rt-multi-thread", "macros"] } 49 | tracing-subscriber = { version = "0.3.18", features = [ 50 | "env-filter", 51 | "registry", 52 | "tracing-log", 53 | ] } 54 | rust_decimal = "1.34.3" 55 | fast-float = "0.2.0" 56 | 57 | [dependencies] 58 | tracing = { workspace = true } 59 | tokio = { workspace = true } 60 | chrono = { workspace = true } 61 | thiserror = { workspace = true } 62 | actix = { workspace = true } 63 | actix-web = { workspace = true } 64 | tracing-subscriber = { workspace = true } 65 | rust_decimal = { workspace = true } 66 | serde_json = { workspace = true } 67 | 68 | binan_spot = { path = "deps/binan_spot", features = ["full"] } 69 | rsquant-core = { path = "crates/rsquant-core" } 70 | rsquant-bt = { path = "crates/rsquant-bt" } 71 | clap = { version = "4.5.1", features = ["derive"] } 72 | barter-data = "0.7.0" 73 | 74 | [profile.release] 75 | panic = 'abort' 76 | lto = true 77 | strip = true 78 | opt-level = 'z' 79 | codegen-units = 1 80 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:jammy 2 | LABEL authors="changfeng" 3 | 4 | ARG DEBIAN_FRONTEND=noninteractive 5 | ARG CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse 6 | 7 | 8 | ## Replace software mirror 9 | RUN sed -i "s#http://archive.ubuntu.com/ubuntu#http://mirrors.tuna.tsinghua.edu.cn/ubuntu#" /etc/apt/sources.list 10 | 11 | ## Update source 12 | RUN apt-get update -y 13 | RUN apt-get upgrade -y 14 | 15 | ## Install necessary dependencies 16 | RUN apt-get install -y \ 17 | tzdata \ 18 | curl \ 19 | pkg-config \ 20 | libpq-dev \ 21 | libssl-dev \ 22 | libsqlite3-dev \ 23 | python3 \ 24 | python3-pip \ 25 | postgresql 26 | 27 | ## Set timezone 28 | RUN ln -fs /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ 29 | && dpkg-reconfigure --frontend noninteractive tzdata 30 | 31 | ENV TZ="Asia/Shanghai" 32 | 33 | COPY . /app 34 | WORKDIR /app 35 | 36 | RUN chown -R 1000:1000 /app 37 | 38 | # Create a new user with 1000:1000 39 | RUN useradd -m -u 1000 -s /bin/bash -d /home/noroot noroot 40 | USER noroot 41 | 42 | ## Setup rust config 43 | RUN mkdir -p /home/noroot/.cargo 44 | 45 | RUN export RUSTUP_DIST_SERVER=https://rsproxy.cn && \ 46 | export RUSTUP_UPDATE_ROOT=https://rsproxy.cn/rustup && \ 47 | curl -o /tmp/rustup-init https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup/dist/x86_64-unknown-linux-musl/rustup-init && \ 48 | chmod +x /tmp/rustup-init && \ 49 | /tmp/rustup-init -y --profile minimal --default-toolchain stable-x86_64-unknown-linux-gnu --default-host x86_64-unknown-linux-gnu --no-modify-path && \ 50 | rm -f /tmp/rustup-init && \ 51 | /home/noroot/.cargo/bin/rustup default nightly && \ 52 | /home/noroot/.cargo/bin/rustup target add x86_64-unknown-linux-musl && \ 53 | echo '. $HOME/.cargo/env' >> /home/noroot/.bashrc 54 | 55 | RUN echo " \n\ 56 | [source.crates-io] \n\ 57 | replace-with = 'mirror' \n\ 58 | [source.mirror] \n\ 59 | registry = 'https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git' \n\ 60 | "> /home/noroot/.cargo/config 61 | 62 | ## Setup python config 63 | RUN mkdir -p /home/noroot/.config/pip 64 | 65 | RUN echo " \n\ 66 | [global] \n\ 67 | index-url = https://pypi.tuna.tsinghua.edu.cn/simple \n\ 68 | break-system-packages = true \n\ 69 | "> /home/noroot/.config/pip/pip.conf 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Changfeng 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 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - [ ] api request 2 | - [ ] strategy 3 | - [ ] trade 4 | - [ ] scheduler 5 | - [ ] refactor to actor model with `actix` 6 | -------------------------------------------------------------------------------- /crates/bin/bt.rs: -------------------------------------------------------------------------------- 1 | use tracing_subscriber::{ 2 | layer::SubscriberExt, 3 | util::SubscriberInitExt, 4 | EnvFilter, 5 | }; 6 | 7 | #[tokio::main] 8 | async fn main() { 9 | let filter_layer = EnvFilter::try_from_env("QUANT_LOG_LEVEL") 10 | .or_else(|_| EnvFilter::try_new("info")) 11 | .unwrap(); 12 | 13 | let tui_layer = tracing_subscriber::fmt::layer() 14 | .with_target(true) 15 | .with_ansi(true) 16 | .with_file(false) 17 | .with_line_number(true) 18 | .with_thread_names(false) 19 | .with_thread_ids(false) 20 | .with_writer(std::io::stdout); 21 | 22 | tracing_subscriber::registry() 23 | .with(tui_layer) 24 | .with(filter_layer) 25 | .init(); 26 | 27 | rsquant_bt::run_bt().await.unwrap(); 28 | } 29 | -------------------------------------------------------------------------------- /crates/bin/crawler.rs: -------------------------------------------------------------------------------- 1 | use actix::Actor; 2 | use barter_data::subscription::candle::Candle; 3 | use binan_spot::market::klines::KlineInterval; 4 | use clap::Parser; 5 | use rsquant_core::{ 6 | actor::BinanApiActor, 7 | message::KlineApiRequest, 8 | model::kline::Kline, 9 | util::config::QuantConfig, 10 | ConfigBuilder, 11 | Result, 12 | }; 13 | 14 | #[derive(Parser)] 15 | #[command(version, about, long_about = None)] 16 | struct Cli { 17 | #[arg(short, long, value_name = "FILE")] 18 | config: std::path::PathBuf, 19 | } 20 | 21 | #[actix_web::main] 22 | async fn main() -> Result<()> { 23 | tracing_subscriber::fmt::init(); 24 | 25 | let args: Cli = Cli::parse(); 26 | let config = ConfigBuilder::build(args.config)?; 27 | 28 | let QuantConfig { 29 | api_credentials, .. 30 | } = config; 31 | 32 | let api = BinanApiActor::from_config(api_credentials).start(); 33 | 34 | let req = KlineApiRequest { 35 | symbol: "BTCUSDT".to_string(), 36 | interval: KlineInterval::Days1, 37 | limit: 1000, 38 | start_time: None, 39 | end_time: None, 40 | }; 41 | 42 | if let Ok(Ok(res)) = api.send(req).await { 43 | let kline = res.klines.into_iter().map(convert).collect::>(); 44 | 45 | // serialize klines and write to file 46 | let klines = serde_json::to_string(&kline).unwrap(); 47 | std::fs::write("fixture/data/btcusdt_candles_1d.json", klines).unwrap(); 48 | } 49 | 50 | Ok(()) 51 | } 52 | 53 | fn convert(k: Kline) -> Candle { 54 | Candle { 55 | close_time: k.close_time.and_utc(), 56 | open: k.open_price.parse().unwrap(), 57 | high: k.high_price.parse().unwrap(), 58 | low: k.low_price.parse().unwrap(), 59 | close: k.close_price.parse().unwrap(), 60 | volume: k.volume.parse().unwrap(), 61 | trade_count: k.trades_num as u64, 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /crates/bin/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use rsquant_core::{ 3 | ConfigBuilder, 4 | Error, 5 | Result, 6 | Strategy, 7 | }; 8 | 9 | #[derive(Parser)] 10 | #[command(version, about, long_about = None)] 11 | struct Cli { 12 | #[arg(short, long, value_name = "FILE")] 13 | config: std::path::PathBuf, 14 | } 15 | 16 | #[actix_web::main] 17 | async fn main() -> Result<()> { 18 | let args = Cli::parse(); 19 | let config = ConfigBuilder::build(args.config)?; 20 | 21 | let symbols = vec![ 22 | "BTCUSDT".into(), 23 | "ETHUSDT".into(), 24 | "BNBUSDT".into(), 25 | "SOLUSDT".into(), 26 | "PEPEUSDT".into(), 27 | "XRPUSDT".into(), 28 | "DOGEUSDT".into(), 29 | "SHIBUSDT".into(), 30 | "ADAUSDT".into(), 31 | "TRXUSDT".into(), 32 | "AVAXUSDT".into(), 33 | "WBTCUSDT".into(), 34 | "DOTUSDT".into(), 35 | "LINKUSDT".into(), 36 | "BCHUSDT".into(), 37 | "DAIUSDT".into(), 38 | "MATICUSDT".into(), 39 | "LTCUSDT".into(), 40 | "ETCUSDT".into(), 41 | "PEOPLE".into(), 42 | "TON".into(), 43 | "NOT".into(), 44 | "ONDO".into(), 45 | "AXL".into(), 46 | "AEVO".into(), 47 | "WIF".into(), 48 | ]; 49 | let strategies: Vec<(&str, Box)> = vec![ 50 | ( 51 | "common", 52 | Box::new(rsquant_core::CommonMacdAndRsiStrategy::new( 53 | 12, 26, 9, 14, 30.0, 70.0, 54 | )), 55 | ), 56 | ( 57 | "double_ema", 58 | Box::new(rsquant_core::DoubleEmaStrategy::new(20, 60)), 59 | ), 60 | ]; 61 | 62 | rsquant_core::init_state(config.clone(), strategies.into_iter()).await; 63 | rsquant_core::set_ctrlc_handler(); 64 | 65 | rsquant_core::run_trade(config.basic).await?; 66 | rsquant_core::run_monitor(symbols).await?; 67 | rsquant_core::run_web() 68 | .await 69 | .map_err(|e| Error::Custom(e.to_string()))?; 70 | 71 | Ok(()) 72 | } 73 | -------------------------------------------------------------------------------- /crates/rsquant-bt/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rsquant-bt" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | tokio = { workspace = true } 8 | tracing = { workspace = true } 9 | chrono = { workspace = true } 10 | 11 | barter = { path = "../../deps/barter-rs" } 12 | barter-data = "0.7.0" 13 | barter-integration = "0.7.2" 14 | 15 | tokio-stream = { version = "0.1.9", features = ["sync"] } 16 | futures = "0.3.21" 17 | 18 | uuid = { version = "1.8.0", features = ["v4", "serde"] } 19 | parking_lot = "0.12.3" 20 | serde = { version = "1.0.143", features = ["derive"] } 21 | serde_json = "1.0.83" 22 | ta = "0.5.0" 23 | -------------------------------------------------------------------------------- /crates/rsquant-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rsquant-core" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | tracing = { workspace = true } 10 | tracing-subscriber = { workspace = true } 11 | chrono = { workspace = true } 12 | serde = { workspace = true } 13 | rust_decimal = { workspace = true } 14 | fast-float = { workspace = true } 15 | thiserror = { workspace = true } 16 | serde_json = { workspace = true } 17 | actix = { workspace = true } 18 | actix-web = { workspace = true } 19 | tokio = { workspace = true } 20 | 21 | 22 | binan_spot = { path = "../../deps/binan_spot", features = ["full"] } 23 | rsquant-derive = { path = "../rsquant-derive" } 24 | rsquant-tool = { path = "../rsquant-tool" } 25 | 26 | once_cell = "1.19.0" 27 | itertools = "0.13.0" 28 | clokwerk = "0.4.0" 29 | lettre = "0.11.7" 30 | tera = "1.14.0" 31 | tracing-appender = "0.2.3" 32 | url = "2.5.0" 33 | sha2 = "0.10.8" 34 | http = "1.1.0" 35 | reqwest = { version = "0.12.4", features = ["rustls-tls", "gzip"] } 36 | futures = "0.3.30" 37 | actix-web-actors = "4.3.0" 38 | actix-cors = "0.7.0" 39 | ta = "0.5.0" 40 | polars = { version = "0.40.0", features = [ 41 | "lazy", 42 | "temporal", 43 | "describe", 44 | "json", 45 | "parquet", 46 | "dtype-datetime", 47 | "diff", 48 | ] } 49 | sea-orm = { version = "0.12.15", features = [ 50 | "macros", 51 | "sqlx-postgres", 52 | "debug-print", 53 | "runtime-tokio-native-tls", 54 | ] } 55 | -------------------------------------------------------------------------------- /crates/rsquant-core/migration/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "migration" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [lib] 8 | name = "migration" 9 | path = "src/lib.rs" 10 | 11 | [dependencies] 12 | 13 | rsquant-core = { path = "../" } 14 | async-std = { version = "1", features = ["attributes", "tokio1"] } 15 | 16 | [dependencies.sea-orm-migration] 17 | version = "0.12.0" 18 | features = [ 19 | # Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI. 20 | # View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime. 21 | # e.g. 22 | "runtime-tokio-rustls", # `ASYNC_RUNTIME` feature 23 | "sqlx-postgres", # `DATABASE_DRIVER` feature 24 | ] 25 | -------------------------------------------------------------------------------- /crates/rsquant-core/migration/README.md: -------------------------------------------------------------------------------- 1 | # Running Migrator CLI 2 | 3 | - Generate a new migration file 4 | 5 | ```sh 6 | cargo run -- generate MIGRATION_NAME 7 | ``` 8 | 9 | - Apply all pending migrations 10 | 11 | ```sh 12 | cargo run 13 | ``` 14 | 15 | ```sh 16 | cargo run -- up 17 | ``` 18 | 19 | - Apply first 10 pending migrations 20 | 21 | ```sh 22 | cargo run -- up -n 10 23 | ``` 24 | 25 | - Rollback last applied migrations 26 | 27 | ```sh 28 | cargo run -- down 29 | ``` 30 | 31 | - Rollback last 10 applied migrations 32 | 33 | ```sh 34 | cargo run -- down -n 10 35 | ``` 36 | 37 | - Drop all tables from the database, then reapply all migrations 38 | 39 | ```sh 40 | cargo run -- fresh 41 | ``` 42 | 43 | - Rollback all applied migrations, then reapply all migrations 44 | 45 | ```sh 46 | cargo run -- refresh 47 | ``` 48 | 49 | - Rollback all applied migrations 50 | 51 | ```sh 52 | cargo run -- reset 53 | ``` 54 | 55 | - Check the status of all migrations 56 | 57 | ```sh 58 | cargo run -- status 59 | ``` 60 | -------------------------------------------------------------------------------- /crates/rsquant-core/migration/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use sea_orm_migration::prelude::*; 2 | 3 | mod m20240621_165714_create_table; 4 | 5 | pub struct Migrator; 6 | 7 | #[async_trait::async_trait] 8 | impl MigratorTrait for Migrator { 9 | fn migrations() -> Vec> { 10 | vec![Box::new(m20240621_165714_create_table::Migration)] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /crates/rsquant-core/migration/src/m20240621_165714_create_table.rs: -------------------------------------------------------------------------------- 1 | use rsquant_core::entity::order; 2 | use sea_orm_migration::prelude::*; 3 | 4 | use crate::{ 5 | extension::postgres::Type, 6 | sea_orm::Schema, 7 | }; 8 | 9 | #[derive(DeriveMigrationName)] 10 | pub struct Migration; 11 | 12 | #[async_trait::async_trait] 13 | impl MigrationTrait for Migration { 14 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 15 | let builder = manager.get_database_backend(); 16 | let schema = Schema::new(builder); 17 | 18 | for stmt in schema.create_enum_from_entity(order::Entity) { 19 | manager.create_type(stmt).await?; 20 | } 21 | 22 | manager 23 | .create_table( 24 | schema 25 | .create_table_from_entity(order::Entity) 26 | .if_not_exists() 27 | .to_owned(), 28 | ) 29 | .await?; 30 | 31 | Ok(()) 32 | } 33 | 34 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 35 | manager 36 | .drop_table( 37 | Table::drop() 38 | .table(order::Entity.into_table_ref()) 39 | .if_exists() 40 | .to_owned(), 41 | ) 42 | .await?; 43 | 44 | let ty_names = [Orders::TradeSide]; 45 | let stmt = Type::drop() 46 | .if_exists() 47 | .names(ty_names) 48 | .cascade() 49 | .to_owned(); 50 | 51 | manager.drop_type(stmt).await?; 52 | 53 | Ok(()) 54 | } 55 | } 56 | 57 | #[derive(DeriveIden)] 58 | enum Orders { 59 | TradeSide, 60 | } 61 | -------------------------------------------------------------------------------- /crates/rsquant-core/migration/src/main.rs: -------------------------------------------------------------------------------- 1 | use sea_orm_migration::prelude::*; 2 | 3 | #[async_std::main] 4 | async fn main() { 5 | cli::run_cli(migration::Migrator).await; 6 | } 7 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/actor/mod.rs: -------------------------------------------------------------------------------- 1 | mod binan_api; 2 | mod frontend; 3 | mod send_email; 4 | mod strategy; 5 | 6 | pub use binan_api::BinanApiActor; 7 | pub use frontend::{ 8 | run_web, 9 | SubscribeTickerActor, 10 | }; 11 | pub use send_email::EmailActor; 12 | pub use strategy::StrategyActor; 13 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/actor/send_email.rs: -------------------------------------------------------------------------------- 1 | use actix::{ 2 | Actor, 3 | ActorContext, 4 | Context, 5 | Handler, 6 | }; 7 | 8 | use crate::{ 9 | message::{ 10 | NormalRequest, 11 | NormalResponse, 12 | SendEmailRequest, 13 | }, 14 | util::{ 15 | config::EmailConfig, 16 | email::{ 17 | EmailBuilder, 18 | EmailManager, 19 | }, 20 | }, 21 | Error, 22 | }; 23 | 24 | pub struct EmailActor { 25 | inner: EmailManager, 26 | } 27 | 28 | impl EmailActor { 29 | pub fn from_config(config: EmailConfig) -> Self { 30 | let inner = EmailBuilder::from_config(config).build(); 31 | Self { inner } 32 | } 33 | } 34 | 35 | impl Actor for EmailActor { 36 | type Context = Context; 37 | 38 | fn started(&mut self, _ctx: &mut Self::Context) { 39 | tracing::info!("[email]: email actor started"); 40 | } 41 | } 42 | 43 | impl Handler for EmailActor { 44 | type Result = Result<(), Error>; 45 | 46 | fn handle(&mut self, msg: SendEmailRequest, _: &mut Self::Context) -> Self::Result { 47 | let SendEmailRequest { subject, content } = msg; 48 | self.inner.send(&subject, &content) 49 | } 50 | } 51 | 52 | impl Handler for EmailActor { 53 | type Result = Result; 54 | 55 | fn handle(&mut self, msg: NormalRequest, ctx: &mut Self::Context) -> Self::Result { 56 | match msg { 57 | NormalRequest::Stop => { 58 | ctx.stop(); 59 | Ok(NormalResponse::Success) 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/actor/strategy.rs: -------------------------------------------------------------------------------- 1 | use actix::{ 2 | Actor, 3 | Handler, 4 | }; 5 | 6 | use crate::{ 7 | entity::side, 8 | message::KlineStrategyRequest, 9 | trade::Strategy, 10 | Result, 11 | }; 12 | 13 | pub struct StrategyActor { 14 | inner: Box, 15 | } 16 | 17 | impl StrategyActor { 18 | pub fn new(inner: Box) -> Self { 19 | Self { inner } 20 | } 21 | } 22 | 23 | impl Actor for StrategyActor { 24 | type Context = actix::Context; 25 | 26 | fn started(&mut self, _ctx: &mut Self::Context) { 27 | tracing::info!( 28 | "[strategy:{}]: strategy actor started", 29 | self.inner.get_name() 30 | ); 31 | } 32 | } 33 | 34 | impl Handler for StrategyActor { 35 | type Result = Result; 36 | 37 | fn handle(&mut self, msg: KlineStrategyRequest, _ctx: &mut Self::Context) -> Self::Result { 38 | let res = self.inner.check(&msg.data); 39 | tracing::info!("[strategy:{}]: {:?}", self.inner.get_name(), res); 40 | Ok(res) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/api/basic/filters.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::enum_variant_names)] 2 | use serde::{ 3 | Deserialize, 4 | Serialize, 5 | }; 6 | 7 | #[derive(Serialize, Deserialize)] 8 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 9 | enum SymbolFilters { 10 | PriceFilter(PriceFilterInfo), 11 | PercentPrice(PercentPriceInfo), 12 | PercentPriceBySide, 13 | LotSize, 14 | MinNotional, 15 | Notional, 16 | IcebergParts, 17 | MarketLotSize, 18 | MaxNumOrders, 19 | MaxNumAlgoOrders, 20 | MaxNumIcebergOrders, 21 | MaxPosition, 22 | TrailingDelta, 23 | } 24 | 25 | #[derive(Serialize, Deserialize)] 26 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 27 | enum ExchangeFilters { 28 | ExchangeMaxNumOrders, 29 | ExchangeMaxNumAlgoOrders, 30 | ExchangeMaxNumIcebergOrders, 31 | } 32 | 33 | #[derive(Serialize, Deserialize)] 34 | #[serde(rename_all = "camelCase")] 35 | struct PriceFilterInfo { 36 | min_price: String, 37 | max_price: String, 38 | tick_size: String, 39 | } 40 | 41 | #[derive(Serialize, Deserialize)] 42 | #[serde(rename_all = "camelCase")] 43 | struct PercentPriceInfo { 44 | multiplier_up: String, 45 | multiplier_down: String, 46 | avg_price_mins: usize, 47 | } 48 | 49 | #[derive(Serialize, Deserialize)] 50 | #[serde(rename_all = "camelCase")] 51 | struct PercentPriceBySideInfo { 52 | // TODO: Symbol Filter 53 | } 54 | 55 | #[derive(Serialize, Deserialize)] 56 | #[serde(rename_all = "camelCase")] 57 | struct LotSizeInfo { 58 | // TODO: Symbol Filter 59 | } 60 | 61 | #[derive(Serialize, Deserialize)] 62 | #[serde(rename_all = "camelCase")] 63 | struct MinNotionalInfo { 64 | // TODO: Symbol Filter 65 | } 66 | 67 | #[derive(Serialize, Deserialize)] 68 | #[serde(rename_all = "camelCase")] 69 | struct NotionalInfo { 70 | // TODO: Symbol Filter 71 | } 72 | 73 | #[derive(Serialize, Deserialize)] 74 | #[serde(rename_all = "camelCase")] 75 | struct IcebergPartsInfo { 76 | // TODO: Symbol Filter 77 | } 78 | 79 | #[derive(Serialize, Deserialize)] 80 | #[serde(rename_all = "camelCase")] 81 | struct MarketLotSizeInfo { 82 | // TODO: Symbol Filter 83 | } 84 | 85 | #[derive(Serialize, Deserialize)] 86 | #[serde(rename_all = "camelCase")] 87 | struct MaxNumOrdersInfo { 88 | // TODO: Symbol Filter 89 | } 90 | 91 | #[derive(Serialize, Deserialize)] 92 | #[serde(rename_all = "camelCase")] 93 | struct MaxNumAlgoOrdersInfo { 94 | // TODO: Symbol Filter 95 | } 96 | 97 | #[derive(Serialize, Deserialize)] 98 | #[serde(rename_all = "camelCase")] 99 | struct MaxNumIcebergOrdersInfo { 100 | // TODO: Symbol Filter 101 | } 102 | 103 | #[derive(Serialize, Deserialize)] 104 | #[serde(rename_all = "camelCase")] 105 | struct MaxPositionInfo { 106 | // TODO: Symbol Filter 107 | } 108 | 109 | #[derive(Serialize, Deserialize)] 110 | #[serde(rename_all = "camelCase")] 111 | struct TrailingDeltaInfo { 112 | // TODO: Symbol Filter 113 | } 114 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/api/basic/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod enum_def; 2 | pub mod filters; 3 | 4 | pub use binan_spot::{ 5 | http::{ 6 | Credentials, 7 | Method, 8 | }, 9 | hyper::create_query_string, 10 | utils::sign, 11 | }; 12 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/api/credential/mod.rs: -------------------------------------------------------------------------------- 1 | use binan_spot::http::Credentials; 2 | 3 | use crate::util::{ 4 | config, 5 | env, 6 | }; 7 | 8 | pub struct CredentialBuilder; 9 | 10 | impl CredentialBuilder { 11 | pub fn from_config(config: config::BinanCredentialsConfig) -> Option { 12 | let sig_type = config.signature_type; 13 | let api_key = config.api_key; 14 | let api_secret = config.api_secret?; 15 | match sig_type.as_str() { 16 | "HMAC" => Some(Credentials::from_hmac(api_key, api_secret)), 17 | _ => None, 18 | } 19 | } 20 | 21 | /// Get Credentials from environment varibales. 22 | /// 23 | /// ## Examples 24 | /// 25 | /// Your local envs need to be set as below. 26 | /// 27 | /// ```bash 28 | /// # Hmac 29 | /// BINAN_SIG_TYPE=HMAC 30 | /// BINAN_API_KEY=xxx 31 | /// BINAN_API_SECRET=xxx 32 | /// 33 | /// # Rsa 34 | /// BINAN_SIG_TYPE=RSA 35 | /// BINAN_API_KEY=xxx 36 | /// BINAN_SIG_KEY=xxx 37 | /// BINAN_SIG_PASSWD=xxx # Maybe absent 38 | /// ``` 39 | pub fn from_env() -> Option { 40 | let sig_type = env::EnvManager::get_env_var("BINAN_SIG_TYPE")?; 41 | let api_key = env::EnvManager::get_env_var("BINAN_API_KEY")?; 42 | match sig_type.as_str() { 43 | "HMAC" => { 44 | let api_secret = env::EnvManager::get_env_var("BINAN_API_SECRET")?; 45 | let hmac_credential = Credentials::from_hmac(api_key, api_secret); 46 | Some(hmac_credential) 47 | } 48 | "RSA" => { 49 | let sig_key = env::EnvManager::get_env_var("BINAN_SIG_KEY")?; 50 | let sig_passwd = env::EnvManager::get_env_var("BINAN_SIG_PASSWD"); 51 | let rsa_credential = match sig_passwd { 52 | Some(sig_passwd) => { 53 | Credentials::from_rsa_protected(api_key, sig_key, sig_passwd) 54 | } 55 | None => Credentials::from_rsa(api_key, sig_key), 56 | }; 57 | 58 | Some(rsa_credential) 59 | } 60 | _ => None, 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/api/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod basic; 2 | pub mod credential; 3 | pub mod req; 4 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/api/req/mod.rs: -------------------------------------------------------------------------------- 1 | use binan_spot::http::request::Request as BinanRequest; 2 | 3 | pub mod api_impl; 4 | pub mod send_req; 5 | 6 | pub use self::{ 7 | api_impl::ApiImpl, 8 | send_req::Response, 9 | }; 10 | use crate::{ 11 | api::basic, 12 | Error, 13 | }; 14 | 15 | #[derive(Debug)] 16 | pub struct HttpClient { 17 | base_url: String, 18 | credentials: basic::Credentials, 19 | client: reqwest::Client, 20 | } 21 | 22 | impl HttpClient { 23 | pub fn new(credentials: basic::Credentials) -> Self { 24 | let base_url = "https://api.binance.com"; 25 | 26 | Self { 27 | base_url: base_url.into(), 28 | credentials, 29 | client: reqwest::Client::new(), 30 | } 31 | } 32 | 33 | pub async fn send>(&self, request: R) -> Result { 34 | let BinanRequest { 35 | method, 36 | path, 37 | params: origin_params, 38 | sign, 39 | .. 40 | } = request.into(); 41 | 42 | let method = method.into(); 43 | 44 | let mut header: Vec<(String, String)> = vec![( 45 | "Content-Type".into(), 46 | "application/json;charset=utf-8".into(), 47 | )]; 48 | 49 | let mut params = origin_params; 50 | 51 | if sign { 52 | let origin_query = basic::create_query_string(¶ms); 53 | let signature = basic::sign(&origin_query, &self.credentials.signature)?; 54 | params.push(("signature".into(), signature)); 55 | 56 | header.push(("X-MBX-APIKEY".into(), self.credentials.api_key.clone())); 57 | } 58 | 59 | let uri = url::Url::parse(&self.base_url)?.join(&path)?; 60 | let mut req_builder = self.client.request(method, uri).query(¶ms); 61 | for (k, v) in header { 62 | req_builder = req_builder.header(k, v); 63 | } 64 | let req = req_builder.build().unwrap(); 65 | tracing::debug!("Client request: {:?}", req); 66 | 67 | self.client 68 | .execute(req) 69 | .await 70 | .map(Response::new) 71 | .map_err(|e| Error::Custom(e.to_string())) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/db/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod service; 2 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/db/service.rs: -------------------------------------------------------------------------------- 1 | use actix::{ 2 | Actor, 3 | ActorFutureExt, 4 | Handler, 5 | ResponseActFuture, 6 | WrapFuture, 7 | }; 8 | use sea_orm::{ 9 | ActiveModelTrait, 10 | Database, 11 | DatabaseConnection, 12 | IntoActiveModel, 13 | }; 14 | 15 | use crate::{ 16 | entity::order, 17 | message::RecordOrderRequest, 18 | util::config, 19 | Error, 20 | Result, 21 | }; 22 | 23 | #[derive(Default)] 24 | pub struct DBService { 25 | conn: DatabaseConnection, 26 | } 27 | 28 | impl DBService { 29 | pub async fn from_config(config: config::DatabaseConfig) -> Result { 30 | match config.db_url.as_ref() { 31 | Some(url) => Database::connect(url) 32 | .await 33 | .map(|conn| Self { conn }) 34 | .map_err(Error::from), 35 | e => Err(Error::Custom(format!("Invalid database url: {:?}", e))), 36 | } 37 | } 38 | } 39 | 40 | impl Actor for DBService { 41 | type Context = actix::Context; 42 | 43 | fn started(&mut self, _ctx: &mut Self::Context) { 44 | // let conn = self.conn.clone(); 45 | // actix::spawn(async move { 46 | // init_db(&conn).await; 47 | // }); 48 | 49 | tracing::info!("[db]: db actor started"); 50 | } 51 | } 52 | 53 | async fn insert_order_record( 54 | conn: &DatabaseConnection, 55 | data: order::Model, 56 | ) -> Result { 57 | data.into_active_model() 58 | .save(conn) 59 | .await 60 | .map_err(Error::from) 61 | } 62 | 63 | impl Handler for DBService { 64 | type Result = ResponseActFuture>; 65 | 66 | fn handle(&mut self, msg: RecordOrderRequest, _ctx: &mut Self::Context) -> Self::Result { 67 | let conn = self.conn.clone(); 68 | async move { insert_order_record(&conn, msg.model).await } 69 | .into_actor(self) 70 | .boxed_local() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/entity/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod order; 2 | pub mod side; 3 | 4 | pub use order::Entity as OrderEntity; 5 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/entity/order.rs: -------------------------------------------------------------------------------- 1 | use rust_decimal::Decimal; 2 | use sea_orm::{ 3 | ActiveModelBehavior, 4 | DeriveEntityModel, 5 | DerivePrimaryKey, 6 | DeriveRelation, 7 | EntityTrait, 8 | EnumIter, 9 | PrimaryKeyTrait, 10 | }; 11 | use serde::{ 12 | Deserialize, 13 | Serialize, 14 | }; 15 | 16 | use super::side::TradeSide; 17 | 18 | #[derive(Default, Clone, Debug, PartialEq, Eq, DeriveEntityModel, Deserialize, Serialize)] 19 | #[sea_orm(table_name = "orders")] 20 | pub struct Model { 21 | #[sea_orm(primary_key, auto_increment = true)] 22 | #[serde(skip_deserializing)] 23 | pub id: i32, 24 | 25 | pub order_id: u64, 26 | 27 | pub symbol: String, 28 | 29 | pub price: Decimal, 30 | 31 | pub quantity: Decimal, 32 | 33 | pub side: TradeSide, 34 | 35 | pub time_in_force: String, 36 | 37 | pub r#type: String, 38 | } 39 | 40 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 41 | pub enum Relation {} 42 | 43 | impl ActiveModelBehavior for ActiveModel {} 44 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/entity/side.rs: -------------------------------------------------------------------------------- 1 | use sea_orm::{ 2 | DeriveActiveEnum, 3 | EnumIter, 4 | }; 5 | use serde::{ 6 | Deserialize, 7 | Serialize, 8 | }; 9 | 10 | #[derive( 11 | Debug, Copy, Default, Clone, PartialEq, Eq, Serialize, Deserialize, EnumIter, DeriveActiveEnum, 12 | )] 13 | #[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "trade_side")] 14 | pub enum TradeSide { 15 | #[sea_orm(string_value = "buy")] 16 | Buy, 17 | #[sea_orm(string_value = "sell")] 18 | Sell, 19 | #[default] 20 | #[sea_orm(string_value = "nop")] 21 | Nop, 22 | } 23 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/error.rs: -------------------------------------------------------------------------------- 1 | use binan_spot::hyper::Error as BinanceHttpError; 2 | use sha2::digest::InvalidLength; 3 | 4 | #[derive(Debug, thiserror::Error)] 5 | pub enum Error { 6 | #[error("Invalid length by `{0}`.")] 7 | InvalidLength(#[from] InvalidLength), 8 | 9 | #[error("Failed to parse url by `{0}`.")] 10 | Url(#[from] url::ParseError), 11 | 12 | #[error("Failed to send HTTP request by `{0}`.")] 13 | Http(#[from] http::Error), 14 | 15 | #[error("Failed to send HTTP request to Binance by `{0}`.")] 16 | BinanceHttp(#[from] BinanceHttpError), 17 | 18 | #[error("Failed to convert response body to string by `{0}`.")] 19 | Utf8(#[from] std::string::FromUtf8Error), 20 | 21 | #[error("Failed to deserialize json str by `{0}`")] 22 | Serde(#[from] serde_json::Error), 23 | 24 | #[error("IO error by `{0}`")] 25 | IO(#[from] std::io::Error), 26 | 27 | #[error("Failed to connect to database by `{0}`")] 28 | Database(#[from] sea_orm::error::DbErr), 29 | 30 | #[error("Email error by `{0}`")] 31 | Email(#[from] lettre::error::Error), 32 | 33 | #[error("Tera template error by `{0}`")] 34 | Template(#[from] tera::Error), 35 | 36 | #[error("Decimal error by `{0}`")] 37 | Decimal(#[from] rust_decimal::Error), 38 | 39 | #[error("Custom error by `{0}`")] 40 | Custom(String), 41 | } 42 | 43 | pub trait FlattenErr { 44 | type Error; 45 | type T; 46 | 47 | fn flatten_err(self) -> core::result::Result; 48 | } 49 | 50 | impl FlattenErr 51 | for core::result::Result, E2> 52 | { 53 | type Error = Error; 54 | type T = T; 55 | 56 | fn flatten_err(self) -> core::result::Result { 57 | match self { 58 | Ok(Ok(t)) => Ok(t), 59 | Ok(Err(e)) => Err(Error::Custom(e.to_string())), 60 | Err(e) => Err(Error::Custom(e.to_string())), 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/model/account/account_info.rs: -------------------------------------------------------------------------------- 1 | use core::fmt; 2 | use std::collections::BTreeMap; 3 | 4 | use rust_decimal::Decimal; 5 | use serde::Deserialize; 6 | 7 | use super::coin_info::CoinInfo; 8 | use crate::model::{ 9 | DecodeFromStr, 10 | IntoTarget, 11 | }; 12 | 13 | #[derive(Debug)] 14 | pub struct AccountInfo { 15 | account_type: String, 16 | balances: BTreeMap, 17 | } 18 | 19 | impl AccountInfo { 20 | pub fn account_type(&self) -> &str { 21 | &self.account_type 22 | } 23 | 24 | pub fn query_asset(&self, asset: &str) -> Option { 25 | self.balances.get(asset).map(|(f, _)| f.to_owned()) 26 | } 27 | } 28 | 29 | #[derive(Debug, Deserialize)] 30 | pub struct RawAccountInfo { 31 | /// 账户类型 32 | #[serde(rename = "accountType")] 33 | account_type: String, 34 | /// 资产 35 | balances: Vec, 36 | } 37 | 38 | impl DecodeFromStr<'_, RawAccountInfo> for RawAccountInfo {} 39 | 40 | impl IntoTarget for RawAccountInfo { 41 | fn into_target(self) -> AccountInfo { 42 | let account_type = self.account_type; 43 | let balances = self 44 | .balances 45 | .into_iter() 46 | .map(|c| { 47 | let CoinInfo { 48 | asset, 49 | free, 50 | locked, 51 | } = c; 52 | (asset, (free, locked)) 53 | }) 54 | .collect(); 55 | AccountInfo { 56 | account_type, 57 | balances, 58 | } 59 | } 60 | } 61 | 62 | impl fmt::Display for RawAccountInfo { 63 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 64 | writeln!(f, "{: <10} {: <20}\t{: <20}", "NAME", "FREE", "LOCKED")?; 65 | for coin in self.balances.iter() { 66 | writeln!(f, "{}", coin)?; 67 | } 68 | write!(f, "") 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/model/account/coin_info.rs: -------------------------------------------------------------------------------- 1 | use core::fmt; 2 | 3 | use rust_decimal::Decimal; 4 | use serde::Deserialize; 5 | 6 | #[derive(Debug, Clone, Deserialize)] 7 | pub struct CoinInfo { 8 | /// 资产名称 9 | pub asset: String, 10 | /// 可用余额 11 | pub free: Decimal, 12 | /// 不可用余额 13 | pub locked: Decimal, 14 | } 15 | 16 | impl CoinInfo { 17 | pub fn asset(&self) -> String { 18 | self.asset.to_owned() 19 | } 20 | 21 | pub fn free(&self) -> Decimal { 22 | self.free.to_owned() 23 | } 24 | 25 | pub fn locked(&self) -> Decimal { 26 | self.locked.to_owned() 27 | } 28 | 29 | pub fn is_zero(&self) -> bool { 30 | self.free.is_zero() && self.locked.is_zero() 31 | } 32 | } 33 | 34 | impl fmt::Display for CoinInfo { 35 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 36 | write!( 37 | f, 38 | "{: <10} {:0<20}\t{:0<20}", 39 | self.asset, self.free, self.locked 40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/model/account/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod account_info; 2 | pub mod coin_info; 3 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/model/market/kline.rs: -------------------------------------------------------------------------------- 1 | use core::fmt; 2 | 3 | use serde::Deserialize; 4 | 5 | use crate::{ 6 | model::DecodeFromStr, 7 | util::time::u64_to_datetime, 8 | }; 9 | 10 | #[derive(Debug, Deserialize)] 11 | 12 | pub struct Kline { 13 | /// 开始时间 14 | #[serde(deserialize_with = "u64_to_datetime")] 15 | pub open_time: chrono::NaiveDateTime, 16 | /// 开盘价 17 | pub open_price: String, 18 | /// 最高价 19 | pub high_price: String, 20 | /// 最低价 21 | pub low_price: String, 22 | /// 收盘价 23 | pub close_price: String, 24 | /// 成交量 25 | pub volume: String, 26 | /// 结束时间 27 | #[serde(deserialize_with = "u64_to_datetime")] 28 | pub close_time: chrono::NaiveDateTime, 29 | /// 成交额 30 | pub quote_asset_volume: String, 31 | pub trades_num: i64, 32 | pub buy_base_asset_volume: String, 33 | pub buy_quote_asset_volume: String, 34 | pub ignore_field: String, 35 | } 36 | 37 | impl DecodeFromStr<'_, Vec> for Vec {} 38 | 39 | impl fmt::Display for Kline { 40 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 41 | write!( 42 | f, 43 | "{{ 44 | open_time: {}, 45 | open_price: {}, 46 | high_price: {}, 47 | low_price: {}, 48 | close_price: {}, 49 | volume: {}, 50 | close_time: {}, 51 | quote_asset_volume: {}, 52 | trades_num: {}, 53 | buy_base_asset_volume: {}, 54 | buy_quote_asset_volume: {}, 55 | ignore_field: {} 56 | }}", 57 | self.open_time, 58 | self.open_price, 59 | self.high_price, 60 | self.low_price, 61 | self.close_price, 62 | self.volume, 63 | self.close_time, 64 | self.quote_asset_volume, 65 | self.trades_num, 66 | self.buy_base_asset_volume, 67 | self.buy_quote_asset_volume, 68 | self.ignore_field 69 | ) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/model/market/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod kline; 2 | pub mod ticker_price; 3 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/model/market/ticker_price.rs: -------------------------------------------------------------------------------- 1 | use core::fmt; 2 | use std::str::FromStr; 3 | 4 | use rust_decimal::Decimal; 5 | use serde::{ 6 | Deserialize, 7 | Serialize, 8 | }; 9 | 10 | use crate::model::DecodeFromStr; 11 | 12 | #[derive(Debug, Serialize, Deserialize)] 13 | pub struct TickerPrice { 14 | pub symbol: String, 15 | pub price: String, 16 | } 17 | 18 | impl TickerPrice { 19 | pub fn price(&self) -> Decimal { 20 | Decimal::from_str(self.price.as_str()).unwrap() 21 | } 22 | } 23 | 24 | impl DecodeFromStr<'_, TickerPrice> for TickerPrice {} 25 | impl DecodeFromStr<'_, Vec> for Vec {} 26 | 27 | impl fmt::Display for TickerPrice { 28 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 29 | write!(f, "{{ symbol: {}, price: {} }}", self.symbol, self.price) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/model/mod.rs: -------------------------------------------------------------------------------- 1 | pub use account::{ 2 | account_info, 3 | coin_info, 4 | }; 5 | pub use market::{ 6 | kline, 7 | ticker_price, 8 | }; 9 | use serde::Deserialize; 10 | pub use trade::order; 11 | 12 | pub mod account; 13 | pub mod market; 14 | pub mod trade; 15 | 16 | pub trait DecodeFromStr<'a, T> 17 | where 18 | T: Deserialize<'a>, 19 | { 20 | fn decode_from_str(data: &'a str) -> Result { 21 | match serde_json::from_str(data) { 22 | Ok(t) => { 23 | tracing::trace!("Deserialize response string to data structure."); 24 | Ok(t) 25 | } 26 | Err(e) => { 27 | tracing::error!( 28 | "Failed to deserialize response string to data structure: {} for data `{}`.", 29 | e, 30 | data 31 | ); 32 | Err(e) 33 | } 34 | } 35 | } 36 | } 37 | 38 | pub trait IntoTarget { 39 | fn into_target(self) -> T; 40 | } 41 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/model/trade/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod order; 2 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/model/trade/order.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | use crate::model::DecodeFromStr; 4 | 5 | #[derive(Debug, Deserialize)] 6 | pub struct OrderResponse { 7 | pub symbol: String, 8 | pub order_id: u64, 9 | pub order_list_id: i64, 10 | pub client_order_id: String, 11 | pub transact_time: u64, 12 | pub price: String, 13 | pub orig_qty: String, 14 | pub executed_qty: String, 15 | pub cummulative_quote_qty: String, 16 | pub status: String, 17 | pub time_in_force: String, 18 | pub r#type: String, 19 | pub side: String, 20 | pub working_time: u64, 21 | pub self_trade_prevention_mode: String, 22 | } 23 | 24 | impl DecodeFromStr<'_, OrderResponse> for OrderResponse {} 25 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/trade/indicator/data_item.rs: -------------------------------------------------------------------------------- 1 | use ta::DataItem; 2 | 3 | use crate::model::kline::Kline; 4 | 5 | pub trait ToDataItem { 6 | fn to_data_item(&self) -> Result>; 7 | } 8 | 9 | impl ToDataItem for Kline { 10 | fn to_data_item(&self) -> Result> { 11 | let open: f64 = fast_float::parse(&self.open_price)?; 12 | let high: f64 = fast_float::parse(&self.high_price)?; 13 | let low: f64 = fast_float::parse(&self.low_price)?; 14 | let close: f64 = fast_float::parse(&self.close_price)?; 15 | let volume: f64 = fast_float::parse(&self.volume)?; 16 | 17 | Ok(DataItem::builder() 18 | .open(open) 19 | .high(high) 20 | .low(low) 21 | .close(close) 22 | .volume(volume) 23 | .build()?) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/trade/indicator/ema.rs: -------------------------------------------------------------------------------- 1 | use polars::series::Series; 2 | use ta::{ 3 | indicators::ExponentialMovingAverage as Ema, 4 | Close, 5 | DataItem, 6 | Next, 7 | }; 8 | 9 | use super::Indicator; 10 | 11 | #[derive(Default, Debug, Clone)] 12 | pub struct EmaOutputBuilder { 13 | ema: Ema, 14 | } 15 | 16 | impl EmaOutputBuilder { 17 | pub fn new(period: usize) -> Self { 18 | Self { 19 | ema: Ema::new(period).unwrap(), 20 | } 21 | } 22 | } 23 | 24 | impl Indicator for EmaOutputBuilder { 25 | type Output = Series; 26 | 27 | fn compute(&mut self, data: &[DataItem]) -> Self::Output { 28 | let f = |v: &DataItem| self.ema.next(v.close()); 29 | data.iter().map(f).collect() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/trade/indicator/macd.rs: -------------------------------------------------------------------------------- 1 | use polars::{ 2 | df, 3 | frame::DataFrame, 4 | }; 5 | use ta::{ 6 | indicators::MovingAverageConvergenceDivergence as Macd, 7 | Close, 8 | DataItem, 9 | Next, 10 | }; 11 | 12 | use super::Indicator; 13 | 14 | #[derive(Default, Debug, Clone)] 15 | pub struct MacdOutputBuilder { 16 | macd: Macd, 17 | } 18 | 19 | impl MacdOutputBuilder { 20 | pub fn new(fast_period: usize, slow_period: usize, signal_period: usize) -> Self { 21 | Self { 22 | macd: Macd::new(fast_period, slow_period, signal_period).unwrap(), 23 | } 24 | } 25 | } 26 | 27 | impl Indicator for MacdOutputBuilder { 28 | type Output = DataFrame; 29 | 30 | fn compute(&mut self, data: &[DataItem]) -> Self::Output { 31 | let f = |v: &DataItem| self.macd.next(v.close()); 32 | let macd = data.iter().map(f).collect::>(); 33 | let macd_line = macd.iter().map(|m| m.macd).collect::>(); 34 | let signal_line = macd.iter().map(|m| m.signal).collect::>(); 35 | let histogram = macd.iter().map(|m| m.histogram).collect::>(); 36 | 37 | df! { 38 | "macd" => &macd_line, 39 | "signal" => &signal_line, 40 | "histogram" => &histogram, 41 | } 42 | .expect("create DataFrame failed") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/trade/indicator/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod data_item; 2 | pub mod ema; 3 | pub mod macd; 4 | pub mod rsi; 5 | 6 | pub use data_item::ToDataItem; 7 | use ta::DataItem; 8 | 9 | pub trait Indicator { 10 | type Output; 11 | fn compute(&mut self, data: &[DataItem]) -> Self::Output; 12 | } 13 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/trade/indicator/rsi.rs: -------------------------------------------------------------------------------- 1 | use polars::series::Series; 2 | use ta::{ 3 | indicators::RelativeStrengthIndex as Rsi, 4 | Close, 5 | DataItem, 6 | Next, 7 | }; 8 | 9 | use super::Indicator; 10 | 11 | #[derive(Default, Debug, Clone)] 12 | pub struct RsiOutputBuilder { 13 | rsi: Rsi, 14 | } 15 | 16 | impl RsiOutputBuilder { 17 | pub fn new(period: usize) -> Self { 18 | Self { 19 | rsi: Rsi::new(period).unwrap(), 20 | } 21 | } 22 | } 23 | 24 | impl Indicator for RsiOutputBuilder { 25 | type Output = Series; 26 | 27 | fn compute(&mut self, data: &[DataItem]) -> Self::Output { 28 | let f = |v: &DataItem| self.rsi.next(v.close()); 29 | data.iter().map(f).collect() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/trade/macros.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! min { 3 | ($x:expr) => { 4 | $x 5 | }; 6 | ($x:expr, $y:expr) => { 7 | if $x < $y { 8 | $x 9 | } else { 10 | $y 11 | } 12 | }; 13 | ($x:expr, $($rest:expr),+) => { 14 | min!($x, min!($($rest),+)) 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/trade/mod.rs: -------------------------------------------------------------------------------- 1 | mod indicator; 2 | mod macros; 3 | mod strategy; 4 | 5 | pub use indicator::{ 6 | Indicator, 7 | ToDataItem, 8 | }; 9 | pub use strategy::{ 10 | CommonMacdAndRsiStrategy, 11 | DoubleEmaStrategy, 12 | Strategy, 13 | }; 14 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/trade/strategy/mod.rs: -------------------------------------------------------------------------------- 1 | use rsquant_tool::Name; 2 | use ta::DataItem; 3 | 4 | use crate::entity::side; 5 | 6 | mod common_macd_and_rsi; 7 | mod double_ema; 8 | mod rsi_and_double_ema; 9 | 10 | pub use common_macd_and_rsi::CommonMacdAndRsiStrategy; 11 | pub use double_ema::DoubleEmaStrategy; 12 | pub use rsi_and_double_ema::RsiAndDoubleEmaStrategy; 13 | 14 | pub trait Strategy: Name { 15 | fn check(&mut self, data: &[DataItem]) -> side::TradeSide; 16 | } 17 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/trade/strategy/rsi_and_double_ema.rs: -------------------------------------------------------------------------------- 1 | use rsquant_derive::Name; 2 | use rsquant_tool::Name; 3 | use ta::Close; 4 | 5 | use super::Strategy; 6 | use crate::{ 7 | entity::side::TradeSide, 8 | trade::{ 9 | indicator::{ 10 | ema::EmaOutputBuilder, 11 | rsi::RsiOutputBuilder, 12 | }, 13 | Indicator, 14 | }, 15 | }; 16 | 17 | #[derive(Debug, Clone, Name)] 18 | pub struct RsiAndDoubleEmaStrategy { 19 | short_ema: EmaOutputBuilder, 20 | long_ema: EmaOutputBuilder, 21 | rsi: RsiOutputBuilder, 22 | rsi_buy_limit: f64, 23 | rsi_sell_limit: f64, 24 | } 25 | 26 | impl RsiAndDoubleEmaStrategy { 27 | pub fn new( 28 | short_ema_period: usize, 29 | long_ema_period: usize, 30 | rsi_period: usize, 31 | rsi_buy_limit: f64, 32 | rsi_sell_limit: f64, 33 | ) -> Self { 34 | let short_ema = EmaOutputBuilder::new(short_ema_period); 35 | let long_ema = EmaOutputBuilder::new(long_ema_period); 36 | let rsi = RsiOutputBuilder::new(rsi_period); 37 | Self { 38 | short_ema, 39 | long_ema, 40 | rsi, 41 | rsi_buy_limit, 42 | rsi_sell_limit, 43 | } 44 | } 45 | } 46 | 47 | impl Strategy for RsiAndDoubleEmaStrategy { 48 | fn check(&mut self, data: &[ta::DataItem]) -> crate::entity::side::TradeSide { 49 | // 1. compute double ema and rsi 50 | // 2. if last close is upward of short ema and last rsi larger than rsi_buy_limit, then buy 51 | // 3. if last close is downward of short ema, sell last order 52 | 53 | let short_ema = self.short_ema.compute(data); 54 | let long_ema = self.long_ema.compute(data); 55 | let rsi = self.rsi.compute(data); 56 | 57 | let last_close = data.last().unwrap().close(); 58 | let last_short_ema = short_ema 59 | .tail(Some(1)) 60 | .get(0) 61 | .ok() 62 | .and_then(|v| v.extract()) 63 | .unwrap_or_default(); 64 | let last_long_ema = long_ema 65 | .tail(Some(1)) 66 | .get(0) 67 | .ok() 68 | .and_then(|v| v.extract()) 69 | .unwrap_or_default(); 70 | let last_rsi: f64 = rsi 71 | .tail(Some(1)) 72 | .get(0) 73 | .ok() 74 | .and_then(|v| v.extract()) 75 | .unwrap_or_default(); 76 | 77 | if last_close > last_short_ema 78 | && last_close > last_long_ema 79 | && last_rsi > self.rsi_buy_limit 80 | { 81 | return TradeSide::Buy; 82 | } 83 | if last_close < last_short_ema { 84 | return TradeSide::Sell; 85 | } 86 | TradeSide::Nop 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/util/constants.rs: -------------------------------------------------------------------------------- 1 | pub const DEFAULT_APP_NAME: &str = "rsquant"; 2 | pub const DEFAULT_LOG_FILE: &str = "rsquant.log"; 3 | pub const DEFAULT_POSTGRES_ADDR: &str = "postgres://postgres:postgres@localhost/rsquant"; 4 | 5 | pub const DEFAULT_DATETIME_FORMAT_STR: &str = "%Y-%m-%d %H:%M:%S %z"; 6 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/util/env.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | pub struct EnvManager; 4 | 5 | impl EnvManager { 6 | pub fn get_env_var(key: &str) -> Option { 7 | env::var(key).ok().map_or_else( 8 | || { 9 | tracing::warn!("Environment variable `{}` is unset!", key); 10 | None 11 | }, 12 | Some, 13 | ) 14 | } 15 | 16 | pub fn get_env_var_or(key: &str, default: impl Into) -> String { 17 | match env::var(key) { 18 | Ok(v) => v, 19 | Err(_) => default.into(), 20 | } 21 | } 22 | } 23 | 24 | #[cfg(test)] 25 | mod tests { 26 | use std::env; 27 | 28 | use super::EnvManager; 29 | 30 | #[allow(deprecated)] 31 | #[test] 32 | fn test_get_env_var() { 33 | let actual = EnvManager::get_env_var("HOME").unwrap_or("".into()); 34 | let expect = env::home_dir().unwrap().display().to_string(); 35 | assert_eq!(actual, expect); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/util/log.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | 3 | use tracing_appender::non_blocking::{ 4 | NonBlocking, 5 | WorkerGuard, 6 | }; 7 | use tracing_subscriber::{ 8 | layer::SubscriberExt, 9 | util::SubscriberInitExt, 10 | EnvFilter, 11 | }; 12 | 13 | use crate::{ 14 | util::{ 15 | config::LogConfig, 16 | constants::DEFAULT_LOG_FILE, 17 | }, 18 | Result, 19 | }; 20 | 21 | pub struct Logger { 22 | log_dir: String, 23 | log_file: NonBlocking, 24 | _guards: Vec, 25 | } 26 | 27 | impl Logger { 28 | pub fn from_config(config: LogConfig) -> Self { 29 | let log_path = config.log_path.unwrap_or(DEFAULT_LOG_FILE.into()); 30 | 31 | let file_appender = tracing_appender::rolling::never(&log_path, DEFAULT_LOG_FILE); 32 | let (log_file, guard) = tracing_appender::non_blocking(file_appender); 33 | 34 | Self { 35 | log_dir: log_path, 36 | _guards: vec![guard], 37 | log_file, 38 | } 39 | } 40 | 41 | pub fn init(&self) -> Result<()> { 42 | self.init_log_file()?; 43 | self.init_logger()?; 44 | 45 | Ok(()) 46 | } 47 | 48 | fn init_log_file(&self) -> Result<()> { 49 | fs::create_dir_all(&self.log_dir)?; 50 | 51 | Ok(()) 52 | } 53 | 54 | fn init_logger(&self) -> Result<()> { 55 | let filter_layer = EnvFilter::try_from_env("QUANT_LOG_LEVEL") 56 | .or_else(|_| EnvFilter::try_new("info")) 57 | .unwrap(); 58 | 59 | let file_layer = tracing_subscriber::fmt::layer() 60 | .with_target(true) 61 | .with_ansi(false) 62 | .with_file(false) 63 | .with_line_number(true) 64 | .with_thread_names(true) 65 | .with_thread_ids(false) 66 | .with_writer(self.log_file.clone()); 67 | 68 | let tui_layer = tracing_subscriber::fmt::layer() 69 | .with_target(true) 70 | .with_ansi(true) 71 | .with_file(false) 72 | .with_line_number(true) 73 | .with_thread_names(false) 74 | .with_thread_ids(false) 75 | .with_writer(std::io::stdout); 76 | 77 | tracing_subscriber::registry() 78 | .with(file_layer) 79 | .with(tui_layer) 80 | .with(filter_layer) 81 | .init(); 82 | 83 | Ok(()) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/util/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | pub mod constants; 3 | pub mod email; 4 | pub mod env; 5 | pub mod log; 6 | pub mod time; 7 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/util/time/converter.rs: -------------------------------------------------------------------------------- 1 | use chrono::{ 2 | DateTime, 3 | Local, 4 | TimeZone, 5 | Utc, 6 | }; 7 | 8 | use crate::util::{ 9 | constants::DEFAULT_DATETIME_FORMAT_STR, 10 | time::{ 11 | LocalTimeTool, 12 | UtcTimeTool, 13 | }, 14 | }; 15 | 16 | pub trait TimeConverter 17 | where 18 | Tz::Offset: std::fmt::Display, 19 | { 20 | fn to_date_time(unix_time: i64) -> Option>; 21 | 22 | fn convert_to_date_time(unix_time: i64) -> Option { 23 | Self::to_date_time(unix_time).map(|t| t.format(DEFAULT_DATETIME_FORMAT_STR).to_string()) 24 | } 25 | 26 | fn convert_to_unix_time(date_time: &str) -> Option { 27 | let date_time = DateTime::parse_from_str(date_time, DEFAULT_DATETIME_FORMAT_STR).ok()?; 28 | Some(date_time.timestamp_millis()) 29 | } 30 | } 31 | 32 | impl TimeConverter for LocalTimeTool { 33 | fn to_date_time(unix_time: i64) -> Option> { 34 | let dt_utc = DateTime::from_timestamp_millis(unix_time)?; 35 | let dt = dt_utc.with_timezone(&Local); 36 | Some(dt) 37 | } 38 | } 39 | 40 | impl TimeConverter for UtcTimeTool { 41 | fn to_date_time(unix_time: i64) -> Option> { 42 | let dt = DateTime::from_timestamp_millis(unix_time)?; 43 | Some(dt) 44 | } 45 | } 46 | 47 | #[cfg(test)] 48 | mod tests { 49 | use super::{ 50 | LocalTimeTool, 51 | TimeConverter, 52 | }; 53 | 54 | #[test] 55 | fn test_time_converter() { 56 | let expect_date_time = "2023-05-20 23:00:00 +0800"; 57 | let expect_unix_time = LocalTimeTool::convert_to_unix_time(expect_date_time).unwrap_or(0); 58 | let actual_date_time = 59 | LocalTimeTool::convert_to_date_time(expect_unix_time).unwrap_or("".into()); 60 | assert_eq!(actual_date_time, expect_date_time); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/util/time/current.rs: -------------------------------------------------------------------------------- 1 | use chrono::{ 2 | DateTime, 3 | Local, 4 | TimeZone, 5 | Utc, 6 | }; 7 | 8 | use crate::util::{ 9 | constants::DEFAULT_DATETIME_FORMAT_STR, 10 | time::{ 11 | LocalTimeTool, 12 | UtcTimeTool, 13 | }, 14 | }; 15 | 16 | pub trait CurrentTime 17 | where 18 | Tz::Offset: std::fmt::Display, 19 | { 20 | fn get_current() -> DateTime; 21 | 22 | fn get_unix_time() -> u64 { 23 | Self::get_current().timestamp_millis() as u64 24 | } 25 | 26 | fn get_date_time() -> String { 27 | Self::get_current() 28 | .format(DEFAULT_DATETIME_FORMAT_STR) 29 | .to_string() 30 | } 31 | } 32 | 33 | impl CurrentTime for LocalTimeTool { 34 | fn get_current() -> DateTime { 35 | Local::now() 36 | } 37 | } 38 | 39 | impl CurrentTime for UtcTimeTool { 40 | fn get_current() -> DateTime { 41 | Utc::now() 42 | } 43 | } 44 | 45 | pub enum DurationInterval { 46 | Seconds1, 47 | Minutes1, 48 | Hours1, 49 | Days1, 50 | Weeks1, 51 | Months1, 52 | Years1, 53 | } 54 | 55 | pub trait GetDuration { 56 | fn get_duration(&self, interval: DurationInterval, count: i64) -> (u64, u64); 57 | } 58 | 59 | impl GetDuration for UtcTimeTool { 60 | fn get_duration(&self, interval: DurationInterval, count: i64) -> (u64, u64) { 61 | let current = Self::get_current(); 62 | let start = match interval { 63 | DurationInterval::Seconds1 => current - chrono::Duration::seconds(count), 64 | DurationInterval::Minutes1 => current - chrono::Duration::minutes(count), 65 | DurationInterval::Hours1 => current - chrono::Duration::hours(count), 66 | DurationInterval::Days1 => current - chrono::Duration::days(count), 67 | DurationInterval::Weeks1 => current - chrono::Duration::weeks(count), 68 | DurationInterval::Months1 => current - chrono::Duration::days(count * 30), 69 | DurationInterval::Years1 => current - chrono::Duration::days(count * 365), 70 | }; 71 | ( 72 | start.timestamp_millis() as u64, 73 | current.timestamp_millis() as u64, 74 | ) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/util/time/mod.rs: -------------------------------------------------------------------------------- 1 | extern crate chrono; 2 | 3 | mod converter; 4 | mod current; 5 | mod timezone; 6 | 7 | pub use converter::TimeConverter; 8 | pub use current::{ 9 | CurrentTime, 10 | DurationInterval, 11 | GetDuration, 12 | }; 13 | pub use timezone::TimeZoneConverter; 14 | 15 | pub struct LocalTimeTool; 16 | pub struct UtcTimeTool; 17 | 18 | pub fn u64_to_datetime<'de, D>(deserializer: D) -> Result 19 | where 20 | D: serde::Deserializer<'de>, 21 | { 22 | let timestamp: i64 = serde::Deserialize::deserialize(deserializer)?; 23 | let naive = LocalTimeTool::to_date_time(timestamp) 24 | .unwrap_or_default() 25 | .naive_local(); 26 | 27 | Ok(naive) 28 | } 29 | -------------------------------------------------------------------------------- /crates/rsquant-core/src/util/time/timezone.rs: -------------------------------------------------------------------------------- 1 | pub struct TimeZoneConverter; 2 | 3 | impl TimeZoneConverter { 4 | pub fn convert_utc_to_local(utc_time: u64) -> u64 { 5 | utc_time + 8 * 60 * 60 * 1000 6 | } 7 | 8 | pub fn convert_local_to_utc(local_time: u64) -> u64 { 9 | local_time - 8 * 60 * 60 * 1000 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /crates/rsquant-core/template/email/monitor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 20 | 21 |

{{ datetime }} {{ interval }} 趋势跟踪

22 |

23 |

买多

24 | 25 | 26 | 27 | {% for header in headers %} 28 | 29 | {% endfor %} 30 | 31 | 32 | 33 | {% for s in buy_symbols %} 34 | 35 | 36 | 37 | {% endfor %} 38 | 39 | 40 |

41 |

卖空

42 | 43 | 44 | 45 | {% for header in headers %} 46 | 47 | {% endfor %} 48 | 49 | 50 | 51 | {% for s in sell_symbols %} 52 | 53 | 54 | 55 | {% endfor %} 56 | 57 | 58 |

59 | 60 | 61 | -------------------------------------------------------------------------------- /crates/rsquant-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rsquant-derive" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | proc-macro = true 8 | 9 | [dependencies] 10 | rsquant-tool = { path = "../rsquant-tool" } 11 | 12 | proc-macro2 = "1.0.86" 13 | quote = "1.0.36" 14 | syn = "2.0.67" 15 | -------------------------------------------------------------------------------- /crates/rsquant-derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::{ 2 | self, 3 | TokenStream, 4 | }; 5 | use quote::quote; 6 | use syn::{ 7 | parse_macro_input, 8 | DeriveInput, 9 | }; 10 | 11 | #[proc_macro_derive(Name)] 12 | pub fn derive(input: TokenStream) -> TokenStream { 13 | let DeriveInput { 14 | ident, generics, .. 15 | } = parse_macro_input!(input); 16 | let (impl_generics, type_generics, where_clause) = generics.split_for_impl(); 17 | 18 | let ident_str = ident.to_string(); 19 | let output = quote! { 20 | impl #impl_generics Name for #ident #type_generics #where_clause { 21 | fn get_name(&self) -> String { 22 | #ident_str.into() 23 | } 24 | } 25 | }; 26 | output.into() 27 | } 28 | -------------------------------------------------------------------------------- /crates/rsquant-tool/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rsquant-tool" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | -------------------------------------------------------------------------------- /crates/rsquant-tool/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub trait Name { 2 | fn get_name(&self) -> String; 3 | } 4 | -------------------------------------------------------------------------------- /deps/binan_spot/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "binan_spot" 3 | version = "1.0.0" 4 | authors = ["Binance"] 5 | edition = "2021" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [features] 10 | default = ["enable-ureq", "enable-tungstenite"] 11 | enable-hyper = ["hyper", "hyper-tls", "serde_json", "futures-util", "tokio"] 12 | enable-ureq = ["ureq", "serde_json"] 13 | enable-tungstenite = ["tungstenite"] 14 | enable-tokio-tungstenite = ["tokio-tungstenite"] 15 | full = [ 16 | "enable-hyper", 17 | "enable-tungstenite", 18 | "enable-ureq", 19 | "enable-tokio-tungstenite", 20 | ] 21 | 22 | [dependencies] 23 | hmac = "0.12.0" 24 | log = "0.4.16" 25 | serde = { version = "1.0.163", features = ["derive"] } 26 | sha2 = { version = "0.10.6", default-features = false, features = ["oid"] } 27 | url = "2.2.2" 28 | rust_decimal = "1.29.0" 29 | http = "0.2.9" 30 | strum = { version = "0.24", features = ["derive"] } 31 | rsa = { version = "0.9.2", features = ["pkcs5"] } 32 | rand = "0.8.5" 33 | signature = "2.1.0" 34 | base64 = "0.21.2" 35 | 36 | # enable-ureq 37 | ureq = { version = "2.6.0", optional = true } 38 | 39 | # enable-hyper 40 | hyper = { version = "0.14.16", features = ["full"], optional = true } 41 | serde_json = { version = "1.0.96", optional = true } 42 | hyper-tls = { version = "0.5.0", optional = true } 43 | futures-util = { version = "0.3.28", optional = true } 44 | tokio = { version = "1.28.2", features = ["time"], optional = true } 45 | 46 | # enable-tungstenite 47 | tungstenite = { version = "0.19.0", features = ["native-tls"], optional = true } 48 | 49 | # enable-tokio-tungstenite 50 | tokio-tungstenite = { version = "0.19.0", features = [ 51 | "native-tls", 52 | ], optional = true } 53 | hyper-proxy = "0.9.1" 54 | reqwest = { version = "0.12.4", default-features = false, features = [ 55 | "rustls-tls", 56 | "json", 57 | "gzip", 58 | ] } 59 | 60 | 61 | [dev-dependencies] 62 | tokio = { version = "1.28.2", features = ["full"] } 63 | env_logger = "0.10.0" 64 | tower = "0.4.12" 65 | rust_decimal_macros = "1.29.0" 66 | cargo-audit = "0.17.4" 67 | -------------------------------------------------------------------------------- /deps/binan_spot/src/http/credentials.rs: -------------------------------------------------------------------------------- 1 | /// Binance API Credentials. 2 | /// 3 | /// Communication with Binance API USER_DATA endpoints requires 4 | /// valid API credentials. 5 | /// 6 | /// Note: Production and TESTNET API Credentials are not 7 | /// interchangeable. 8 | /// 9 | /// [API Documentation](https://binance-docs.github.io/apidocs/spot/en/#api-key-restrictions) 10 | #[derive(PartialEq, Eq, Clone)] 11 | pub struct Credentials { 12 | pub api_key: String, 13 | pub signature: Signature, 14 | } 15 | 16 | #[derive(PartialEq, Eq, Clone)] 17 | pub enum Signature { 18 | Hmac(HmacSignature), 19 | Rsa(RsaSignature), 20 | } 21 | 22 | #[derive(PartialEq, Eq, Clone)] 23 | pub struct HmacSignature { 24 | pub api_secret: String, 25 | } 26 | 27 | #[derive(PartialEq, Eq, Clone)] 28 | pub struct RsaSignature { 29 | pub key: String, 30 | pub password: Option, 31 | } 32 | 33 | impl Credentials { 34 | pub fn from_rsa(api_key: impl Into, key: impl Into) -> Self { 35 | Credentials { 36 | api_key: api_key.into(), 37 | signature: Signature::Rsa(RsaSignature { 38 | key: key.into(), 39 | password: None, 40 | }), 41 | } 42 | } 43 | 44 | pub fn from_rsa_protected( 45 | api_key: impl Into, 46 | key: impl Into, 47 | password: impl Into, 48 | ) -> Self { 49 | Credentials { 50 | api_key: api_key.into(), 51 | signature: Signature::Rsa(RsaSignature { 52 | key: key.into(), 53 | password: Some(password.into()), 54 | }), 55 | } 56 | } 57 | 58 | pub fn from_hmac(api_key: impl Into, api_secret: impl Into) -> Self { 59 | Credentials { 60 | api_key: api_key.into(), 61 | signature: Signature::Hmac(HmacSignature { 62 | api_secret: api_secret.into(), 63 | }), 64 | } 65 | } 66 | } 67 | 68 | impl std::fmt::Debug for Credentials { 69 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 70 | f.debug_struct("Credentials") 71 | .field("api_key", &"[redacted]") 72 | .finish() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /deps/binan_spot/src/http/method.rs: -------------------------------------------------------------------------------- 1 | #[derive(PartialEq, Eq, Clone, Debug)] 2 | pub enum Method { 3 | Get, 4 | Post, 5 | Put, 6 | Delete, 7 | } 8 | 9 | impl AsRef for Method { 10 | fn as_ref(&self) -> &str { 11 | match self { 12 | Method::Post => "POST", 13 | Method::Delete => "DELETE", 14 | Method::Get => "GET", 15 | Method::Put => "PUT", 16 | } 17 | } 18 | } 19 | 20 | impl From for reqwest::Method { 21 | fn from(method: Method) -> Self { 22 | match method { 23 | Method::Get => reqwest::Method::GET, 24 | Method::Post => reqwest::Method::POST, 25 | Method::Delete => reqwest::Method::DELETE, 26 | Method::Put => reqwest::Method::PUT, 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /deps/binan_spot/src/http/mod.rs: -------------------------------------------------------------------------------- 1 | mod credentials; 2 | mod method; 3 | 4 | pub mod error; 5 | pub mod request; 6 | 7 | pub use credentials::{ 8 | Credentials, 9 | HmacSignature, 10 | RsaSignature, 11 | Signature, 12 | }; 13 | pub use method::Method; 14 | -------------------------------------------------------------------------------- /deps/binan_spot/src/http/request.rs: -------------------------------------------------------------------------------- 1 | use crate::http::{ 2 | Credentials, 3 | Method, 4 | }; 5 | 6 | #[derive(PartialEq, Eq, Debug, Clone)] 7 | pub struct Request { 8 | pub method: Method, 9 | pub path: String, 10 | pub params: Vec<(String, String)>, 11 | pub credentials: Option, 12 | pub sign: bool, 13 | } 14 | 15 | impl Request { 16 | pub fn method(&self) -> &Method { 17 | &self.method 18 | } 19 | 20 | pub fn path(&self) -> &str { 21 | &self.path 22 | } 23 | 24 | pub fn params(&self) -> &[(String, String)] { 25 | &self.params 26 | } 27 | 28 | pub fn credentials(&self) -> &Option { 29 | &self.credentials 30 | } 31 | 32 | pub fn sign(&self) -> &bool { 33 | &self.sign 34 | } 35 | } 36 | 37 | /// /// API HTTP Request 38 | /// 39 | /// A low-level request builder for API integration 40 | /// decoupled from any specific underlying HTTP library. 41 | pub struct RequestBuilder { 42 | method: Method, 43 | path: String, 44 | params: Vec<(String, String)>, 45 | credentials: Option, 46 | sign: bool, 47 | } 48 | 49 | impl RequestBuilder { 50 | pub fn new(method: Method, path: &str) -> Self { 51 | Self { 52 | method, 53 | path: path.to_owned(), 54 | params: vec![], 55 | credentials: None, 56 | sign: false, 57 | } 58 | } 59 | 60 | /// Append `params` to the request's query string. Parameters may 61 | /// share the same key, and will result in a query string with one or 62 | /// more duplicated query parameter keys. 63 | pub fn params<'a>(mut self, params: impl IntoIterator) -> Self { 64 | self.params.extend( 65 | params 66 | .into_iter() 67 | .map(|param| (param.0.to_owned(), param.1.to_owned())), 68 | ); 69 | 70 | self 71 | } 72 | 73 | pub fn credentials(mut self, credentials: Credentials) -> Self { 74 | self.credentials = Some(credentials); 75 | 76 | self 77 | } 78 | 79 | pub fn sign(mut self) -> Self { 80 | self.sign = true; 81 | 82 | self 83 | } 84 | } 85 | 86 | impl From for Request { 87 | fn from(builder: RequestBuilder) -> Request { 88 | Request { 89 | method: builder.method, 90 | path: builder.path, 91 | params: builder.params, 92 | credentials: builder.credentials, 93 | sign: builder.sign, 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /deps/binan_spot/src/hyper/error.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | error, 3 | fmt, 4 | }; 5 | 6 | use http::{ 7 | uri::InvalidUri, 8 | Error as HttpError, 9 | }; 10 | use hyper::Error as HyperError; 11 | 12 | use crate::http::error::{ 13 | ClientError, 14 | HttpError as BinanceHttpError, 15 | }; 16 | 17 | /// Communication error with the server. 18 | #[derive(Debug)] 19 | pub enum Error { 20 | /// 4XX error from the server. 21 | Client(ClientError), 22 | /// 5XX error from the server. 23 | Server(BinanceHttpError), 24 | /// The format of the API secret is invalid. 25 | InvalidApiSecret, 26 | Parse(HttpError), 27 | Send(HyperError), 28 | } 29 | 30 | impl From for Error { 31 | fn from(err: InvalidUri) -> Error { 32 | Error::Parse(err.into()) 33 | } 34 | } 35 | 36 | impl fmt::Display for Error { 37 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 38 | match *self { 39 | Error::Client(ref e) => write!(f, "Client Error: {}", e), 40 | Error::Server(ref e) => write!(f, "Server Error: {}", e), 41 | Error::InvalidApiSecret => write!(f, "Invalid ApiSecret"), 42 | Error::Parse(ref e) => write!(f, "Parse Error: {}", e), 43 | Error::Send(ref e) => write!(f, "Send Error: {}", e), 44 | } 45 | } 46 | } 47 | 48 | impl error::Error for Error { 49 | fn description(&self) -> &str { 50 | match *self { 51 | Error::Client(..) => "Client error", 52 | Error::Server(..) => "Server error", 53 | Error::InvalidApiSecret => "InvalidApiSecret error", 54 | Error::Parse(..) => "Parse error", 55 | Error::Send(..) => "Send error", 56 | } 57 | } 58 | 59 | fn cause(&self) -> Option<&dyn error::Error> { 60 | match *self { 61 | Error::Client(ref e) => Some(e), 62 | Error::Server(ref e) => Some(e), 63 | Error::InvalidApiSecret => None, 64 | Error::Parse(ref e) => Some(e), 65 | Error::Send(ref e) => Some(e), 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /deps/binan_spot/src/hyper/mod.rs: -------------------------------------------------------------------------------- 1 | //! Binance client using Hyper. 2 | //! 3 | //! # Example 4 | //! 5 | //! ```no_run 6 | //! use binance_spot_connector_rust::{ 7 | //! http::{request::RequestBuilder, Credentials, Method}, 8 | //! hyper::{BinanceHttpClient, Error}, 9 | //! }; 10 | //! use env_logger::Builder; 11 | //! 12 | //! #[tokio::main] 13 | //! async fn main() -> Result<(), Error> { 14 | //! Builder::from_default_env() 15 | //! .filter(None, log::LevelFilter::Info) 16 | //! .init(); 17 | //! let credentials = Credentials::from_hmac("api-key".to_owned(), "api-secret".to_owned()); 18 | //! let client = BinanceHttpClient::default().credentials(credentials); 19 | //! let request = RequestBuilder::new(Method::Post, "/api/v3/order").params(vec![ 20 | //! ("symbol", "BNBUSDT"), 21 | //! ("side", "SELL"), 22 | //! ("type", "LIMIT"), 23 | //! ("quantity", "0.1"), 24 | //! ("price", "320.2"), 25 | //! ]); 26 | //! let data = client.send(request).await?.into_body_str().await?; 27 | //! log::info!("{}", data); 28 | //! Ok(()) 29 | //! } 30 | //! ``` 31 | 32 | mod client; 33 | mod error; 34 | mod response; 35 | 36 | pub use client::*; 37 | pub use error::*; 38 | pub use response::*; 39 | -------------------------------------------------------------------------------- /deps/binan_spot/src/hyper/response.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use hyper::Body; 4 | 5 | use crate::{ 6 | http::error::{ 7 | BinanceApiError, 8 | ClientError, 9 | HttpError, 10 | }, 11 | hyper::Error, 12 | }; 13 | 14 | /// REST Response 15 | #[derive(Debug)] 16 | pub struct Response { 17 | inner_response: hyper::Response, 18 | } 19 | 20 | impl Response { 21 | pub async fn into_body_str(self) -> Result { 22 | let status = self.inner_response.status().as_u16(); 23 | if 400 <= status { 24 | let headers: HashMap = 25 | self.inner_response 26 | .headers() 27 | .iter() 28 | .fold(HashMap::new(), |mut headers, (k, v)| { 29 | headers.entry(k.as_str().to_owned()).or_insert_with(|| { 30 | // Assume all Binance response headers can convert to String. 31 | v.to_str() 32 | .expect("Failed to convert response header value to string") 33 | .to_owned() 34 | }); 35 | headers 36 | }); 37 | 38 | let content = hyper_body_to_string(self.inner_response.into_body()).await?; 39 | if 500 <= status { 40 | Err(Error::Server(HttpError::new(status, content, headers))) 41 | } else { 42 | let client_error = match serde_json::from_str::(&content) { 43 | Ok(err) => ClientError::Structured(HttpError::new(status, err, headers)), 44 | Err(_) => ClientError::Raw(HttpError::new(status, content, headers)), 45 | }; 46 | 47 | Err(Error::Client(client_error)) 48 | } 49 | } else { 50 | Ok(hyper_body_to_string(self.inner_response.into_body()).await?) 51 | } 52 | } 53 | } 54 | 55 | impl From> for Response { 56 | fn from(response: hyper::Response) -> Response { 57 | Response { 58 | inner_response: response, 59 | } 60 | } 61 | } 62 | 63 | impl From for hyper::Response { 64 | fn from(response: Response) -> hyper::Response { 65 | response.inner_response 66 | } 67 | } 68 | 69 | async fn hyper_body_to_string(body: Body) -> Result { 70 | // Assume all Binance responses are of a reasonable size. 71 | let body = hyper::body::to_bytes(body) 72 | .await 73 | .expect("Failed to collect response body."); 74 | 75 | // Assume all Binance responses are in UTF-8. 76 | let content = String::from_utf8(body.to_vec()).expect("Response failed UTF-8 encoding."); 77 | 78 | Ok(content) 79 | } 80 | -------------------------------------------------------------------------------- /deps/binan_spot/src/isolated_margin_stream/close_listen_key.rs: -------------------------------------------------------------------------------- 1 | use crate::http::{ 2 | request::Request, 3 | Credentials, 4 | Method, 5 | }; 6 | 7 | /// `DELETE /sapi/v1/userDataStream/isolated` 8 | /// 9 | /// Close out a user data stream. 10 | /// 11 | /// Weight: 1 12 | /// 13 | /// # Example 14 | /// 15 | /// ``` 16 | /// use binance_spot_connector_rust::isolated_margin_stream; 17 | /// 18 | /// let request = isolated_margin_stream::close_listen_key("BTCUSDT", "listen-key"); 19 | /// ``` 20 | pub struct CloseListenKey { 21 | symbol: String, 22 | listen_key: String, 23 | credentials: Option, 24 | } 25 | 26 | impl CloseListenKey { 27 | pub fn new(symbol: &str, listen_key: &str) -> Self { 28 | Self { 29 | symbol: symbol.to_owned(), 30 | listen_key: listen_key.to_owned(), 31 | credentials: None, 32 | } 33 | } 34 | 35 | pub fn credentials(mut self, credentials: &Credentials) -> Self { 36 | self.credentials = Some(credentials.clone()); 37 | self 38 | } 39 | } 40 | 41 | impl From for Request { 42 | fn from(request: CloseListenKey) -> Request { 43 | let params = vec![ 44 | ("symbol".to_owned(), request.symbol.to_string()), 45 | ("listenKey".to_owned(), request.listen_key.to_string()), 46 | ]; 47 | 48 | Request { 49 | path: "/sapi/v1/userDataStream/isolated".to_owned(), 50 | method: Method::Delete, 51 | params, 52 | credentials: request.credentials, 53 | sign: false, 54 | } 55 | } 56 | } 57 | 58 | #[cfg(test)] 59 | mod tests { 60 | use super::CloseListenKey; 61 | use crate::http::{ 62 | request::Request, 63 | Credentials, 64 | Method, 65 | }; 66 | 67 | static API_KEY: &str = "api-key"; 68 | static API_SECRET: &str = "api-secret"; 69 | 70 | #[test] 71 | fn isolated_margin_stream_close_listen_key_convert_to_request_test() { 72 | let credentials = Credentials::from_hmac(API_KEY.to_owned(), API_SECRET.to_owned()); 73 | 74 | let request: Request = CloseListenKey::new("BTCUSDT", "listen-key") 75 | .credentials(&credentials) 76 | .into(); 77 | 78 | assert_eq!( 79 | request, 80 | Request { 81 | path: "/sapi/v1/userDataStream/isolated".to_owned(), 82 | credentials: Some(credentials), 83 | method: Method::Delete, 84 | params: vec![ 85 | ("symbol".to_owned(), "BTCUSDT".to_string()), 86 | ("listenKey".to_owned(), "listen-key".to_string()), 87 | ], 88 | sign: false 89 | } 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /deps/binan_spot/src/isolated_margin_stream/mod.rs: -------------------------------------------------------------------------------- 1 | //! Market Data 2 | 3 | pub mod close_listen_key; 4 | pub mod new_listen_key; 5 | pub mod renew_listen_key; 6 | 7 | use close_listen_key::CloseListenKey; 8 | use new_listen_key::NewListenKey; 9 | use renew_listen_key::RenewListenKey; 10 | 11 | pub fn new_listen_key(symbol: &str) -> NewListenKey { 12 | NewListenKey::new(symbol) 13 | } 14 | 15 | pub fn renew_listen_key(symbol: &str, listen_key: &str) -> RenewListenKey { 16 | RenewListenKey::new(symbol, listen_key) 17 | } 18 | 19 | pub fn close_listen_key(symbol: &str, listen_key: &str) -> CloseListenKey { 20 | CloseListenKey::new(symbol, listen_key) 21 | } 22 | -------------------------------------------------------------------------------- /deps/binan_spot/src/isolated_margin_stream/new_listen_key.rs: -------------------------------------------------------------------------------- 1 | use crate::http::{ 2 | request::Request, 3 | Credentials, 4 | Method, 5 | }; 6 | 7 | /// `POST /sapi/v1/userDataStream/isolated` 8 | /// 9 | /// Start a new user data stream. 10 | /// The stream will close after 60 minutes unless a keepalive is sent. If the account has an active `listenKey`, that `listenKey` will be returned and its validity will be extended for 60 minutes. 11 | /// 12 | /// Weight: 1 13 | /// 14 | /// # Example 15 | /// 16 | /// ``` 17 | /// use binance_spot_connector_rust::isolated_margin_stream; 18 | /// 19 | /// let request = isolated_margin_stream::new_listen_key("BTCUSDT"); 20 | /// ``` 21 | pub struct NewListenKey { 22 | symbol: String, 23 | credentials: Option, 24 | } 25 | 26 | impl NewListenKey { 27 | pub fn new(symbol: &str) -> Self { 28 | Self { 29 | symbol: symbol.to_owned(), 30 | credentials: None, 31 | } 32 | } 33 | 34 | pub fn credentials(mut self, credentials: &Credentials) -> Self { 35 | self.credentials = Some(credentials.clone()); 36 | self 37 | } 38 | } 39 | 40 | impl From for Request { 41 | fn from(request: NewListenKey) -> Request { 42 | let params = vec![("symbol".to_owned(), request.symbol.to_string())]; 43 | 44 | Request { 45 | path: "/sapi/v1/userDataStream/isolated".to_owned(), 46 | method: Method::Post, 47 | params, 48 | credentials: request.credentials, 49 | sign: false, 50 | } 51 | } 52 | } 53 | 54 | #[cfg(test)] 55 | mod tests { 56 | use super::NewListenKey; 57 | use crate::http::{ 58 | request::Request, 59 | Credentials, 60 | Method, 61 | }; 62 | 63 | static API_KEY: &str = "api-key"; 64 | static API_SECRET: &str = "api-secret"; 65 | 66 | #[test] 67 | fn isolated_margin_stream_new_listen_key_convert_to_request_test() { 68 | let credentials = Credentials::from_hmac(API_KEY.to_owned(), API_SECRET.to_owned()); 69 | 70 | let request: Request = NewListenKey::new("BTCUSDT") 71 | .credentials(&credentials) 72 | .into(); 73 | 74 | assert_eq!( 75 | request, 76 | Request { 77 | path: "/sapi/v1/userDataStream/isolated".to_owned(), 78 | credentials: Some(credentials), 79 | method: Method::Post, 80 | params: vec![("symbol".to_owned(), "BTCUSDT".to_string()),], 81 | sign: false 82 | } 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /deps/binan_spot/src/isolated_margin_stream/renew_listen_key.rs: -------------------------------------------------------------------------------- 1 | use crate::http::{ 2 | request::Request, 3 | Credentials, 4 | Method, 5 | }; 6 | 7 | /// `PUT /sapi/v1/userDataStream/isolated` 8 | /// 9 | /// Keepalive a user data stream to prevent a time out. User data streams will close after 60 minutes. It's recommended to send a ping about every 30 minutes. 10 | /// 11 | /// Weight: 1 12 | /// 13 | /// # Example 14 | /// 15 | /// ``` 16 | /// use binance_spot_connector_rust::isolated_margin_stream; 17 | /// 18 | /// let request = isolated_margin_stream::renew_listen_key("BTCUSDT", "listen-key"); 19 | /// ``` 20 | pub struct RenewListenKey { 21 | symbol: String, 22 | listen_key: String, 23 | credentials: Option, 24 | } 25 | 26 | impl RenewListenKey { 27 | pub fn new(symbol: &str, listen_key: &str) -> Self { 28 | Self { 29 | symbol: symbol.to_owned(), 30 | listen_key: listen_key.to_owned(), 31 | credentials: None, 32 | } 33 | } 34 | 35 | pub fn credentials(mut self, credentials: &Credentials) -> Self { 36 | self.credentials = Some(credentials.clone()); 37 | self 38 | } 39 | } 40 | 41 | impl From for Request { 42 | fn from(request: RenewListenKey) -> Request { 43 | let params = vec![ 44 | ("symbol".to_owned(), request.symbol.to_string()), 45 | ("listenKey".to_owned(), request.listen_key.to_string()), 46 | ]; 47 | 48 | Request { 49 | path: "/sapi/v1/userDataStream/isolated".to_owned(), 50 | method: Method::Put, 51 | params, 52 | credentials: request.credentials, 53 | sign: false, 54 | } 55 | } 56 | } 57 | 58 | #[cfg(test)] 59 | mod tests { 60 | use super::RenewListenKey; 61 | use crate::http::{ 62 | request::Request, 63 | Credentials, 64 | Method, 65 | }; 66 | 67 | static API_KEY: &str = "api-key"; 68 | static API_SECRET: &str = "api-secret"; 69 | 70 | #[test] 71 | fn isolated_margin_stream_renew_listen_key_convert_to_request_test() { 72 | let credentials = Credentials::from_hmac(API_KEY.to_owned(), API_SECRET.to_owned()); 73 | 74 | let request: Request = RenewListenKey::new("BTCUSDT", "listen-key") 75 | .credentials(&credentials) 76 | .into(); 77 | 78 | assert_eq!( 79 | request, 80 | Request { 81 | path: "/sapi/v1/userDataStream/isolated".to_owned(), 82 | credentials: Some(credentials), 83 | method: Method::Put, 84 | params: vec![ 85 | ("symbol".to_owned(), "BTCUSDT".to_string()), 86 | ("listenKey".to_owned(), "listen-key".to_string()), 87 | ], 88 | sign: false 89 | } 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /deps/binan_spot/src/margin/bnb_burn_status.rs: -------------------------------------------------------------------------------- 1 | use crate::http::{ 2 | request::Request, 3 | Credentials, 4 | Method, 5 | }; 6 | 7 | /// `GET /sapi/v1/bnbBurn` 8 | /// 9 | /// Weight(IP): 1 10 | /// 11 | /// # Example 12 | /// 13 | /// ``` 14 | /// use binance_spot_connector_rust::margin; 15 | /// 16 | /// let request = margin::bnb_burn_status(); 17 | /// ``` 18 | pub struct BNBBurnStatus { 19 | recv_window: Option, 20 | credentials: Option, 21 | } 22 | 23 | impl BNBBurnStatus { 24 | pub fn new() -> Self { 25 | Self { 26 | recv_window: None, 27 | credentials: None, 28 | } 29 | } 30 | 31 | pub fn recv_window(mut self, recv_window: u64) -> Self { 32 | self.recv_window = Some(recv_window); 33 | self 34 | } 35 | 36 | pub fn credentials(mut self, credentials: &Credentials) -> Self { 37 | self.credentials = Some(credentials.clone()); 38 | self 39 | } 40 | } 41 | 42 | impl From for Request { 43 | fn from(request: BNBBurnStatus) -> Request { 44 | let mut params = vec![]; 45 | 46 | if let Some(recv_window) = request.recv_window { 47 | params.push(("recvWindow".to_owned(), recv_window.to_string())); 48 | } 49 | 50 | Request { 51 | path: "/sapi/v1/bnbBurn".to_owned(), 52 | method: Method::Get, 53 | params, 54 | credentials: request.credentials, 55 | sign: true, 56 | } 57 | } 58 | } 59 | 60 | impl Default for BNBBurnStatus { 61 | fn default() -> Self { 62 | Self::new() 63 | } 64 | } 65 | 66 | #[cfg(test)] 67 | mod tests { 68 | use super::BNBBurnStatus; 69 | use crate::http::{ 70 | request::Request, 71 | Credentials, 72 | Method, 73 | }; 74 | 75 | static API_KEY: &str = "api-key"; 76 | static API_SECRET: &str = "api-secret"; 77 | 78 | #[test] 79 | fn margin_bnb_burn_status_convert_to_request_test() { 80 | let credentials = Credentials::from_hmac(API_KEY.to_owned(), API_SECRET.to_owned()); 81 | 82 | let request: Request = BNBBurnStatus::new() 83 | .recv_window(5000) 84 | .credentials(&credentials) 85 | .into(); 86 | 87 | assert_eq!( 88 | request, 89 | Request { 90 | path: "/sapi/v1/bnbBurn".to_owned(), 91 | credentials: Some(credentials), 92 | method: Method::Get, 93 | params: vec![("recvWindow".to_owned(), "5000".to_string()),], 94 | sign: true 95 | } 96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /deps/binan_spot/src/margin/isolated_margin_account_limit.rs: -------------------------------------------------------------------------------- 1 | use crate::http::{ 2 | request::Request, 3 | Credentials, 4 | Method, 5 | }; 6 | 7 | /// `GET /sapi/v1/margin/isolated/accountLimit` 8 | /// 9 | /// Query enabled isolated margin account limit. 10 | /// 11 | /// Weight(IP): 1 12 | /// 13 | /// # Example 14 | /// 15 | /// ``` 16 | /// use binance_spot_connector_rust::margin; 17 | /// 18 | /// let request = margin::isolated_margin_account_limit(); 19 | /// ``` 20 | pub struct IsolatedMarginAccountLimit { 21 | recv_window: Option, 22 | credentials: Option, 23 | } 24 | 25 | impl IsolatedMarginAccountLimit { 26 | pub fn new() -> Self { 27 | Self { 28 | recv_window: None, 29 | credentials: None, 30 | } 31 | } 32 | 33 | pub fn recv_window(mut self, recv_window: u64) -> Self { 34 | self.recv_window = Some(recv_window); 35 | self 36 | } 37 | 38 | pub fn credentials(mut self, credentials: &Credentials) -> Self { 39 | self.credentials = Some(credentials.clone()); 40 | self 41 | } 42 | } 43 | 44 | impl From for Request { 45 | fn from(request: IsolatedMarginAccountLimit) -> Request { 46 | let mut params = vec![]; 47 | 48 | if let Some(recv_window) = request.recv_window { 49 | params.push(("recvWindow".to_owned(), recv_window.to_string())); 50 | } 51 | 52 | Request { 53 | path: "/sapi/v1/margin/isolated/accountLimit".to_owned(), 54 | method: Method::Get, 55 | params, 56 | credentials: request.credentials, 57 | sign: true, 58 | } 59 | } 60 | } 61 | 62 | impl Default for IsolatedMarginAccountLimit { 63 | fn default() -> Self { 64 | Self::new() 65 | } 66 | } 67 | 68 | #[cfg(test)] 69 | mod tests { 70 | use super::IsolatedMarginAccountLimit; 71 | use crate::http::{ 72 | request::Request, 73 | Credentials, 74 | Method, 75 | }; 76 | 77 | static API_KEY: &str = "api-key"; 78 | static API_SECRET: &str = "api-secret"; 79 | 80 | #[test] 81 | fn margin_isolated_margin_account_limit_convert_to_request_test() { 82 | let credentials = Credentials::from_hmac(API_KEY.to_owned(), API_SECRET.to_owned()); 83 | 84 | let request: Request = IsolatedMarginAccountLimit::new() 85 | .recv_window(5000) 86 | .credentials(&credentials) 87 | .into(); 88 | 89 | assert_eq!( 90 | request, 91 | Request { 92 | path: "/sapi/v1/margin/isolated/accountLimit".to_owned(), 93 | credentials: Some(credentials), 94 | method: Method::Get, 95 | params: vec![("recvWindow".to_owned(), "5000".to_string()),], 96 | sign: true 97 | } 98 | ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /deps/binan_spot/src/margin/isolated_margin_all_symbols.rs: -------------------------------------------------------------------------------- 1 | use crate::http::{ 2 | request::Request, 3 | Credentials, 4 | Method, 5 | }; 6 | 7 | /// `GET /sapi/v1/margin/isolated/allPairs` 8 | /// 9 | /// Weight(IP): 10 10 | /// 11 | /// # Example 12 | /// 13 | /// ``` 14 | /// use binance_spot_connector_rust::margin; 15 | /// 16 | /// let request = margin::isolated_margin_all_symbols(); 17 | /// ``` 18 | pub struct IsolatedMarginAllSymbols { 19 | recv_window: Option, 20 | credentials: Option, 21 | } 22 | 23 | impl IsolatedMarginAllSymbols { 24 | pub fn new() -> Self { 25 | Self { 26 | recv_window: None, 27 | credentials: None, 28 | } 29 | } 30 | 31 | pub fn recv_window(mut self, recv_window: u64) -> Self { 32 | self.recv_window = Some(recv_window); 33 | self 34 | } 35 | 36 | pub fn credentials(mut self, credentials: &Credentials) -> Self { 37 | self.credentials = Some(credentials.clone()); 38 | self 39 | } 40 | } 41 | 42 | impl From for Request { 43 | fn from(request: IsolatedMarginAllSymbols) -> Request { 44 | let mut params = vec![]; 45 | 46 | if let Some(recv_window) = request.recv_window { 47 | params.push(("recvWindow".to_owned(), recv_window.to_string())); 48 | } 49 | 50 | Request { 51 | path: "/sapi/v1/margin/isolated/allPairs".to_owned(), 52 | method: Method::Get, 53 | params, 54 | credentials: request.credentials, 55 | sign: true, 56 | } 57 | } 58 | } 59 | 60 | impl Default for IsolatedMarginAllSymbols { 61 | fn default() -> Self { 62 | Self::new() 63 | } 64 | } 65 | 66 | #[cfg(test)] 67 | mod tests { 68 | use super::IsolatedMarginAllSymbols; 69 | use crate::http::{ 70 | request::Request, 71 | Credentials, 72 | Method, 73 | }; 74 | 75 | static API_KEY: &str = "api-key"; 76 | static API_SECRET: &str = "api-secret"; 77 | 78 | #[test] 79 | fn margin_isolated_margin_all_symbols_convert_to_request_test() { 80 | let credentials = Credentials::from_hmac(API_KEY.to_owned(), API_SECRET.to_owned()); 81 | 82 | let request: Request = IsolatedMarginAllSymbols::new() 83 | .recv_window(5000) 84 | .credentials(&credentials) 85 | .into(); 86 | 87 | assert_eq!( 88 | request, 89 | Request { 90 | path: "/sapi/v1/margin/isolated/allPairs".to_owned(), 91 | credentials: Some(credentials), 92 | method: Method::Get, 93 | params: vec![("recvWindow".to_owned(), "5000".to_string()),], 94 | sign: true 95 | } 96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /deps/binan_spot/src/margin/isolated_margin_symbol.rs: -------------------------------------------------------------------------------- 1 | use crate::http::{ 2 | request::Request, 3 | Credentials, 4 | Method, 5 | }; 6 | 7 | /// `GET /sapi/v1/margin/isolated/pair` 8 | /// 9 | /// Weight(IP): 10 10 | /// 11 | /// # Example 12 | /// 13 | /// ``` 14 | /// use binance_spot_connector_rust::margin; 15 | /// 16 | /// let request = margin::isolated_margin_symbol("BNBUSDT"); 17 | /// ``` 18 | pub struct IsolatedMarginSymbol { 19 | symbol: String, 20 | recv_window: Option, 21 | credentials: Option, 22 | } 23 | 24 | impl IsolatedMarginSymbol { 25 | pub fn new(symbol: &str) -> Self { 26 | Self { 27 | symbol: symbol.to_owned(), 28 | recv_window: None, 29 | credentials: None, 30 | } 31 | } 32 | 33 | pub fn recv_window(mut self, recv_window: u64) -> Self { 34 | self.recv_window = Some(recv_window); 35 | self 36 | } 37 | 38 | pub fn credentials(mut self, credentials: &Credentials) -> Self { 39 | self.credentials = Some(credentials.clone()); 40 | self 41 | } 42 | } 43 | 44 | impl From for Request { 45 | fn from(request: IsolatedMarginSymbol) -> Request { 46 | let mut params = vec![("symbol".to_owned(), request.symbol.to_string())]; 47 | 48 | if let Some(recv_window) = request.recv_window { 49 | params.push(("recvWindow".to_owned(), recv_window.to_string())); 50 | } 51 | 52 | Request { 53 | path: "/sapi/v1/margin/isolated/pair".to_owned(), 54 | method: Method::Get, 55 | params, 56 | credentials: request.credentials, 57 | sign: true, 58 | } 59 | } 60 | } 61 | 62 | #[cfg(test)] 63 | mod tests { 64 | use super::IsolatedMarginSymbol; 65 | use crate::http::{ 66 | request::Request, 67 | Credentials, 68 | Method, 69 | }; 70 | 71 | static API_KEY: &str = "api-key"; 72 | static API_SECRET: &str = "api-secret"; 73 | 74 | #[test] 75 | fn margin_isolated_margin_symbol_convert_to_request_test() { 76 | let credentials = Credentials::from_hmac(API_KEY.to_owned(), API_SECRET.to_owned()); 77 | 78 | let request: Request = IsolatedMarginSymbol::new("BNBUSDT") 79 | .recv_window(5000) 80 | .credentials(&credentials) 81 | .into(); 82 | 83 | assert_eq!( 84 | request, 85 | Request { 86 | path: "/sapi/v1/margin/isolated/pair".to_owned(), 87 | credentials: Some(credentials), 88 | method: Method::Get, 89 | params: vec![ 90 | ("symbol".to_owned(), "BNBUSDT".to_string()), 91 | ("recvWindow".to_owned(), "5000".to_string()), 92 | ], 93 | sign: true 94 | } 95 | ); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /deps/binan_spot/src/margin/margin_account.rs: -------------------------------------------------------------------------------- 1 | use crate::http::{ 2 | request::Request, 3 | Credentials, 4 | Method, 5 | }; 6 | 7 | /// `GET /sapi/v1/margin/account` 8 | /// 9 | /// Weight(IP): 10 10 | /// 11 | /// # Example 12 | /// 13 | /// ``` 14 | /// use binance_spot_connector_rust::margin; 15 | /// 16 | /// let request = margin::margin_account(); 17 | /// ``` 18 | pub struct MarginAccount { 19 | recv_window: Option, 20 | credentials: Option, 21 | } 22 | 23 | impl MarginAccount { 24 | pub fn new() -> Self { 25 | Self { 26 | recv_window: None, 27 | credentials: None, 28 | } 29 | } 30 | 31 | pub fn recv_window(mut self, recv_window: u64) -> Self { 32 | self.recv_window = Some(recv_window); 33 | self 34 | } 35 | 36 | pub fn credentials(mut self, credentials: &Credentials) -> Self { 37 | self.credentials = Some(credentials.clone()); 38 | self 39 | } 40 | } 41 | 42 | impl From for Request { 43 | fn from(request: MarginAccount) -> Request { 44 | let mut params = vec![]; 45 | 46 | if let Some(recv_window) = request.recv_window { 47 | params.push(("recvWindow".to_owned(), recv_window.to_string())); 48 | } 49 | 50 | Request { 51 | path: "/sapi/v1/margin/account".to_owned(), 52 | method: Method::Get, 53 | params, 54 | credentials: request.credentials, 55 | sign: true, 56 | } 57 | } 58 | } 59 | 60 | impl Default for MarginAccount { 61 | fn default() -> Self { 62 | Self::new() 63 | } 64 | } 65 | 66 | #[cfg(test)] 67 | mod tests { 68 | use super::MarginAccount; 69 | use crate::http::{ 70 | request::Request, 71 | Credentials, 72 | Method, 73 | }; 74 | 75 | static API_KEY: &str = "api-key"; 76 | static API_SECRET: &str = "api-secret"; 77 | 78 | #[test] 79 | fn margin_margin_account_convert_to_request_test() { 80 | let credentials = Credentials::from_hmac(API_KEY.to_owned(), API_SECRET.to_owned()); 81 | 82 | let request: Request = MarginAccount::new() 83 | .recv_window(5000) 84 | .credentials(&credentials) 85 | .into(); 86 | 87 | assert_eq!( 88 | request, 89 | Request { 90 | path: "/sapi/v1/margin/account".to_owned(), 91 | credentials: Some(credentials), 92 | method: Method::Get, 93 | params: vec![("recvWindow".to_owned(), "5000".to_string()),], 94 | sign: true 95 | } 96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /deps/binan_spot/src/margin/margin_all_assets.rs: -------------------------------------------------------------------------------- 1 | use crate::http::{ 2 | request::Request, 3 | Credentials, 4 | Method, 5 | }; 6 | 7 | /// `GET /sapi/v1/margin/allAssets` 8 | /// 9 | /// Weight(IP): 1 10 | /// 11 | /// # Example 12 | /// 13 | /// ``` 14 | /// use binance_spot_connector_rust::margin; 15 | /// 16 | /// let request = margin::margin_all_assets(); 17 | /// ``` 18 | pub struct MarginAllAssets { 19 | credentials: Option, 20 | } 21 | 22 | impl MarginAllAssets { 23 | pub fn new() -> Self { 24 | Self { credentials: None } 25 | } 26 | 27 | pub fn credentials(mut self, credentials: &Credentials) -> Self { 28 | self.credentials = Some(credentials.clone()); 29 | self 30 | } 31 | } 32 | 33 | impl From for Request { 34 | fn from(_request: MarginAllAssets) -> Request { 35 | let params = vec![]; 36 | 37 | Request { 38 | path: "/sapi/v1/margin/allAssets".to_owned(), 39 | method: Method::Get, 40 | params, 41 | credentials: _request.credentials, 42 | sign: false, 43 | } 44 | } 45 | } 46 | 47 | impl Default for MarginAllAssets { 48 | fn default() -> Self { 49 | Self::new() 50 | } 51 | } 52 | 53 | #[cfg(test)] 54 | mod tests { 55 | use super::MarginAllAssets; 56 | use crate::http::{ 57 | request::Request, 58 | Credentials, 59 | Method, 60 | }; 61 | 62 | static API_KEY: &str = "api-key"; 63 | static API_SECRET: &str = "api-secret"; 64 | 65 | #[test] 66 | fn margin_margin_all_assets_convert_to_request_test() { 67 | let credentials = Credentials::from_hmac(API_KEY.to_owned(), API_SECRET.to_owned()); 68 | 69 | let request: Request = MarginAllAssets::new().credentials(&credentials).into(); 70 | 71 | assert_eq!( 72 | request, 73 | Request { 74 | path: "/sapi/v1/margin/allAssets".to_owned(), 75 | credentials: Some(credentials), 76 | method: Method::Get, 77 | params: vec![], 78 | sign: false 79 | } 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /deps/binan_spot/src/margin/margin_all_pairs.rs: -------------------------------------------------------------------------------- 1 | use crate::http::{ 2 | request::Request, 3 | Credentials, 4 | Method, 5 | }; 6 | 7 | /// `GET /sapi/v1/margin/allPairs` 8 | /// 9 | /// Weight(IP): 1 10 | /// 11 | /// # Example 12 | /// 13 | /// ``` 14 | /// use binance_spot_connector_rust::margin; 15 | /// 16 | /// let request = margin::margin_all_pairs(); 17 | /// ``` 18 | pub struct MarginAllPairs { 19 | credentials: Option, 20 | } 21 | 22 | impl MarginAllPairs { 23 | pub fn new() -> Self { 24 | Self { credentials: None } 25 | } 26 | 27 | pub fn credentials(mut self, credentials: &Credentials) -> Self { 28 | self.credentials = Some(credentials.clone()); 29 | self 30 | } 31 | } 32 | 33 | impl From for Request { 34 | fn from(_request: MarginAllPairs) -> Request { 35 | let params = vec![]; 36 | 37 | Request { 38 | path: "/sapi/v1/margin/allPairs".to_owned(), 39 | method: Method::Get, 40 | params, 41 | credentials: _request.credentials, 42 | sign: false, 43 | } 44 | } 45 | } 46 | 47 | impl Default for MarginAllPairs { 48 | fn default() -> Self { 49 | Self::new() 50 | } 51 | } 52 | 53 | #[cfg(test)] 54 | mod tests { 55 | use super::MarginAllPairs; 56 | use crate::http::{ 57 | request::Request, 58 | Credentials, 59 | Method, 60 | }; 61 | 62 | static API_KEY: &str = "api-key"; 63 | static API_SECRET: &str = "api-secret"; 64 | 65 | #[test] 66 | fn margin_margin_all_pairs_convert_to_request_test() { 67 | let credentials = Credentials::from_hmac(API_KEY.to_owned(), API_SECRET.to_owned()); 68 | 69 | let request: Request = MarginAllPairs::new().credentials(&credentials).into(); 70 | 71 | assert_eq!( 72 | request, 73 | Request { 74 | path: "/sapi/v1/margin/allPairs".to_owned(), 75 | credentials: Some(credentials), 76 | method: Method::Get, 77 | params: vec![], 78 | sign: false 79 | } 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /deps/binan_spot/src/margin/margin_asset.rs: -------------------------------------------------------------------------------- 1 | use crate::http::{ 2 | request::Request, 3 | Credentials, 4 | Method, 5 | }; 6 | 7 | /// `GET /sapi/v1/margin/asset` 8 | /// 9 | /// Weight(IP): 10 10 | /// 11 | /// # Example 12 | /// 13 | /// ``` 14 | /// use binance_spot_connector_rust::margin; 15 | /// 16 | /// let request = margin::margin_asset("BTC"); 17 | /// ``` 18 | pub struct MarginAsset { 19 | asset: String, 20 | credentials: Option, 21 | } 22 | 23 | impl MarginAsset { 24 | pub fn new(asset: &str) -> Self { 25 | Self { 26 | asset: asset.to_owned(), 27 | credentials: None, 28 | } 29 | } 30 | 31 | pub fn credentials(mut self, credentials: &Credentials) -> Self { 32 | self.credentials = Some(credentials.clone()); 33 | self 34 | } 35 | } 36 | 37 | impl From for Request { 38 | fn from(request: MarginAsset) -> Request { 39 | let params = vec![("asset".to_owned(), request.asset.to_string())]; 40 | 41 | Request { 42 | path: "/sapi/v1/margin/asset".to_owned(), 43 | method: Method::Get, 44 | params, 45 | credentials: request.credentials, 46 | sign: false, 47 | } 48 | } 49 | } 50 | 51 | #[cfg(test)] 52 | mod tests { 53 | use super::MarginAsset; 54 | use crate::http::{ 55 | request::Request, 56 | Credentials, 57 | Method, 58 | }; 59 | 60 | static API_KEY: &str = "api-key"; 61 | static API_SECRET: &str = "api-secret"; 62 | 63 | #[test] 64 | fn margin_margin_asset_convert_to_request_test() { 65 | let credentials = Credentials::from_hmac(API_KEY.to_owned(), API_SECRET.to_owned()); 66 | 67 | let request: Request = MarginAsset::new("BTC").credentials(&credentials).into(); 68 | 69 | assert_eq!( 70 | request, 71 | Request { 72 | path: "/sapi/v1/margin/asset".to_owned(), 73 | credentials: Some(credentials), 74 | method: Method::Get, 75 | params: vec![("asset".to_owned(), "BTC".to_string()),], 76 | sign: false 77 | } 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /deps/binan_spot/src/margin/margin_pair.rs: -------------------------------------------------------------------------------- 1 | use crate::http::{ 2 | request::Request, 3 | Credentials, 4 | Method, 5 | }; 6 | 7 | /// `GET /sapi/v1/margin/pair` 8 | /// 9 | /// Weight(IP): 10 10 | /// 11 | /// # Example 12 | /// 13 | /// ``` 14 | /// use binance_spot_connector_rust::margin; 15 | /// 16 | /// let request = margin::margin_pair("BNBUSDT"); 17 | /// ``` 18 | pub struct MarginPair { 19 | symbol: String, 20 | credentials: Option, 21 | } 22 | 23 | impl MarginPair { 24 | pub fn new(symbol: &str) -> Self { 25 | Self { 26 | symbol: symbol.to_owned(), 27 | credentials: None, 28 | } 29 | } 30 | 31 | pub fn credentials(mut self, credentials: &Credentials) -> Self { 32 | self.credentials = Some(credentials.clone()); 33 | self 34 | } 35 | } 36 | 37 | impl From for Request { 38 | fn from(request: MarginPair) -> Request { 39 | let params = vec![("symbol".to_owned(), request.symbol.to_string())]; 40 | 41 | Request { 42 | path: "/sapi/v1/margin/pair".to_owned(), 43 | method: Method::Get, 44 | params, 45 | credentials: request.credentials, 46 | sign: false, 47 | } 48 | } 49 | } 50 | 51 | #[cfg(test)] 52 | mod tests { 53 | use super::MarginPair; 54 | use crate::http::{ 55 | request::Request, 56 | Credentials, 57 | Method, 58 | }; 59 | 60 | static API_KEY: &str = "api-key"; 61 | static API_SECRET: &str = "api-secret"; 62 | 63 | #[test] 64 | fn margin_margin_pair_convert_to_request_test() { 65 | let credentials = Credentials::from_hmac(API_KEY.to_owned(), API_SECRET.to_owned()); 66 | 67 | let request: Request = MarginPair::new("BNBUSDT").credentials(&credentials).into(); 68 | 69 | assert_eq!( 70 | request, 71 | Request { 72 | path: "/sapi/v1/margin/pair".to_owned(), 73 | credentials: Some(credentials), 74 | method: Method::Get, 75 | params: vec![("symbol".to_owned(), "BNBUSDT".to_string()),], 76 | sign: false 77 | } 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /deps/binan_spot/src/margin/margin_price_index.rs: -------------------------------------------------------------------------------- 1 | use crate::http::{ 2 | request::Request, 3 | Credentials, 4 | Method, 5 | }; 6 | 7 | /// `GET /sapi/v1/margin/priceIndex` 8 | /// 9 | /// Weight(IP): 10 10 | /// 11 | /// # Example 12 | /// 13 | /// ``` 14 | /// use binance_spot_connector_rust::margin; 15 | /// 16 | /// let request = margin::margin_price_index("BNBUSDT"); 17 | /// ``` 18 | pub struct MarginPriceIndex { 19 | symbol: String, 20 | credentials: Option, 21 | } 22 | 23 | impl MarginPriceIndex { 24 | pub fn new(symbol: &str) -> Self { 25 | Self { 26 | symbol: symbol.to_owned(), 27 | credentials: None, 28 | } 29 | } 30 | 31 | pub fn credentials(mut self, credentials: &Credentials) -> Self { 32 | self.credentials = Some(credentials.clone()); 33 | self 34 | } 35 | } 36 | 37 | impl From for Request { 38 | fn from(request: MarginPriceIndex) -> Request { 39 | let params = vec![("symbol".to_owned(), request.symbol.to_string())]; 40 | 41 | Request { 42 | path: "/sapi/v1/margin/priceIndex".to_owned(), 43 | method: Method::Get, 44 | params, 45 | credentials: request.credentials, 46 | sign: false, 47 | } 48 | } 49 | } 50 | 51 | #[cfg(test)] 52 | mod tests { 53 | use super::MarginPriceIndex; 54 | use crate::http::{ 55 | request::Request, 56 | Credentials, 57 | Method, 58 | }; 59 | 60 | static API_KEY: &str = "api-key"; 61 | static API_SECRET: &str = "api-secret"; 62 | 63 | #[test] 64 | fn margin_margin_price_index_convert_to_request_test() { 65 | let credentials = Credentials::from_hmac(API_KEY.to_owned(), API_SECRET.to_owned()); 66 | 67 | let request: Request = MarginPriceIndex::new("BNBUSDT") 68 | .credentials(&credentials) 69 | .into(); 70 | 71 | assert_eq!( 72 | request, 73 | Request { 74 | path: "/sapi/v1/margin/priceIndex".to_owned(), 75 | credentials: Some(credentials), 76 | method: Method::Get, 77 | params: vec![("symbol".to_owned(), "BNBUSDT".to_string()),], 78 | sign: false 79 | } 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /deps/binan_spot/src/margin_stream/close_listen_key.rs: -------------------------------------------------------------------------------- 1 | use crate::http::{ 2 | request::Request, 3 | Credentials, 4 | Method, 5 | }; 6 | 7 | /// `DELETE /sapi/v1/userDataStream` 8 | /// 9 | /// Close out a user data stream. 10 | /// 11 | /// Weight: 1 12 | /// 13 | /// # Example 14 | /// 15 | /// ``` 16 | /// use binance_spot_connector_rust::margin_stream; 17 | /// 18 | /// let request = margin_stream::close_listen_key("listen-key"); 19 | /// ``` 20 | pub struct CloseListenKey { 21 | listen_key: String, 22 | credentials: Option, 23 | } 24 | 25 | impl CloseListenKey { 26 | pub fn new(listen_key: &str) -> Self { 27 | Self { 28 | listen_key: listen_key.to_owned(), 29 | credentials: None, 30 | } 31 | } 32 | 33 | pub fn credentials(mut self, credentials: &Credentials) -> Self { 34 | self.credentials = Some(credentials.clone()); 35 | self 36 | } 37 | } 38 | 39 | impl From for Request { 40 | fn from(request: CloseListenKey) -> Request { 41 | let params = vec![("listenKey".to_owned(), request.listen_key.to_string())]; 42 | 43 | Request { 44 | path: "/sapi/v1/userDataStream".to_owned(), 45 | method: Method::Delete, 46 | params, 47 | credentials: request.credentials, 48 | sign: false, 49 | } 50 | } 51 | } 52 | 53 | #[cfg(test)] 54 | mod tests { 55 | use super::CloseListenKey; 56 | use crate::http::{ 57 | request::Request, 58 | Credentials, 59 | Method, 60 | }; 61 | 62 | static API_KEY: &str = "api-key"; 63 | static API_SECRET: &str = "api-secret"; 64 | 65 | #[test] 66 | fn margin_stream_close_listen_key_convert_to_request_test() { 67 | let credentials = Credentials::from_hmac(API_KEY.to_owned(), API_SECRET.to_owned()); 68 | 69 | let request: Request = CloseListenKey::new("listen-key") 70 | .credentials(&credentials) 71 | .into(); 72 | 73 | assert_eq!( 74 | request, 75 | Request { 76 | path: "/sapi/v1/userDataStream".to_owned(), 77 | credentials: Some(credentials), 78 | method: Method::Delete, 79 | params: vec![("listenKey".to_owned(), "listen-key".to_string()),], 80 | sign: false 81 | } 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /deps/binan_spot/src/margin_stream/mod.rs: -------------------------------------------------------------------------------- 1 | //! Market Data 2 | 3 | pub mod close_listen_key; 4 | pub mod new_listen_key; 5 | pub mod renew_listen_key; 6 | 7 | use close_listen_key::CloseListenKey; 8 | use new_listen_key::NewListenKey; 9 | use renew_listen_key::RenewListenKey; 10 | 11 | pub fn new_listen_key() -> NewListenKey { 12 | NewListenKey::new() 13 | } 14 | 15 | pub fn renew_listen_key(listen_key: &str) -> RenewListenKey { 16 | RenewListenKey::new(listen_key) 17 | } 18 | 19 | pub fn close_listen_key(listen_key: &str) -> CloseListenKey { 20 | CloseListenKey::new(listen_key) 21 | } 22 | -------------------------------------------------------------------------------- /deps/binan_spot/src/margin_stream/new_listen_key.rs: -------------------------------------------------------------------------------- 1 | use crate::http::{ 2 | request::Request, 3 | Credentials, 4 | Method, 5 | }; 6 | 7 | /// `POST /sapi/v1/userDataStream` 8 | /// 9 | /// Start a new user data stream. 10 | /// The stream will close after 60 minutes unless a keepalive is sent. If the account has an active `listenKey`, that `listenKey` will be returned and its validity will be extended for 60 minutes. 11 | /// 12 | /// Weight: 1 13 | /// 14 | /// # Example 15 | /// 16 | /// ``` 17 | /// use binance_spot_connector_rust::margin_stream; 18 | /// 19 | /// let request = margin_stream::new_listen_key(); 20 | /// ``` 21 | pub struct NewListenKey { 22 | credentials: Option, 23 | } 24 | 25 | impl NewListenKey { 26 | pub fn new() -> Self { 27 | Self { credentials: None } 28 | } 29 | 30 | pub fn credentials(mut self, credentials: &Credentials) -> Self { 31 | self.credentials = Some(credentials.clone()); 32 | self 33 | } 34 | } 35 | 36 | impl From for Request { 37 | fn from(_request: NewListenKey) -> Request { 38 | let params = vec![]; 39 | 40 | Request { 41 | path: "/sapi/v1/userDataStream".to_owned(), 42 | method: Method::Post, 43 | params, 44 | credentials: _request.credentials, 45 | sign: false, 46 | } 47 | } 48 | } 49 | 50 | impl Default for NewListenKey { 51 | fn default() -> Self { 52 | Self::new() 53 | } 54 | } 55 | 56 | #[cfg(test)] 57 | mod tests { 58 | use super::NewListenKey; 59 | use crate::http::{ 60 | request::Request, 61 | Credentials, 62 | Method, 63 | }; 64 | 65 | static API_KEY: &str = "api-key"; 66 | static API_SECRET: &str = "api-secret"; 67 | 68 | #[test] 69 | fn margin_stream_new_listen_key_convert_to_request_test() { 70 | let credentials = Credentials::from_hmac(API_KEY.to_owned(), API_SECRET.to_owned()); 71 | 72 | let request: Request = NewListenKey::new().credentials(&credentials).into(); 73 | 74 | assert_eq!( 75 | request, 76 | Request { 77 | path: "/sapi/v1/userDataStream".to_owned(), 78 | credentials: Some(credentials), 79 | method: Method::Post, 80 | params: vec![], 81 | sign: false 82 | } 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /deps/binan_spot/src/margin_stream/renew_listen_key.rs: -------------------------------------------------------------------------------- 1 | use crate::http::{ 2 | request::Request, 3 | Credentials, 4 | Method, 5 | }; 6 | 7 | /// `PUT /sapi/v1/userDataStream` 8 | /// 9 | /// Keepalive a user data stream to prevent a time out. User data streams will close after 60 minutes. It's recommended to send a ping about every 30 minutes. 10 | /// 11 | /// Weight: 1 12 | /// 13 | /// # Example 14 | /// 15 | /// ``` 16 | /// use binance_spot_connector_rust::margin_stream; 17 | /// 18 | /// let request = margin_stream::renew_listen_key("listen-key"); 19 | /// ``` 20 | pub struct RenewListenKey { 21 | listen_key: String, 22 | credentials: Option, 23 | } 24 | 25 | impl RenewListenKey { 26 | pub fn new(listen_key: &str) -> Self { 27 | Self { 28 | listen_key: listen_key.to_owned(), 29 | credentials: None, 30 | } 31 | } 32 | 33 | pub fn credentials(mut self, credentials: &Credentials) -> Self { 34 | self.credentials = Some(credentials.clone()); 35 | self 36 | } 37 | } 38 | 39 | impl From for Request { 40 | fn from(request: RenewListenKey) -> Request { 41 | let params = vec![("listenKey".to_owned(), request.listen_key.to_string())]; 42 | 43 | Request { 44 | path: "/sapi/v1/userDataStream".to_owned(), 45 | method: Method::Put, 46 | params, 47 | credentials: request.credentials, 48 | sign: false, 49 | } 50 | } 51 | } 52 | 53 | #[cfg(test)] 54 | mod tests { 55 | use super::RenewListenKey; 56 | use crate::http::{ 57 | request::Request, 58 | Credentials, 59 | Method, 60 | }; 61 | 62 | static API_KEY: &str = "api-key"; 63 | static API_SECRET: &str = "api-secret"; 64 | 65 | #[test] 66 | fn margin_stream_renew_listen_key_convert_to_request_test() { 67 | let credentials = Credentials::from_hmac(API_KEY.to_owned(), API_SECRET.to_owned()); 68 | 69 | let request: Request = RenewListenKey::new("listen-key") 70 | .credentials(&credentials) 71 | .into(); 72 | 73 | assert_eq!( 74 | request, 75 | Request { 76 | path: "/sapi/v1/userDataStream".to_owned(), 77 | credentials: Some(credentials), 78 | method: Method::Put, 79 | params: vec![("listenKey".to_owned(), "listen-key".to_string()),], 80 | sign: false 81 | } 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /deps/binan_spot/src/market/avg_price.rs: -------------------------------------------------------------------------------- 1 | use crate::http::{ 2 | request::Request, 3 | Method, 4 | }; 5 | 6 | /// `GET /api/v3/avgPrice` 7 | /// 8 | /// Current average price for a symbol. 9 | /// 10 | /// Weight(IP): 1 11 | /// 12 | /// # Example 13 | /// 14 | /// ``` 15 | /// use binance_spot_connector_rust::market; 16 | /// 17 | /// let request = market::avg_price("BNBUSDT"); 18 | /// ``` 19 | pub struct AvgPrice { 20 | symbol: String, 21 | } 22 | 23 | impl AvgPrice { 24 | pub fn new(symbol: &str) -> Self { 25 | Self { 26 | symbol: symbol.to_owned(), 27 | } 28 | } 29 | } 30 | 31 | impl From for Request { 32 | fn from(request: AvgPrice) -> Request { 33 | let params = vec![("symbol".to_owned(), request.symbol)]; 34 | 35 | Request { 36 | path: "/api/v3/avgPrice".to_owned(), 37 | method: Method::Get, 38 | params, 39 | credentials: None, 40 | sign: false, 41 | } 42 | } 43 | } 44 | 45 | #[cfg(test)] 46 | mod tests { 47 | use super::AvgPrice; 48 | use crate::http::{ 49 | request::Request, 50 | Method, 51 | }; 52 | 53 | #[test] 54 | fn market_avg_price_convert_to_request_test() { 55 | let request: Request = AvgPrice::new("BNBUSDT").into(); 56 | 57 | assert_eq!( 58 | request, 59 | Request { 60 | path: "/api/v3/avgPrice".to_owned(), 61 | credentials: None, 62 | method: Method::Get, 63 | params: vec![("symbol".to_owned(), "BNBUSDT".to_string()),], 64 | sign: false 65 | } 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /deps/binan_spot/src/market/depth.rs: -------------------------------------------------------------------------------- 1 | use crate::http::{ 2 | request::Request, 3 | Method, 4 | }; 5 | 6 | /// `GET /api/v3/depth` 7 | /// 8 | /// | Limit | Weight(IP) | 9 | /// |---------------------|-------------| 10 | /// | 1-100 | 1 | 11 | /// | 101-500 | 5 | 12 | /// | 501-1000 | 10 | 13 | /// | 1001-5000 | 50 | 14 | /// 15 | /// # Example 16 | /// 17 | /// ``` 18 | /// use binance_spot_connector_rust::market; 19 | /// 20 | /// let request = market::depth("BNBUSDT").limit(100); 21 | /// ``` 22 | pub struct Depth { 23 | symbol: String, 24 | limit: Option, 25 | } 26 | 27 | impl Depth { 28 | pub fn new(symbol: &str) -> Self { 29 | Self { 30 | symbol: symbol.to_owned(), 31 | limit: None, 32 | } 33 | } 34 | 35 | pub fn limit(mut self, limit: u32) -> Self { 36 | self.limit = Some(limit); 37 | self 38 | } 39 | } 40 | 41 | impl From for Request { 42 | fn from(request: Depth) -> Request { 43 | let mut params = vec![("symbol".to_owned(), request.symbol.to_string())]; 44 | 45 | if let Some(limit) = request.limit { 46 | params.push(("limit".to_owned(), limit.to_string())); 47 | } 48 | 49 | Request { 50 | path: "/api/v3/depth".to_owned(), 51 | method: Method::Get, 52 | params, 53 | credentials: None, 54 | sign: false, 55 | } 56 | } 57 | } 58 | 59 | #[cfg(test)] 60 | mod tests { 61 | use super::Depth; 62 | use crate::http::{ 63 | request::Request, 64 | Method, 65 | }; 66 | 67 | #[test] 68 | fn market_depth_convert_to_request_test() { 69 | let request: Request = Depth::new("BNBUSDT").limit(100).into(); 70 | 71 | assert_eq!( 72 | request, 73 | Request { 74 | path: "/api/v3/depth".to_owned(), 75 | credentials: None, 76 | method: Method::Get, 77 | params: vec![ 78 | ("symbol".to_owned(), "BNBUSDT".to_string()), 79 | ("limit".to_owned(), "100".to_string()), 80 | ], 81 | sign: false 82 | } 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /deps/binan_spot/src/market/exchange_info.rs: -------------------------------------------------------------------------------- 1 | use crate::http::{ 2 | request::Request, 3 | Method, 4 | }; 5 | 6 | /// `GET /api/v3/exchangeInfo` 7 | /// 8 | /// Current exchange trading rules and symbol information 9 | /// 10 | /// * If any symbol provided in either symbol or symbols do not exist, the endpoint will throw an error. 11 | /// 12 | /// Weight(IP): 10 13 | /// 14 | /// # Example 15 | /// 16 | /// ``` 17 | /// use binance_spot_connector_rust::market; 18 | /// 19 | /// let request = market::exchange_info().symbol("BNBUSDT").symbols(vec!["BTCUSDT","BNBBTC"]); 20 | /// ``` 21 | pub struct ExchangeInfo { 22 | symbol: Option, 23 | symbols: Option>, 24 | } 25 | 26 | impl ExchangeInfo { 27 | pub fn new() -> Self { 28 | Self { 29 | symbol: None, 30 | symbols: None, 31 | } 32 | } 33 | 34 | pub fn symbol(mut self, symbol: &str) -> Self { 35 | self.symbol = Some(symbol.to_owned()); 36 | self 37 | } 38 | 39 | pub fn symbols(mut self, symbols: Vec<&str>) -> Self { 40 | self.symbols = Some(symbols.iter().map(|s| s.to_string()).collect()); 41 | self 42 | } 43 | } 44 | 45 | impl From for Request { 46 | fn from(request: ExchangeInfo) -> Request { 47 | let mut params = vec![]; 48 | 49 | if let Some(symbol) = request.symbol { 50 | params.push(("symbol".to_owned(), symbol)); 51 | } 52 | 53 | if let Some(symbols) = request.symbols { 54 | params.push(( 55 | "symbols".to_owned(), 56 | format!("[\"{}\"]", symbols.join("\",\"")), 57 | )); 58 | } 59 | 60 | Request { 61 | path: "/api/v3/exchangeInfo".to_owned(), 62 | method: Method::Get, 63 | params, 64 | credentials: None, 65 | sign: false, 66 | } 67 | } 68 | } 69 | 70 | impl Default for ExchangeInfo { 71 | fn default() -> Self { 72 | Self::new() 73 | } 74 | } 75 | 76 | #[cfg(test)] 77 | mod tests { 78 | use super::ExchangeInfo; 79 | use crate::http::{ 80 | request::Request, 81 | Method, 82 | }; 83 | 84 | #[test] 85 | fn market_exchange_info_convert_to_request_test() { 86 | let request: Request = ExchangeInfo::new() 87 | .symbol("BNBUSDT") 88 | .symbols(vec!["BTCUSDT", "BNBBTC"]) 89 | .into(); 90 | 91 | assert_eq!( 92 | request, 93 | Request { 94 | path: "/api/v3/exchangeInfo".to_owned(), 95 | credentials: None, 96 | method: Method::Get, 97 | params: vec![ 98 | ("symbol".to_owned(), "BNBUSDT".to_string()), 99 | ("symbols".to_owned(), "[\"BTCUSDT\",\"BNBBTC\"]".to_string()), 100 | ], 101 | sign: false 102 | } 103 | ); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /deps/binan_spot/src/market/mod.rs: -------------------------------------------------------------------------------- 1 | //! Market Data 2 | 3 | pub mod agg_trades; 4 | pub mod avg_price; 5 | pub mod book_ticker; 6 | pub mod depth; 7 | pub mod exchange_info; 8 | pub mod historical_trades; 9 | pub mod klines; 10 | pub mod ping; 11 | pub mod rolling_window_price_change_statistics; 12 | pub mod ticker_price; 13 | pub mod ticker_twenty_four_hr; 14 | pub mod time; 15 | pub mod trades; 16 | 17 | use agg_trades::AggTrades; 18 | use avg_price::AvgPrice; 19 | use book_ticker::BookTicker; 20 | use depth::Depth; 21 | use exchange_info::ExchangeInfo; 22 | use historical_trades::HistoricalTrades; 23 | use klines::{ 24 | KlineInterval, 25 | Klines, 26 | }; 27 | use ping::Ping; 28 | use rolling_window_price_change_statistics::RollingWindowPriceChangeStatistics; 29 | use ticker_price::TickerPrice; 30 | use ticker_twenty_four_hr::Ticker24hr; 31 | use time::Time; 32 | use trades::Trades; 33 | 34 | pub fn ping() -> Ping { 35 | Ping::new() 36 | } 37 | 38 | pub fn time() -> Time { 39 | Time::new() 40 | } 41 | 42 | pub fn exchange_info() -> ExchangeInfo { 43 | ExchangeInfo::new() 44 | } 45 | 46 | pub fn depth(symbol: &str) -> Depth { 47 | Depth::new(symbol) 48 | } 49 | 50 | pub fn trades(symbol: &str) -> Trades { 51 | Trades::new(symbol) 52 | } 53 | 54 | pub fn historical_trades(symbol: &str) -> HistoricalTrades { 55 | HistoricalTrades::new(symbol) 56 | } 57 | 58 | pub fn agg_trades(symbol: &str) -> AggTrades { 59 | AggTrades::new(symbol) 60 | } 61 | 62 | pub fn klines(symbol: &str, interval: KlineInterval) -> Klines { 63 | Klines::new(symbol, interval) 64 | } 65 | 66 | pub fn avg_price(symbol: &str) -> AvgPrice { 67 | AvgPrice::new(symbol) 68 | } 69 | 70 | pub fn ticker_twenty_four_hr() -> Ticker24hr { 71 | Ticker24hr::new() 72 | } 73 | 74 | pub fn ticker_price() -> TickerPrice { 75 | TickerPrice::new() 76 | } 77 | 78 | pub fn book_ticker() -> BookTicker { 79 | BookTicker::new() 80 | } 81 | 82 | pub fn rolling_window_price_change_statistics() -> RollingWindowPriceChangeStatistics { 83 | RollingWindowPriceChangeStatistics::new() 84 | } 85 | -------------------------------------------------------------------------------- /deps/binan_spot/src/market/ping.rs: -------------------------------------------------------------------------------- 1 | use crate::http::{ 2 | request::Request, 3 | Method, 4 | }; 5 | 6 | /// `GET /api/v3/ping` 7 | /// 8 | /// Test connectivity to the Rest API. 9 | /// 10 | /// Weight(IP): 1 11 | /// 12 | /// # Example 13 | /// 14 | /// ``` 15 | /// use binance_spot_connector_rust::market; 16 | /// 17 | /// let request = market::ping(); 18 | /// ``` 19 | pub struct Ping {} 20 | 21 | impl Ping { 22 | pub fn new() -> Self { 23 | Self {} 24 | } 25 | } 26 | 27 | impl From for Request { 28 | fn from(_request: Ping) -> Request { 29 | let params = vec![]; 30 | 31 | Request { 32 | path: "/api/v3/ping".to_owned(), 33 | method: Method::Get, 34 | params, 35 | credentials: None, 36 | sign: false, 37 | } 38 | } 39 | } 40 | 41 | impl Default for Ping { 42 | fn default() -> Self { 43 | Self::new() 44 | } 45 | } 46 | 47 | #[cfg(test)] 48 | mod tests { 49 | use super::Ping; 50 | use crate::http::{ 51 | request::Request, 52 | Method, 53 | }; 54 | 55 | #[test] 56 | fn market_ping_convert_to_request_test() { 57 | let request: Request = Ping::new().into(); 58 | 59 | assert_eq!( 60 | request, 61 | Request { 62 | path: "/api/v3/ping".to_owned(), 63 | credentials: None, 64 | method: Method::Get, 65 | params: vec![], 66 | sign: false 67 | } 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /deps/binan_spot/src/market/time.rs: -------------------------------------------------------------------------------- 1 | use crate::http::{ 2 | request::Request, 3 | Method, 4 | }; 5 | 6 | /// `GET /api/v3/time` 7 | /// 8 | /// Test connectivity to the Rest API and get the current server time. 9 | /// 10 | /// Weight(IP): 1 11 | /// 12 | /// # Example 13 | /// 14 | /// ``` 15 | /// use binance_spot_connector_rust::market; 16 | /// 17 | /// let request = market::time(); 18 | /// ``` 19 | pub struct Time {} 20 | 21 | impl Time { 22 | pub fn new() -> Self { 23 | Self {} 24 | } 25 | } 26 | 27 | impl From