@ -0,0 +1,59 @@ |
||||
[run] |
||||
source_pkgs = hummingbot |
||||
omit = |
||||
hummingbot/core/gateway/* |
||||
hummingbot/core/management/* |
||||
hummingbot/client/config/config_helpers.py |
||||
hummingbot/client/config/conf_migration.py |
||||
hummingbot/client/config/security.py |
||||
hummingbot/client/hummingbot_application.py |
||||
hummingbot/client/command/* |
||||
hummingbot/client/settings.py |
||||
hummingbot/client/ui/completer.py |
||||
hummingbot/client/ui/layout.py |
||||
hummingbot/client/tab/* |
||||
hummingbot/client/ui/parser.py |
||||
hummingbot/connector/connector/balancer* |
||||
hummingbot/connector/connector/terra* |
||||
hummingbot/connector/connector/uniswap* |
||||
hummingbot/connector/connector/uniswap_v3* |
||||
hummingbot/connector/derivative/perpetual_finance* |
||||
hummingbot/connector/derivative/position.py |
||||
hummingbot/connector/exchange/bitfinex* |
||||
hummingbot/connector/exchange/coinbase_pro* |
||||
hummingbot/connector/exchange/hitbtc* |
||||
hummingbot/connector/exchange/paper_trade* |
||||
hummingbot/connector/gateway/** |
||||
hummingbot/connector/test_support/* |
||||
hummingbot/core/utils/gateway_config_utils.py |
||||
hummingbot/core/utils/kill_switch.py |
||||
hummingbot/core/utils/wallet_setup.py |
||||
hummingbot/connector/mock* |
||||
hummingbot/strategy/*/start.py |
||||
hummingbot/strategy/dev* |
||||
hummingbot/user/user_balances.py |
||||
hummingbot/smart_components/controllers/* |
||||
dynamic_context = test_function |
||||
branch = true |
||||
|
||||
[report] |
||||
fail_under = 70 |
||||
precision = 2 |
||||
skip_empty = true |
||||
exclude_lines = |
||||
@(abc\.)?abstractmethod |
||||
if TYPE_CHECKING: |
||||
pragma: no cover |
||||
if __name__ == .__main__.: |
||||
if 0: |
||||
raise AssertionError |
||||
raise NotImplementedError |
||||
if settings.DEBUG |
||||
except asyncio.exceptions.TimeoutError: |
||||
|
||||
[html] |
||||
directory = coverage_html_report |
||||
show_contexts = true |
||||
|
||||
[xml] |
||||
output = coverage.xml |
@ -0,0 +1,13 @@ |
||||
/data |
||||
/logs |
||||
/build |
||||
/dist |
||||
/.git |
||||
/.github |
||||
.DS_Store |
||||
/conf_backup |
||||
/conf/strategies/*.yml |
||||
/conf/*.yml |
||||
/conf/gateway_connections.json |
||||
/conf/.password_verification |
||||
/conf/connectors/*.yml |
@ -0,0 +1,9 @@ |
||||
[flake8] |
||||
filename = *.py, *.pyx, *.pxd |
||||
ignore = E251, E501,E702,W504,W503 |
||||
per-file-ignores = |
||||
hummingbot/**/*.pyx: E225, E226, E251, E999 |
||||
hummingbot/**/*.pxd: E225, E226, E251, E999 |
||||
test/**/*.pyx: E225, E226, E251, E999 |
||||
test/**/*.pxd: E225, E226, E251, E999 |
||||
max-line-length = 120 |
@ -0,0 +1,94 @@ |
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm |
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 |
||||
|
||||
# .idea |
||||
/.idea |
||||
|
||||
# CMake |
||||
cmake-build-debug/ |
||||
|
||||
## File-based project format: |
||||
*.iws |
||||
|
||||
## Plugin-specific files: |
||||
|
||||
# IntelliJ |
||||
/out/ |
||||
|
||||
# JIRA plugin |
||||
atlassian-ide-plugin.xml |
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ) |
||||
com_crashlytics_export_strings.xml |
||||
crashlytics.properties |
||||
crashlytics-build.properties |
||||
fabric.properties |
||||
|
||||
__pycache__ |
||||
.ipynb_checkpoints |
||||
*.pyc |
||||
|
||||
# Cython objects |
||||
/**/*.so |
||||
/**/*.pyd |
||||
/hummingbot/**/*.c |
||||
/hummingbot/**/*.cpp |
||||
!/hummingbot/core/cpp/*.cpp |
||||
/hummingbot/wallet/*.c |
||||
/hummingbot/wallet/*.cpp |
||||
/hummingbot/wallet/ethereum/*.c |
||||
/hummingbot/wallet/ethereum/*.cpp |
||||
/test/**/*.cpp |
||||
/build |
||||
|
||||
# Local files |
||||
**/.DS_Store |
||||
portfolio_value.csv |
||||
/notebooks |
||||
/data |
||||
debug.log |
||||
/installation/docker-commands/hummingbot_files |
||||
|
||||
# Distribution files |
||||
/build |
||||
/dist |
||||
/src |
||||
/*.egg-info |
||||
*.whl |
||||
|
||||
|
||||
# VSCode |
||||
.vscode/ |
||||
.history/ |
||||
|
||||
# emacs files |
||||
\#*\# |
||||
.\#* |
||||
|
||||
# Documentation build dir |
||||
/documentation/site/ |
||||
|
||||
# Cert files |
||||
/certs |
||||
*.pem |
||||
|
||||
# Coverage |
||||
.coverage |
||||
/cover/ |
||||
/coverage_html_report/ |
||||
coverage.xml |
||||
|
||||
# misc |
||||
*.srl |
||||
*.key |
||||
*.crt |
||||
*.log |
||||
|
||||
# Debug console |
||||
.debug_console_ssh_host_key |
||||
|
||||
/conf_backup |
||||
|
||||
# External SDK files |
||||
/**/.chain_cookie |
||||
/**/.injective_cookie |
@ -0,0 +1,26 @@ |
||||
repos: |
||||
- repo: https://github.com/pre-commit/pre-commit-hooks |
||||
rev: v2.3.0 |
||||
hooks: |
||||
- id: flake8 |
||||
types: ['file'] |
||||
files: \.(py|pyx|pxd)$ |
||||
- id: detect-private-key |
||||
- repo: https://github.com/pre-commit/mirrors-eslint |
||||
rev: v8.10.0 |
||||
hooks: |
||||
- id: eslint |
||||
files: \.[jt]sx?$ # *.js, *.jsx, *.ts and *.tsx |
||||
types: [file] |
||||
- repo: https://github.com/CoinAlpha/git-hooks |
||||
rev: 78f0683233a09c68a072fd52740d32c0376d4f0f |
||||
hooks: |
||||
- id: detect-wallet-private-key |
||||
types: [file] |
||||
exclude: .json |
||||
- repo: https://github.com/pycqa/isort |
||||
rev: 5.12.0 |
||||
hooks: |
||||
- id: isort |
||||
files: "\\.(py)$" |
||||
args: [--settings-path=pyproject.toml] |
@ -0,0 +1,76 @@ |
||||
# Contributor Covenant Code of Conduct |
||||
|
||||
## Our Pledge |
||||
|
||||
In the interest of fostering an open and welcoming environment, we as |
||||
contributors and maintainers pledge to making participation in our project and |
||||
our community a harassment-free experience for everyone, regardless of age, body |
||||
size, disability, ethnicity, sex characteristics, gender identity and expression, |
||||
level of experience, education, socio-economic status, nationality, personal |
||||
appearance, race, religion, or sexual identity and orientation. |
||||
|
||||
## Our Standards |
||||
|
||||
Examples of behavior that contributes to creating a positive environment |
||||
include: |
||||
|
||||
* Using welcoming and inclusive language |
||||
* Being respectful of differing viewpoints and experiences |
||||
* Gracefully accepting constructive criticism |
||||
* Focusing on what is best for the community |
||||
* Showing empathy towards other community members |
||||
|
||||
Examples of unacceptable behavior by participants include: |
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or |
||||
advances |
||||
* Trolling, insulting/derogatory comments, and personal or political attacks |
||||
* Public or private harassment |
||||
* Publishing others' private information, such as a physical or electronic |
||||
address, without explicit permission |
||||
* Other conduct which could reasonably be considered inappropriate in a |
||||
professional setting |
||||
|
||||
## Our Responsibilities |
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable |
||||
behavior and are expected to take appropriate and fair corrective action in |
||||
response to any instances of unacceptable behavior. |
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or |
||||
reject comments, commits, code, wiki edits, issues, and other contributions |
||||
that are not aligned to this Code of Conduct, or to ban temporarily or |
||||
permanently any contributor for other behaviors that they deem inappropriate, |
||||
threatening, offensive, or harmful. |
||||
|
||||
## Scope |
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces |
||||
when an individual is representing the project or its community. Examples of |
||||
representing a project or community include using an official project e-mail |
||||
address, posting via an official social media account, or acting as an appointed |
||||
representative at an online or offline event. Representation of a project may be |
||||
further defined and clarified by project maintainers. |
||||
|
||||
## Enforcement |
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be |
||||
reported by contacting the project team at operations@hummingbot.org. All |
||||
complaints will be reviewed and investigated and will result in a response that |
||||
is deemed necessary and appropriate to the circumstances. The project team is |
||||
obligated to maintain confidentiality with regard to the reporter of an incident. |
||||
Further details of specific enforcement policies may be posted separately. |
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good |
||||
faith may face temporary or permanent repercussions as determined by other |
||||
members of the project's leadership. |
||||
|
||||
## Attribution |
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, |
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html |
||||
|
||||
[homepage]: https://www.contributor-covenant.org |
||||
|
||||
For answers to common questions about this code of conduct, see |
||||
https://www.contributor-covenant.org/faq |
@ -0,0 +1,98 @@ |
||||
# Contribution Guidelines |
||||
|
||||
## General Workflow |
||||
|
||||
1. Fork the `hummingbot/hummingbot` repository. |
||||
2. Create a new branch from the `development` branch in your fork. |
||||
3. Commit your changes to your branch. |
||||
4. Once you've completed your fix, feature, connector, or documentation: |
||||
|
||||
- Rebase upstream changes into your branch. |
||||
- Create a pull request to the `development` branch. |
||||
- Include a detailed description of your changes. |
||||
- Ensure to `allow edits by maintainers` before submitting the pull request. |
||||
|
||||
5. Your code changes will be reviewed and tested by the Foundation QA team. |
||||
6. Address any changes requested by your reviewer, fix issues raised, and push your fixes as a single new commit. |
||||
7. Once the pull request has been reviewed and accepted, it will be merged by a member of the Hummingbot Foundation team. |
||||
|
||||
**Note:** Tests are crucial. If your pull request contains new, testable behavior, please submit tests. Refer to the 'Unit Test Coverage' section for more information. |
||||
|
||||
## Detailed Workflow |
||||
|
||||
### 1. Fork the Repository |
||||
|
||||
Use GitHub's interface to fork the repo, add the Hummingbot repo as an upstream remote, and fetch upstream data: |
||||
|
||||
```bash |
||||
git remote add upstream https://github.com/hummingbot/hummingbot.git |
||||
git fetch upstream |
||||
``` |
||||
|
||||
### 2. Create Your Branch |
||||
|
||||
Create your local branch following this naming convention: |
||||
|
||||
- feat/... |
||||
- fix/... |
||||
- refactor/... |
||||
- doc/... |
||||
|
||||
Create and switch to a new local branch called feat/[branch_name] based on the development branch of the remote upstream: |
||||
|
||||
```bash |
||||
git checkout -b feat/[branch_name] upstream/development |
||||
``` |
||||
|
||||
### 3. Commit Changes to Your Branch |
||||
|
||||
Make commits to your branch. Prefix each commit like so: |
||||
|
||||
- (feat) add a new feature |
||||
- (fix) fix inconsistent tests |
||||
- (refactor) ... |
||||
- (cleanup) ... |
||||
- (doc) ... |
||||
|
||||
Commit messages should be written in the present tense, e.g., "(feat) add unit tests". The first line of your commit message should be a summary of what the commit changes, aiming for about 70 characters max. If you want to explain the commit in more depth, provide a more detailed description after a blank line following the first line. |
||||
|
||||
### 4. Rebase Upstream Changes |
||||
|
||||
Rebase upstream changes to the development branch into yours by running this command from your branch: |
||||
|
||||
```bash |
||||
git pull --rebase upstream development |
||||
``` |
||||
|
||||
If there are conflicting changes, git will pause rebasing to allow you to sort out the conflicts. Once you are done fixing conflicts for a specific commit, run: |
||||
|
||||
```bash |
||||
git rebase --continue |
||||
``` |
||||
|
||||
Ensure all tests pass after rebasing. |
||||
|
||||
### 5. Create a Pull Request |
||||
|
||||
Create a clear pull request from your fork and branch to the upstream `development` branch, detailing your changes. Check 'Allow edits by maintainers' for the Foundation team to update your branch with development whenever needed. |
||||
|
||||
If the Foundation team requests changes, make more commits to your branch to address these, then follow this process again from rebasing onwards. Once you've addressed the requests, request further review. |
||||
|
||||
## Unit Test Coverage |
||||
|
||||
A minimum of 75% unit test coverage is required for all changes included in a pull request. However, some components, like UI components, are excluded from this validation. |
||||
|
||||
To calculate the diff-coverage locally on your computer, run `make development-diff-cover` after running all tests. |
||||
|
||||
## Checklist |
||||
|
||||
- Did I create my branch from `development` (don't create new branches from existing feature branches)? |
||||
- Did I follow the correct naming convention for my branch? |
||||
- Is my branch focused on a single main change? |
||||
- Do all of my changes directly relate to this change? |
||||
- Did I rebase the upstream development branch after I finished all my work? |
||||
- Did I write a clear pull request message detailing what changes I made? |
||||
- Did I get a code review? |
||||
- Did I make any requested changes from that code review? |
||||
|
||||
If you adhere to these guidelines and make quality changes, you should have no problems getting your contributions accepted. Thank you for contributing! |
@ -0,0 +1,33 @@ |
||||
# Important Disclosure re: Hummingbot Data Collection |
||||
|
||||
Hummingbot (“Hummingbot”) is open-source software developed and maintained by Hummingbot Foundation ("The Foundation, “we”, or “us”) and available at https://github.com/hummingbot/hummingbot. |
||||
|
||||
The Foundation collects certain usage information from users that have configured Hummingbot to allow for the sharing of this data. |
||||
|
||||
## Purpose of data collection |
||||
|
||||
### Software improvement |
||||
|
||||
Data collected by the Foundation is used to help improve Hummingbot for all users. It is important that our team understands the usage patterns and detects bugs as soon as possible, so we can best decide how to design future features and prioritize current work. |
||||
|
||||
### Support |
||||
|
||||
Users who provide data allow us to more easily diagnose problems, troubleshoot, and resolve any issues encountered when running Hummingbot. |
||||
|
||||
## Types of data collected |
||||
|
||||
Hummingbot aggregates and collects total traded volume per exchange denominated in USDT. The data collected is subsequently reported to the Foundation's backend services. |
||||
|
||||
All data collected will be used exclusively by the Foundation team for analytical and marketing purposes only. The data will be neither accessible nor sold to any third party. |
||||
|
||||
## Sensitive data |
||||
|
||||
Hummingbot will never collect and/or report sensitive information, such as private keys, API keys, or passwords. |
||||
|
||||
## How do I opt-in to or opt-out of data sharing? |
||||
|
||||
See https://docs.hummingbot.org/faq/#reporting for information on configuring this functionality. |
||||
|
||||
## How this data is used |
||||
|
||||
When enabled by a user, data is reported by Hummingbot to the Foundation. The Foundation uses its own, third-party systems, as well as commercial software (such as Datadog Inc.) to collect and process data. |
@ -0,0 +1,146 @@ |
||||
# Docker |
||||
|
||||
## Why use Docker Compose? |
||||
|
||||
Using Docker for Hummingbot deployment offers several benefits, such as simplifying the installation process, enabling easy versioning and scaling, and ensuring a consistent and isolated environment for running the bot. This repository aims to help users get started with deploying Hummingbot using Docker by providing different examples that demonstrate how to set up and customize the bot according to their needs. |
||||
|
||||
## Install Docker Compose |
||||
|
||||
The examples below use Docker Compose, a tool for defining and running multi-container Docker applications. You can install Docker Compose either via command line or by running an installer. |
||||
|
||||
Linux (Ubuntu / Debian): |
||||
|
||||
```bash |
||||
sudo apt-get update |
||||
sudo apt-get install docker-compose-plugin |
||||
``` |
||||
|
||||
Mac (Homebrew): |
||||
|
||||
```bash |
||||
brew install docker-compose |
||||
``` |
||||
|
||||
If you want to be guided through the installation, install [Docker Desktop](https://www.docker.com/products/docker-desktop/) includes Docker Compose along with Docker Engine and Docker CLI which are Compose prerequisites: |
||||
|
||||
* [Linux](https://docs.docker.com/desktop/install/linux-install/) |
||||
* [Mac](https://docs.docker.com/desktop/install/mac-install/) |
||||
* [Windows](https://docs.docker.com/desktop/install/windows-install/) |
||||
|
||||
|
||||
Verify that Docker Compose is installed correctly by checking the version: |
||||
|
||||
```bash |
||||
docker compose version |
||||
``` |
||||
|
||||
Hummingbot's [deploy-examples](https://github.com/hummingbot/deploy-examples) repository provides various examples of how to deploy Hummingbot using Docker Compose, a tool for defining and running multi-container Docker applications. |
||||
|
||||
Compiled images of `hummingbot` are available on our official DockerHub: https://hub.docker.com/r/hummingbot/hummingbot |
||||
|
||||
## Building a Docker Image |
||||
|
||||
You can also build and run a Docker-based Hummingbot image using the `docker-compose.yml` file in the root folder: |
||||
```yml |
||||
version: "3.9" |
||||
services: |
||||
hummingbot: |
||||
container_name: hummingbot |
||||
build: |
||||
context: . |
||||
dockerfile: Dockerfile |
||||
volumes: |
||||
- ./conf:/home/hummingbot/conf |
||||
- ./conf/connectors:/home/hummingbot/conf/connectors |
||||
- ./conf/strategies:/home/hummingbot/conf/strategies |
||||
- ./logs:/home/hummingbot/logs |
||||
- ./data:/home/hummingbot/data |
||||
- ./scripts:/home/hummingbot/scripts |
||||
environment: |
||||
# - CONFIG_PASSWORD=a |
||||
# - CONFIG_FILE_NAME=directional_strategy_rsi.py |
||||
logging: |
||||
driver: "json-file" |
||||
options: |
||||
max-size: "10m" |
||||
max-file: 5 |
||||
tty: true |
||||
stdin_open: true |
||||
network_mode: host |
||||
|
||||
dashboard: |
||||
container_name: dashboard |
||||
image: hummingbot/dashboard:latest |
||||
volumes: |
||||
- ./data:/home/dashboard/data |
||||
ports: |
||||
- "8501:8501" |
||||
``` |
||||
|
||||
Build and launch the image by running: |
||||
``` |
||||
docker compose up -d |
||||
``` |
||||
|
||||
Uncomment the following lines in the YML file before running the command above if you would like to: |
||||
* Bypass the password screen by entering the previously set password |
||||
* Auto-starting a script |
||||
``` |
||||
# environment: |
||||
# - CONFIG_PASSWORD=a |
||||
# - CONFIG_FILE_NAME=directional_strategy_rsi.py |
||||
``` |
||||
|
||||
## Useful Docker Commands |
||||
|
||||
Use the commands below or use the Docker Desktop application to manage your containers: |
||||
|
||||
### Create the Compose project |
||||
``` |
||||
docker compose up -d |
||||
``` |
||||
|
||||
### Stop the Compose project |
||||
``` |
||||
docker compose down |
||||
``` |
||||
|
||||
### Update the Compose project for the latest images |
||||
``` |
||||
docker compose up --force-recreate --build -d |
||||
``` |
||||
|
||||
### Give all users read/write permissions to local files |
||||
``` |
||||
sudo chmod -R a+rw <files/folders> |
||||
``` |
||||
|
||||
### Attach to the container |
||||
``` |
||||
docker attach <container-name> |
||||
``` |
||||
|
||||
### Detach from the container and return to command line |
||||
|
||||
Press keys <kbd>Ctrl</kbd> + <kbd>P</kbd> then <kbd>Ctrl</kbd> + <kbd>Q</kbd> |
||||
|
||||
|
||||
### Update the container to the latest image |
||||
``` |
||||
docker compose up --force-recreate --build -d |
||||
``` |
||||
|
||||
### List all containers |
||||
``` |
||||
docker ps -a |
||||
``` |
||||
|
||||
### Stop a container |
||||
``` |
||||
docker stop <container-name> |
||||
``` |
||||
|
||||
### Remove a container |
||||
``` |
||||
docker rm <container-name> |
||||
``` |
@ -0,0 +1,76 @@ |
||||
# Set the base image |
||||
FROM continuumio/miniconda3:latest AS builder |
||||
|
||||
# Install system dependencies |
||||
RUN apt-get update && \ |
||||
apt-get install -y sudo libusb-1.0 gcc g++ python3-dev && \ |
||||
rm -rf /var/lib/apt/lists/* |
||||
|
||||
WORKDIR /home/hummingbot |
||||
|
||||
# Create conda environment |
||||
COPY setup/environment.yml /tmp/environment.yml |
||||
RUN conda env create -f /tmp/environment.yml && \ |
||||
conda clean -afy && \ |
||||
rm /tmp/environment.yml |
||||
|
||||
# Copy remaining files |
||||
COPY bin/ bin/ |
||||
COPY hummingbot/ hummingbot/ |
||||
COPY scripts/ scripts/ |
||||
COPY scripts/ scripts-copy/ |
||||
COPY setup.py . |
||||
COPY LICENSE . |
||||
COPY README.md . |
||||
COPY DATA_COLLECTION.md . |
||||
|
||||
# activate hummingbot env when entering the CT |
||||
SHELL [ "/bin/bash", "-lc" ] |
||||
RUN echo "conda activate hummingbot" >> ~/.bashrc |
||||
|
||||
RUN python3 setup.py build_ext --inplace -j 8 && \ |
||||
rm -rf build/ && \ |
||||
find . -type f -name "*.cpp" -delete |
||||
|
||||
|
||||
# Build final image using artifacts from builder |
||||
FROM continuumio/miniconda3:latest AS release |
||||
|
||||
# Dockerfile author / maintainer |
||||
LABEL maintainer="Fede Cardoso @dardonacci <federico@hummingbot.org>" |
||||
|
||||
# Build arguments |
||||
ARG BRANCH="" |
||||
ARG COMMIT="" |
||||
ARG BUILD_DATE="" |
||||
LABEL branch=${BRANCH} |
||||
LABEL commit=${COMMIT} |
||||
LABEL date=${BUILD_DATE} |
||||
|
||||
# Set ENV variables |
||||
ENV COMMIT_SHA=${COMMIT} |
||||
ENV COMMIT_BRANCH=${BRANCH} |
||||
ENV BUILD_DATE=${DATE} |
||||
|
||||
ENV INSTALLATION_TYPE=docker |
||||
|
||||
# Install system dependencies |
||||
RUN apt-get update && \ |
||||
apt-get install -y sudo libusb-1.0 && \ |
||||
rm -rf /var/lib/apt/lists/* |
||||
|
||||
# Create mount points |
||||
RUN mkdir -p /home/hummingbot/conf /home/hummingbot/conf/connectors /home/hummingbot/conf/strategies /home/hummingbot/logs /home/hummingbot/data /home/hummingbot/certs /home/hummingbot/scripts |
||||
|
||||
WORKDIR /home/hummingbot |
||||
|
||||
# Copy all build artifacts from builder image |
||||
COPY --from=builder /opt/conda/ /opt/conda/ |
||||
COPY --from=builder /home/ /home/ |
||||
|
||||
# Setting bash as default shell because we have .bashrc with customized PATH (setting SHELL affects RUN, CMD and ENTRYPOINT, but not manual commands e.g. `docker run image COMMAND`!) |
||||
SHELL [ "/bin/bash", "-lc" ] |
||||
|
||||
# Set the default command to run when starting the container |
||||
|
||||
CMD conda activate hummingbot && ./bin/hummingbot_quickstart.py 2>> ./logs/errors.log |
@ -0,0 +1,201 @@ |
||||
Apache License |
||||
Version 2.0, January 2004 |
||||
http://www.apache.org/licenses/ |
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION |
||||
|
||||
1. Definitions. |
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction, |
||||
and distribution as defined by Sections 1 through 9 of this document. |
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by |
||||
the copyright owner that is granting the License. |
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all |
||||
other entities that control, are controlled by, or are under common |
||||
control with that entity. For the purposes of this definition, |
||||
"control" means (i) the power, direct or indirect, to cause the |
||||
direction or management of such entity, whether by contract or |
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the |
||||
outstanding shares, or (iii) beneficial ownership of such entity. |
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity |
||||
exercising permissions granted by this License. |
||||
|
||||
"Source" form shall mean the preferred form for making modifications, |
||||
including but not limited to software source code, documentation |
||||
source, and configuration files. |
||||
|
||||
"Object" form shall mean any form resulting from mechanical |
||||
transformation or translation of a Source form, including but |
||||
not limited to compiled object code, generated documentation, |
||||
and conversions to other media types. |
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or |
||||
Object form, made available under the License, as indicated by a |
||||
copyright notice that is included in or attached to the work |
||||
(an example is provided in the Appendix below). |
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object |
||||
form, that is based on (or derived from) the Work and for which the |
||||
editorial revisions, annotations, elaborations, or other modifications |
||||
represent, as a whole, an original work of authorship. For the purposes |
||||
of this License, Derivative Works shall not include works that remain |
||||
separable from, or merely link (or bind by name) to the interfaces of, |
||||
the Work and Derivative Works thereof. |
||||
|
||||
"Contribution" shall mean any work of authorship, including |
||||
the original version of the Work and any modifications or additions |
||||
to that Work or Derivative Works thereof, that is intentionally |
||||
submitted to Licensor for inclusion in the Work by the copyright owner |
||||
or by an individual or Legal Entity authorized to submit on behalf of |
||||
the copyright owner. For the purposes of this definition, "submitted" |
||||
means any form of electronic, verbal, or written communication sent |
||||
to the Licensor or its representatives, including but not limited to |
||||
communication on electronic mailing lists, source code control systems, |
||||
and issue tracking systems that are managed by, or on behalf of, the |
||||
Licensor for the purpose of discussing and improving the Work, but |
||||
excluding communication that is conspicuously marked or otherwise |
||||
designated in writing by the copyright owner as "Not a Contribution." |
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity |
||||
on behalf of whom a Contribution has been received by Licensor and |
||||
subsequently incorporated within the Work. |
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of |
||||
this License, each Contributor hereby grants to You a perpetual, |
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
||||
copyright license to reproduce, prepare Derivative Works of, |
||||
publicly display, publicly perform, sublicense, and distribute the |
||||
Work and such Derivative Works in Source or Object form. |
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of |
||||
this License, each Contributor hereby grants to You a perpetual, |
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
||||
(except as stated in this section) patent license to make, have made, |
||||
use, offer to sell, sell, import, and otherwise transfer the Work, |
||||
where such license applies only to those patent claims licensable |
||||
by such Contributor that are necessarily infringed by their |
||||
Contribution(s) alone or by combination of their Contribution(s) |
||||
with the Work to which such Contribution(s) was submitted. If You |
||||
institute patent litigation against any entity (including a |
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work |
||||
or a Contribution incorporated within the Work constitutes direct |
||||
or contributory patent infringement, then any patent licenses |
||||
granted to You under this License for that Work shall terminate |
||||
as of the date such litigation is filed. |
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the |
||||
Work or Derivative Works thereof in any medium, with or without |
||||
modifications, and in Source or Object form, provided that You |
||||
meet the following conditions: |
||||
|
||||
(a) You must give any other recipients of the Work or |
||||
Derivative Works a copy of this License; and |
||||
|
||||
(b) You must cause any modified files to carry prominent notices |
||||
stating that You changed the files; and |
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works |
||||
that You distribute, all copyright, patent, trademark, and |
||||
attribution notices from the Source form of the Work, |
||||
excluding those notices that do not pertain to any part of |
||||
the Derivative Works; and |
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its |
||||
distribution, then any Derivative Works that You distribute must |
||||
include a readable copy of the attribution notices contained |
||||
within such NOTICE file, excluding those notices that do not |
||||
pertain to any part of the Derivative Works, in at least one |
||||
of the following places: within a NOTICE text file distributed |
||||
as part of the Derivative Works; within the Source form or |
||||
documentation, if provided along with the Derivative Works; or, |
||||
within a display generated by the Derivative Works, if and |
||||
wherever such third-party notices normally appear. The contents |
||||
of the NOTICE file are for informational purposes only and |
||||
do not modify the License. You may add Your own attribution |
||||
notices within Derivative Works that You distribute, alongside |
||||
or as an addendum to the NOTICE text from the Work, provided |
||||
that such additional attribution notices cannot be construed |
||||
as modifying the License. |
||||
|
||||
You may add Your own copyright statement to Your modifications and |
||||
may provide additional or different license terms and conditions |
||||
for use, reproduction, or distribution of Your modifications, or |
||||
for any such Derivative Works as a whole, provided Your use, |
||||
reproduction, and distribution of the Work otherwise complies with |
||||
the conditions stated in this License. |
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise, |
||||
any Contribution intentionally submitted for inclusion in the Work |
||||
by You to the Licensor shall be under the terms and conditions of |
||||
this License, without any additional terms or conditions. |
||||
Notwithstanding the above, nothing herein shall supersede or modify |
||||
the terms of any separate license agreement you may have executed |
||||
with Licensor regarding such Contributions. |
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade |
||||
names, trademarks, service marks, or product names of the Licensor, |
||||
except as required for reasonable and customary use in describing the |
||||
origin of the Work and reproducing the content of the NOTICE file. |
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or |
||||
agreed to in writing, Licensor provides the Work (and each |
||||
Contributor provides its Contributions) on an "AS IS" BASIS, |
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
||||
implied, including, without limitation, any warranties or conditions |
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A |
||||
PARTICULAR PURPOSE. You are solely responsible for determining the |
||||
appropriateness of using or redistributing the Work and assume any |
||||
risks associated with Your exercise of permissions under this License. |
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory, |
||||
whether in tort (including negligence), contract, or otherwise, |
||||
unless required by applicable law (such as deliberate and grossly |
||||
negligent acts) or agreed to in writing, shall any Contributor be |
||||
liable to You for damages, including any direct, indirect, special, |
||||
incidental, or consequential damages of any character arising as a |
||||
result of this License or out of the use or inability to use the |
||||
Work (including but not limited to damages for loss of goodwill, |
||||
work stoppage, computer failure or malfunction, or any and all |
||||
other commercial damages or losses), even if such Contributor |
||||
has been advised of the possibility of such damages. |
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing |
||||
the Work or Derivative Works thereof, You may choose to offer, |
||||
and charge a fee for, acceptance of support, warranty, indemnity, |
||||
or other liability obligations and/or rights consistent with this |
||||
License. However, in accepting such obligations, You may act only |
||||
on Your own behalf and on Your sole responsibility, not on behalf |
||||
of any other Contributor, and only if You agree to indemnify, |
||||
defend, and hold each Contributor harmless for any liability |
||||
incurred by, or claims asserted against, such Contributor by reason |
||||
of your accepting any such warranty or additional liability. |
||||
|
||||
END OF TERMS AND CONDITIONS |
||||
|
||||
APPENDIX: How to apply the Apache License to your work. |
||||
|
||||
To apply the Apache License to your work, attach the following |
||||
boilerplate notice, with the fields enclosed by brackets "[]" |
||||
replaced with your own identifying information. (Don't include |
||||
the brackets!) The text should be enclosed in the appropriate |
||||
comment syntax for the file format. We also recommend that a |
||||
file or class name and description of purpose be included on the |
||||
same "printed page" as the copyright notice for easier |
||||
identification within third-party archives. |
||||
|
||||
Copyright 2023 Hummingbot Foundation. |
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
you may not use this file except in compliance with the License. |
||||
You may obtain a copy of the License at |
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
||||
Unless required by applicable law or agreed to in writing, software |
||||
distributed under the License is distributed on an "AS IS" BASIS, |
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
See the License for the specific language governing permissions and |
||||
limitations under the License. |
@ -0,0 +1,51 @@ |
||||
.ONESHELL: |
||||
.PHONY: test |
||||
.PHONY: run_coverage |
||||
.PHONY: report_coverage |
||||
.PHONY: development-diff-cover |
||||
.PHONY: docker |
||||
.PHONY: install |
||||
.PHONY: uninstall |
||||
.PHONY: clean |
||||
.PHONY: build |
||||
|
||||
test: |
||||
coverage run -m nose \
|
||||
--exclude-dir="test/connector" \
|
||||
--exclude-dir="test/debug" \
|
||||
--exclude-dir="test/mock" \
|
||||
--exclude-dir="test/hummingbot/connector/gateway/amm" \
|
||||
--exclude-dir="test/hummingbot/connector/exchange/coinbase_pro" \
|
||||
--exclude-dir="test/hummingbot/connector/exchange/kraken" \
|
||||
--exclude-dir="test/hummingbot/connector/exchange/hitbtc" \
|
||||
--exclude-dir="test/hummingbot/connector/gateway/clob_spot/data_sources/dexalot" \
|
||||
--exclude-dir="test/hummingbot/strategy/amm_arb" \
|
||||
--exclude-dir="test/hummingbot/core/gateway" \
|
||||
--exclude-dir="test/hummingbot/strategy/uniswap_v3_lp"
|
||||
|
||||
run_coverage: test |
||||
coverage report
|
||||
coverage html
|
||||
|
||||
report_coverage: |
||||
coverage report
|
||||
coverage html
|
||||
|
||||
development-diff-cover: |
||||
coverage xml
|
||||
diff-cover --compare-branch=origin/development coverage.xml
|
||||
|
||||
docker: |
||||
git clean -xdf && make clean && docker build -t hummingbot/hummingbot${TAG} -f Dockerfile .
|
||||
|
||||
clean: |
||||
./clean
|
||||
|
||||
install: |
||||
./install
|
||||
|
||||
uninstall: |
||||
./uninstall
|
||||
|
||||
build: |
||||
./compile
|
@ -1,3 +1,137 @@ |
||||
# hummingbot |
||||
![Hummingbot](https://i.ibb.co/X5zNkKw/blacklogo-with-text.png) |
||||
|
||||
Custom HummingBot for Whitebit |
||||
---- |
||||
[![License](https://img.shields.io/badge/License-Apache%202.0-informational.svg)](https://github.com/hummingbot/hummingbot/blob/master/LICENSE) |
||||
[![Twitter](https://img.shields.io/twitter/url?url=https://twitter.com/_hummingbot?style=social&label=_hummingbot)](https://twitter.com/_hummingbot) |
||||
[![Youtube](https://img.shields.io/youtube/channel/subscribers/UCxzzdEnDRbylLMWmaMjywOA)](https://www.youtube.com/@hummingbot) |
||||
[![Discord](https://img.shields.io/discord/530578568154054663?logo=discord&logoColor=white&style=flat-square)](https://discord.gg/hummingbot) |
||||
|
||||
Hummingbot is an open source framework that helps you build automated trading strategies, or **bots** that run on cryptocurrency exchanges. |
||||
|
||||
This code is free and publicly available under the Apache 2.0 open source license! |
||||
|
||||
## Why Hummingbot? |
||||
|
||||
* **Both CEX and DEX connectors**: Hummingbot supports connectors to centralized exchanges like Binance and KuCoin, as well as decentralized exchanges like Uniswap and PancakeSwap on various blockchains (Ethereum, BNB Chain, etc). |
||||
* **Community-contributed strategies**: The Hummingbot community has added many customizable templates for market making, arbitrage, and other algo trading strategies. |
||||
* **Secure local client**: Hummingbot is a local client software that you install and run on your own devices or cloud virtual machines. It encrypts your API keys and private keys and never exposes them to any third parties. |
||||
* **Community modules and events**: Hummingbot is driven by a global community of quant traders and developers. Check out community-maintained modules like Orchestration, join our bi-weekly developer calls, and learn how to build custom strategies using Hummingbot by taking Botcamp! |
||||
|
||||
Help us **democratize high-frequency trading** and make powerful trading algorithms accessible to everyone in the world! |
||||
|
||||
|
||||
## Quick Links |
||||
|
||||
* [Docs](https://docs.hummingbot.org): Check out the official Hummingbot documentation |
||||
* [Installation](https://hummingbot.org/installation/): Install Hummingbot on various platforms |
||||
* [FAQs](https://hummingbot.org/faq/): Answers to all your burning questions |
||||
* [Botcamp](https://hummingbot.org/botcamp/): Learn how build your own custom HFT strategy in Hummingbot with our hands-on bootcamp! |
||||
* [HBOT](https://hummingbot.org/hbot/): Learn how you can decide how this codebase evolves by voting with HBOT tokens |
||||
|
||||
## Community |
||||
|
||||
* [Newsletter](https://hummingbot.substack.com): Get our monthly newletter whenever we ship a new release |
||||
* [Discord](https://discord.gg/hummingbot): The main gathering spot for the global Hummingbot community |
||||
* [YouTube](https://www.youtube.com/c/hummingbot): Videos that teach you how to get the most of of Hummingbot |
||||
* [Twitter](https://twitter.com/_hummingbot): Get the latest announcements about Hummingbot |
||||
* [Snapshot](https://snapshot.org/#/hbot-prp.eth): Participate in monthly polls that decide which components should be prioritized and included |
||||
|
||||
## Exchange Connectors |
||||
|
||||
Hummingbot connectors standardize trading logic and order types across different exchange types. Currently, we support the following exchange types: |
||||
|
||||
* **SPOT**: Connectors to central limit order book (CLOB) exchanges that trade spot markets |
||||
* **PERP**: Connectors to central limit order book (CLOB) exchanges that trade perpetual swap markets |
||||
* **AMM**: Connectors to decentralized exchanges that use the Automatic Market Maker (AMM) methodology |
||||
|
||||
Exchanges may be centralized (**CEX**), or decentralized (**DEX**), in which case user assets are stored on the blockchain and trading is performed via wallet addresses. |
||||
|
||||
| Tier | Exchange | Type | Signup code | |
||||
|------|----------|------|-------------| |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=GOLD&color=yellow) | [Binance](https://docs.hummingbot.org/exchanges/binance/) | SPOT CEX | [FQQNNGCD](https://www.binance.com/en/register?ref=FQQNNGCD) |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=GOLD&color=yellow) | [Binance Futures](https://docs.hummingbot.org/exchanges/binance-perpetual/) | PERP CEX | [hummingbot](https://www.binance.com/en/futures/ref?code=hummingbot) |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=GOLD&color=yellow) | [dYdX](https://dydx.exchange/) | PERP DEX | |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=SILVER&color=silver) | [Dexalot](https://docs.hummingbot.org/exchanges/dexalot/) | CLOB DEX | |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=SILVER&color=silver) | [Gate.io](https://docs.hummingbot.org/exchanges/gate-io/) | SPOT CEX | [5868285](https://www.gate.io/signup/5868285) |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=SILVER&color=silver) | [Gate.io Perpetual](https://docs.hummingbot.org/exchanges/gate-io-perpetual/) | PERP CEX | [5868285](https://www.gate.io/signup/5868285) |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=SILVER&color=silver) | [Huobi](https://docs.hummingbot.org/exchanges/huobi/) | SPOT CEX | |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=SILVER&color=silver)| [Injective Helix](https://docs.hummingbot.org/exchanges/injective/) | CLOB DEX | |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=SILVER&color=silver) | [KuCoin](https://docs.hummingbot.org/exchanges/kucoin/) | SPOT CEX | [272KvRf](https://www.kucoin.com/ucenter/signup?rcode=272KvRf) |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=SILVER&color=silver) | [KuCoin Perpetual](https://docs.hummingbot.org/exchanges/kucoin-perpetual/) | PERP CEX | [272KvRf](https://www.kucoin.com/ucenter/signup?rcode=272KvRf) |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=SILVER&color=silver) | [Polkadex](https://docs.hummingbot.org/exchanges/polkadex/) | SPOT DEX | |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=BRONZE&color=green) | [AscendEx](https://docs.hummingbot.org/exchanges/ascend-ex/) | SPOT CEX | [UEIXNXKW](https://ascendex.com/register?inviteCode=UEIXNXKW) |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=BRONZE&color=green) | [BTC-Markets](https://docs.hummingbot.org/exchanges/btc-markets/) | SPOT CEX | |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=BRONZE&color=green) | [Bit.com](https://docs.hummingbot.org/exchanges/bit-com) | PERP CEX | |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=BRONZE&color=green) | [BitMart](https://docs.hummingbot.org/exchanges/bitmart/) | SPOT CEX | |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=BRONZE&color=green) | [Bitfinex](https://docs.hummingbot.org/exchanges/bitfinex/) | SPOT CEX | |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=BRONZE&color=green) | [BitGet](https://docs.hummingbot.org/exchanges/bitget-perpetual/) | PERP CEX | |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=BRONZE&color=green) | [Bitmex](https://docs.hummingbot.org/exchanges/bitmex/) | SPOT CEX | |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=BRONZE&color=green) | [Bitmex (perp](https://docs.hummingbot.org/exchanges/bitmex-perpetual/) | SPOT CEX | |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=BRONZE&color=green) | [Bybit](https://docs.hummingbot.org/exchanges/bybit/) | SPOT CEX | |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=BRONZE&color=green) | [Bybit (perp)](https://docs.hummingbot.org/exchanges/bitmex-perpetual/) | PERP CEX | |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=BRONZE&color=green) | [Coinbase](https://docs.hummingbot.org/exchanges/coinbase/) | SPOT CEX | |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=BRONZE&color=green) | [HitBTC](https://docs.hummingbot.org/exchanges/hitbtc/) | SPOT CEX | |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=BRONZE&color=green) | [Kraken](https://docs.hummingbot.org/exchanges/kraken/) | SPOT CEX | |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=BRONZE&color=green) | [MEXC](https://docs.hummingbot.org/exchanges/mexc/) | SPOT CEX | |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=BRONZE&color=green) | [Mad Meerkat](https://docs.hummingbot.org/exchanges/mad-meerkat/) | SPOT DEX | |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=BRONZE&color=green) | [NDAX](https://docs.hummingbot.org/exchanges/ndax/) | SPOT DEX | |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=BRONZE&color=green) | [OKX](https://docs.hummingbot.org/exchanges/okx/) | SPOT CEX | |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=BRONZE&color=green) | [OpenOcean](https://docs.hummingbot.org/exchanges/openocean/) | AMM DEX | |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=BRONZE&color=green) | [Pancakeswap](https://docs.hummingbot.org/exchanges/pancakeswap/) | AMM DEX | |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=BRONZE&color=green) | [Pangolin](https://docs.hummingbot.org/exchanges/pangolin/) | AMM DEX | |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=BRONZE&color=green) | [Perpetual Protocol](https://docs.hummingbot.org/exchanges/perp/) | PERP DEX | |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=BRONZE&color=green) | [Phemex Perpetual](https://docs.hummingbot.org/exchanges/perp/) | PERP CEX | |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=BRONZE&color=green) | [Plenty](https://docs.hummingbot.org/exchanges/plenty/) | AMM DEX | |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=BRONZE&color=green) | [Quickswap](https://docs.hummingbot.org/exchanges/quickswap/) | AMM DEX | |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=BRONZE&color=green) | [Ref Finance](https://docs.hummingbot.org/exchanges/ref/) | SPOT DEX | |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=BRONZE&color=green) | [Sushiswap](https://docs.hummingbot.org/exchanges/sushiswap/) | AMM DEX | |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=BRONZE&color=green) | [Tinyman](https://docs.hummingbot.org/exchanges/tinyman/) | SPOT DEX | |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=BRONZE&color=green) | [Traderjoe](https://docs.hummingbot.org/exchanges/traderjoe) | AMM |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=BRONZE&color=green) | [Uniswap](https://docs.hummingbot.org/exchanges/uniswap/) | AMM DEX | |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=BRONZE&color=green) | [VVS Finance](https://docs.hummingbot.org/exchanges/vvs/) | AMM DEX | |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=BRONZE&color=green) | [Vertex](https://docs.hummingbot.org/exchanges/vertex/) | CLOB DEX | |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=BRONZE&color=green) | [WOO X](https://docs.hummingbot.org/exchanges/woo-x)| CEX CLOB | |
||||
| ![](https://img.shields.io/static/v1?label=Hummingbot&message=BRONZE&color=green) | [XSWAP](https://docs.hummingbot.org/exchanges/xswap/) | AMM DEX | |
||||
|
||||
|
||||
|
||||
Quarterly [Polls](https://docs.hummingbot.org/governance/polls/) allow the Hummingbot community to vote using HBOT tokens to decide which exchanges should be certified GOLD or SILVER, which means that they are maintained and continually improved by Hummingbot Foundation. In addition, the codebase includes BRONZE exchange connectors that are maintained by community members. See the [Hummingbot documentation](https://docs.hummingbot.org/exchanges) for all exchanges supported. |
||||
|
||||
## Strategies and Scripts |
||||
|
||||
We provide customizable strategy templates for core trading strategies that users can configure, extend, and run. Hummingbot Foundation maintains three **CORE** strategies: |
||||
|
||||
* [Pure Market Making](https://docs.hummingbot.org/strategies/pure-market-making/): Our original single-pair market making strategy |
||||
* [Cross Exchange Market Making](https://docs.hummingbot.org/strategies/cross-exchange-market-making/): Provide liquidity while hedging filled orders on another exchange |
||||
* [AMM Arbitrage](https://docs.hummingbot.org/strategies/amm-arbitrage/): Exploits price differences between AMM and SPOT exchanges |
||||
|
||||
**CORE** strategies are selected via HBOT voting through quarterly [Polls](https://docs.hummingbot.org/governance/polls/). In addition, the codebase includes **COMMUNITY** strategies that are maintained by individuals or firms in the community. See the [Hummingbot documentation](https://docs.hummingbot.org/strategies) for all strategies supported. |
||||
|
||||
### Scripts |
||||
|
||||
Scripts are a newer, lighter way to build Hummingbot strategies in Python, which let you modify the script's code and re-run it to apply the changes without exiting the Hummingbot interface or re-compiling the code. |
||||
|
||||
See the [Scripts](https://docs.hummingbot.org/scripts/) section in the documentation for more info, or check out the [/scripts](https://github.com/hummingbot/hummingbot/tree/master/scripts) folder for all Script examples included in the codebase. |
||||
|
||||
## Other Hummingbot Repos |
||||
|
||||
* [Hummingbot Site](https://github.com/hummingbot/hummingbot-site): Official documentation for Hummingbot - we welcome contributions here too! |
||||
* [Hummingbot Project Management](https://github.com/hummingbot/pm): Agendas and recordings of regular Hummingbot developer and community calls |
||||
* [Awesome Hummingbot](https://github.com/hummingbot/awesome-hummingbot): All the Hummingbot links |
||||
* [Hummingbot StreamLit Apps](https://github.com/hummingbot/streamlit-apps): Hummingbot-related StreamLit data apps and dashboards |
||||
* [Community Tools](https://github.com/hummingbot/community-tools): Community contributed resources related to Hummingbot |
||||
* [Brokers](https://github.com/hummingbot/brokers): Different brokers that can be used to communicate with multiple instances of Hummingbot |
||||
* [Deploy Examples](https://github.com/hummingbot/deploy-examples): Deploy Hummingbot in various configurations with Docker |
||||
* [Remote Client](https://github.com/hummingbot/hbot-remote-client-py): A remote client for Hummingbot in Python |
||||
* [Dashboard](https://github.com/hummingbot/dashboard): Hummingbot Dashboard community project |
||||
|
||||
## Contributions |
||||
|
||||
Hummingbot belongs to its community, so we welcome contributions! Please review these [guidelines](./CONTRIBUTING.md) first. |
||||
|
||||
To have your pull request merged into the codebase, submit a [Pull Request Proposal](https://snapshot.org/#/hbot-prp.eth) on our Snapshot. Note that you will need 1 HBOT in your Ethereum wallet to submit a Pull Request Proposal. See [HBOT](https://hummingbot.org/hbot) for more information. |
||||
|
||||
## Legal |
||||
|
||||
* **License**: Hummingbot is licensed under [Apache 2.0](./LICENSE). |
||||
* **Data collection**: read important information regarding [Hummingbot Data Collection](./DATA_COLLECTION.md). |
||||
|
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 7.4 KiB |
After Width: | Height: | Size: 152 KiB |
After Width: | Height: | Size: 128 KiB |
After Width: | Height: | Size: 141 KiB |
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 163 KiB |
After Width: | Height: | Size: 196 KiB |
After Width: | Height: | Size: 172 KiB |
After Width: | Height: | Size: 118 KiB |
After Width: | Height: | Size: 5.7 KiB |
After Width: | Height: | Size: 168 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 140 KiB |
After Width: | Height: | Size: 104 KiB |
After Width: | Height: | Size: 119 KiB |
After Width: | Height: | Size: 132 KiB |
After Width: | Height: | Size: 120 KiB |
After Width: | Height: | Size: 153 KiB |
After Width: | Height: | Size: 131 KiB |
After Width: | Height: | Size: 158 KiB |
After Width: | Height: | Size: 128 KiB |
After Width: | Height: | Size: 130 KiB |
After Width: | Height: | Size: 962 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 109 KiB |
After Width: | Height: | Size: 143 KiB |
After Width: | Height: | Size: 182 KiB |
After Width: | Height: | Size: 130 KiB |
After Width: | Height: | Size: 141 KiB |
After Width: | Height: | Size: 134 KiB |
After Width: | Height: | Size: 143 KiB |
After Width: | Height: | Size: 141 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 126 KiB |
After Width: | Height: | Size: 141 KiB |
After Width: | Height: | Size: 138 KiB |
After Width: | Height: | Size: 8.4 KiB |
After Width: | Height: | Size: 163 KiB |
After Width: | Height: | Size: 128 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 132 KiB |
After Width: | Height: | Size: 116 KiB |
After Width: | Height: | Size: 136 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 144 KiB |
After Width: | Height: | Size: 105 KiB |
After Width: | Height: | Size: 116 KiB |
After Width: | Height: | Size: 130 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 138 KiB |
After Width: | Height: | Size: 1.1 KiB |
@ -0,0 +1 @@ |
||||
dev.py |
@ -0,0 +1,13 @@ |
||||
import argparse |
||||
|
||||
import path_util # noqa: F401 |
||||
|
||||
from hummingbot.client.config.conf_migration import migrate_configs |
||||
from hummingbot.client.config.config_crypt import ETHKeyFileSecretManger |
||||
|
||||
if __name__ == "__main__": |
||||
parser = argparse.ArgumentParser(description="Migrate the HummingBot confs") |
||||
parser.add_argument("password", type=str, help="Required to migrate all encrypted configs.") |
||||
args = parser.parse_args() |
||||
secrets_manager_ = ETHKeyFileSecretManger(args.password) |
||||
migrate_configs(secrets_manager_) |
@ -0,0 +1,99 @@ |
||||
#!/usr/bin/env python |
||||
|
||||
import asyncio |
||||
from typing import Coroutine, List, Optional |
||||
from weakref import ReferenceType, ref |
||||
|
||||
import path_util # noqa: F401 |
||||
|
||||
from hummingbot import chdir_to_data_directory, init_logging |
||||
from hummingbot.client.config.client_config_map import ClientConfigMap |
||||
from hummingbot.client.config.config_crypt import ETHKeyFileSecretManger |
||||
from hummingbot.client.config.config_helpers import ( |
||||
ClientConfigAdapter, |
||||
create_yml_files_legacy, |
||||
load_client_config_map_from_file, |
||||
write_config_to_yml, |
||||
) |
||||
from hummingbot.client.hummingbot_application import HummingbotApplication |
||||
from hummingbot.client.settings import AllConnectorSettings |
||||
from hummingbot.client.ui import login_prompt |
||||
from hummingbot.client.ui.style import load_style |
||||
from hummingbot.core.event.event_listener import EventListener |
||||
from hummingbot.core.event.events import HummingbotUIEvent |
||||
from hummingbot.core.utils import detect_available_port |
||||
from hummingbot.core.utils.async_utils import safe_gather |
||||
|
||||
|
||||
class UIStartListener(EventListener): |
||||
def __init__(self, hummingbot_app: HummingbotApplication, is_script: Optional[bool] = False, is_quickstart: Optional[bool] = False): |
||||
super().__init__() |
||||
self._hb_ref: ReferenceType = ref(hummingbot_app) |
||||
self._is_script = is_script |
||||
self._is_quickstart = is_quickstart |
||||
|
||||
def __call__(self, _): |
||||
asyncio.create_task(self.ui_start_handler()) |
||||
|
||||
@property |
||||
def hummingbot_app(self) -> HummingbotApplication: |
||||
return self._hb_ref() |
||||
|
||||
async def ui_start_handler(self): |
||||
hb: HummingbotApplication = self.hummingbot_app |
||||
if hb.strategy_name is not None: |
||||
if not self._is_script: |
||||
write_config_to_yml(hb.strategy_config_map, hb.strategy_file_name, hb.client_config_map) |
||||
hb.start(log_level=hb.client_config_map.log_level, |
||||
script=hb.strategy_name if self._is_script else None, |
||||
is_quickstart=self._is_quickstart) |
||||
|
||||
|
||||
async def main_async(client_config_map: ClientConfigAdapter): |
||||
await create_yml_files_legacy() |
||||
|
||||
# This init_logging() call is important, to skip over the missing config warnings. |
||||
init_logging("hummingbot_logs.yml", client_config_map) |
||||
|
||||
AllConnectorSettings.initialize_paper_trade_settings(client_config_map.paper_trade.paper_trade_exchanges) |
||||
|
||||
hb = HummingbotApplication.main_application(client_config_map) |
||||
|
||||
# The listener needs to have a named variable for keeping reference, since the event listener system |
||||
# uses weak references to remove unneeded listeners. |
||||
start_listener: UIStartListener = UIStartListener(hb) |
||||
hb.app.add_listener(HummingbotUIEvent.Start, start_listener) |
||||
|
||||
tasks: List[Coroutine] = [hb.run()] |
||||
if client_config_map.debug_console: |
||||
if not hasattr(__builtins__, "help"): |
||||
import _sitebuiltins |
||||
__builtins__["help"] = _sitebuiltins._Helper() |
||||
|
||||
from hummingbot.core.management.console import start_management_console |
||||
management_port: int = detect_available_port(8211) |
||||
tasks.append(start_management_console(locals(), host="localhost", port=management_port)) |
||||
await safe_gather(*tasks) |
||||
|
||||
|
||||
def main(): |
||||
chdir_to_data_directory() |
||||
secrets_manager_cls = ETHKeyFileSecretManger |
||||
|
||||
try: |
||||
ev_loop: asyncio.AbstractEventLoop = asyncio.get_event_loop() |
||||
except Exception: |
||||
ev_loop: asyncio.AbstractEventLoop = asyncio.new_event_loop() |
||||
asyncio.set_event_loop(ev_loop) |
||||
|
||||
# We need to load a default style for the login screen because the password is required to load the |
||||
# real configuration now that it can include secret parameters |
||||
style = load_style(ClientConfigAdapter(ClientConfigMap())) |
||||
|
||||
if login_prompt(secrets_manager_cls, style=style): |
||||
client_config_map = load_client_config_map_from_file() |
||||
ev_loop.run_until_complete(main_async(client_config_map)) |
||||
|
||||
|
||||
if __name__ == "__main__": |
||||
main() |
@ -0,0 +1,161 @@ |
||||
#!/usr/bin/env python |
||||
|
||||
import argparse |
||||
import asyncio |
||||
import grp |
||||
import logging |
||||
import os |
||||
import pwd |
||||
import subprocess |
||||
from pathlib import Path |
||||
from typing import Coroutine, List |
||||
|
||||
import path_util # noqa: F401 |
||||
|
||||
from bin.hummingbot import UIStartListener, detect_available_port |
||||
from hummingbot import init_logging |
||||
from hummingbot.client.config.config_crypt import BaseSecretsManager, ETHKeyFileSecretManger |
||||
from hummingbot.client.config.config_helpers import ( |
||||
ClientConfigAdapter, |
||||
all_configs_complete, |
||||
create_yml_files_legacy, |
||||
load_client_config_map_from_file, |
||||
load_strategy_config_map_from_file, |
||||
read_system_configs_from_yml, |
||||
) |
||||
from hummingbot.client.config.security import Security |
||||
from hummingbot.client.hummingbot_application import HummingbotApplication |
||||
from hummingbot.client.settings import STRATEGIES_CONF_DIR_PATH, AllConnectorSettings |
||||
from hummingbot.client.ui import login_prompt |
||||
from hummingbot.client.ui.style import load_style |
||||
from hummingbot.core.event.events import HummingbotUIEvent |
||||
from hummingbot.core.management.console import start_management_console |
||||
from hummingbot.core.utils.async_utils import safe_gather |
||||
|
||||
|
||||
class CmdlineParser(argparse.ArgumentParser): |
||||
def __init__(self): |
||||
super().__init__() |
||||
self.add_argument("--config-file-name", "-f", |
||||
type=str, |
||||
required=False, |
||||
help="Specify a file in `conf/` to load as the strategy config file.") |
||||
self.add_argument("--config-password", "-p", |
||||
type=str, |
||||
required=False, |
||||
help="Specify the password to unlock your encrypted files.") |
||||
self.add_argument("--auto-set-permissions", |
||||
type=str, |
||||
required=False, |
||||
help="Try to automatically set config / logs / data dir permissions, " |
||||
"useful for Docker containers.") |
||||
|
||||
|
||||
def autofix_permissions(user_group_spec: str): |
||||
uid, gid = [sub_str for sub_str in user_group_spec.split(':')] |
||||
|
||||
uid = int(uid) if uid.isnumeric() else pwd.getpwnam(uid).pw_uid |
||||
gid = int(gid) if gid.isnumeric() else grp.getgrnam(gid).gr_gid |
||||
|
||||
os.environ["HOME"] = pwd.getpwuid(uid).pw_dir |
||||
project_home: str = os.path.realpath(os.path.join(__file__, "../../")) |
||||
|
||||
gateway_path: str = Path.home().joinpath(".hummingbot-gateway").as_posix() |
||||
subprocess.run( |
||||
f"cd '{project_home}' && " |
||||
f"sudo chown -R {user_group_spec} conf/ data/ logs/ scripts/ {gateway_path}", |
||||
capture_output=True, |
||||
shell=True |
||||
) |
||||
os.setgid(gid) |
||||
os.setuid(uid) |
||||
|
||||
|
||||
async def quick_start(args: argparse.Namespace, secrets_manager: BaseSecretsManager): |
||||
config_file_name = args.config_file_name |
||||
client_config_map = load_client_config_map_from_file() |
||||
|
||||
if args.auto_set_permissions is not None: |
||||
autofix_permissions(args.auto_set_permissions) |
||||
|
||||
if not Security.login(secrets_manager): |
||||
logging.getLogger().error("Invalid password.") |
||||
return |
||||
|
||||
await Security.wait_til_decryption_done() |
||||
await create_yml_files_legacy() |
||||
init_logging("hummingbot_logs.yml", client_config_map) |
||||
await read_system_configs_from_yml() |
||||
|
||||
AllConnectorSettings.initialize_paper_trade_settings(client_config_map.paper_trade.paper_trade_exchanges) |
||||
|
||||
hb = HummingbotApplication.main_application(client_config_map=client_config_map) |
||||
# Todo: validate strategy and config_file_name before assinging |
||||
|
||||
strategy_config = None |
||||
is_script = False |
||||
if config_file_name is not None: |
||||
hb.strategy_file_name = config_file_name |
||||
if config_file_name.split(".")[-1] == "py": |
||||
hb.strategy_name = hb.strategy_file_name |
||||
is_script = True |
||||
else: |
||||
strategy_config = await load_strategy_config_map_from_file( |
||||
STRATEGIES_CONF_DIR_PATH / config_file_name |
||||
) |
||||
hb.strategy_name = ( |
||||
strategy_config.strategy |
||||
if isinstance(strategy_config, ClientConfigAdapter) |
||||
else strategy_config.get("strategy").value |
||||
) |
||||
hb.strategy_config_map = strategy_config |
||||
|
||||
if strategy_config is not None: |
||||
if not all_configs_complete(strategy_config, hb.client_config_map): |
||||
hb.status() |
||||
|
||||
# The listener needs to have a named variable for keeping reference, since the event listener system |
||||
# uses weak references to remove unneeded listeners. |
||||
start_listener: UIStartListener = UIStartListener(hb, is_script=is_script, is_quickstart=True) |
||||
hb.app.add_listener(HummingbotUIEvent.Start, start_listener) |
||||
|
||||
tasks: List[Coroutine] = [hb.run()] |
||||
if client_config_map.debug_console: |
||||
management_port: int = detect_available_port(8211) |
||||
tasks.append(start_management_console(locals(), host="localhost", port=management_port)) |
||||
|
||||
await safe_gather(*tasks) |
||||
|
||||
|
||||
def main(): |
||||
args = CmdlineParser().parse_args() |
||||
|
||||
# Parse environment variables from Dockerfile. |
||||
# If an environment variable is not empty and it's not defined in the arguments, then we'll use the environment |
||||
# variable. |
||||
if args.config_file_name is None and len(os.environ.get("CONFIG_FILE_NAME", "")) > 0: |
||||
args.config_file_name = os.environ["CONFIG_FILE_NAME"] |
||||
if args.config_password is None and len(os.environ.get("CONFIG_PASSWORD", "")) > 0: |
||||
args.config_password = os.environ["CONFIG_PASSWORD"] |
||||
|
||||
# If no password is given from the command line, prompt for one. |
||||
secrets_manager_cls = ETHKeyFileSecretManger |
||||
client_config_map = load_client_config_map_from_file() |
||||
if args.config_password is None: |
||||
secrets_manager = login_prompt(secrets_manager_cls, style=load_style(client_config_map)) |
||||
if not secrets_manager: |
||||
return |
||||
else: |
||||
secrets_manager = secrets_manager_cls(args.config_password) |
||||
|
||||
try: |
||||
ev_loop: asyncio.AbstractEventLoop = asyncio.get_event_loop() |
||||
except Exception: |
||||
ev_loop: asyncio.AbstractEventLoop = asyncio.new_event_loop() |
||||
asyncio.set_event_loop(ev_loop) |
||||
|
||||
ev_loop.run_until_complete(quick_start(args, secrets_manager)) |
||||
|
||||
|
||||
if __name__ == "__main__": |
||||
main() |
@ -0,0 +1,16 @@ |
||||
#!/usr/bin/python |
||||
|
||||
if "hummingbot-dist" in __file__: |
||||
# Dist environment. |
||||
import os |
||||
import sys |
||||
sys.path.append(sys.path.pop(0)) |
||||
sys.path.insert(0, os.getcwd()) |
||||
|
||||
import hummingbot |
||||
hummingbot.set_prefix_path(os.getcwd()) |
||||
else: |
||||
# Dev environment. |
||||
from os.path import join, realpath |
||||
import sys |
||||
sys.path.insert(0, realpath(join(__file__, "../../"))) |
@ -0,0 +1,11 @@ |
||||
#!/bin/bash |
||||
|
||||
cd $(dirname "$0") |
||||
|
||||
echo "Cleaning up all intermediate and compiled files generated from Cython compilation..." |
||||
|
||||
rm -rf build dist |
||||
rm -f $(find . -name "*.pyx" | sed s/\.pyx$/\*\.cpp/g) |
||||
find . \( -name "*.so" -o -name "*.pyd" \) -exec rm -f {} \; |
||||
|
||||
echo "Done!" |
@ -0,0 +1,5 @@ |
||||
#!/bin/bash |
||||
|
||||
cd $(dirname "$0") |
||||
|
||||
python setup.py build_ext --inplace |
@ -0,0 +1,3 @@ |
||||
@echo off |
||||
|
||||
python setup.py build_ext --inplace -j 8 |
@ -0,0 +1,3 @@ |
||||
*.yml |
||||
*.json |
||||
.password_verification |
@ -0,0 +1,106 @@ |
||||
#!/usr/bin/env python |
||||
|
||||
import logging as _logging |
||||
import os |
||||
|
||||
_logger = _logging.getLogger(__name__) |
||||
|
||||
master_host = "***REMOVED***" |
||||
master_user = "***REMOVED***" |
||||
master_password = "***REMOVED***" |
||||
master_db = "***REMOVED***" |
||||
|
||||
slave_host = "127.0.0.1" |
||||
slave_user = "reader" |
||||
slave_password = "falcon" |
||||
slave_db = "falcon" |
||||
|
||||
mysql_master_server = "***REMOVED***" |
||||
mysql_slave_server = "***REMOVED***" |
||||
|
||||
mysql_user = "***REMOVED***" |
||||
mysql_password = "***REMOVED***" |
||||
mysql_db = "***REMOVED***" |
||||
|
||||
order_book_db = "***REMOVED***" |
||||
sparrow_db = "***REMOVED***" |
||||
|
||||
order_books_db_2 = { |
||||
"host": "***REMOVED***", |
||||
"user": "***REMOVED***", |
||||
"password": "***REMOVED***", |
||||
"db": "**REMOVED***", |
||||
} |
||||
|
||||
# whether to enable api mocking in unit test cases |
||||
mock_api_enabled = os.getenv("MOCK_API_ENABLED") |
||||
|
||||
""" |
||||
# AscendEX Tests |
||||
ascend_ex_api_key = os.getenv("ASCEND_EX_KEY") |
||||
ascend_ex_secret_key = os.getenv("ASCEND_EX_SECRET") |
||||
|
||||
# Binance Tests |
||||
binance_api_key = os.getenv("BINANCE_API_KEY") |
||||
binance_api_secret = os.getenv("BINANCE_API_SECRET") |
||||
|
||||
# Binance Perpetuals Tests |
||||
binance_perpetuals_api_key = os.getenv("BINANCE_PERPETUALS_API_KEY") |
||||
binance_perpetuals_api_secret = os.getenv("BINANCE_PERPETUALS_API_SECRET") |
||||
|
||||
# Coinbase Pro Tests |
||||
coinbase_pro_api_key = os.getenv("COINBASE_PRO_API_KEY") |
||||
coinbase_pro_secret_key = os.getenv("COINBASE_PRO_SECRET_KEY") |
||||
coinbase_pro_passphrase = os.getenv("COINBASE_PRO_PASSPHRASE") |
||||
|
||||
|
||||
# Huobi Tests |
||||
huobi_api_key = os.getenv("HUOBI_API_KEY") |
||||
huobi_secret_key = os.getenv("HUOBI_SECRET_KEY") |
||||
|
||||
# Bittrex Tests |
||||
bittrex_api_key = os.getenv("BITTREX_API_KEY") |
||||
bittrex_secret_key = os.getenv("BITTREX_SECRET_KEY") |
||||
|
||||
# KuCoin Tests |
||||
kucoin_api_key = os.getenv("KUCOIN_API_KEY") |
||||
kucoin_secret_key = os.getenv("KUCOIN_SECRET_KEY") |
||||
kucoin_passphrase = os.getenv("KUCOIN_PASSPHRASE") |
||||
|
||||
test_web3_provider_list = [os.getenv("WEB3_PROVIDER")] |
||||
|
||||
# Kraken Tests |
||||
kraken_api_key = os.getenv("KRAKEN_API_KEY") |
||||
kraken_secret_key = os.getenv("KRAKEN_SECRET_KEY") |
||||
|
||||
# OKX Test |
||||
okx_api_key = os.getenv("OKX_API_KEY") |
||||
okx_secret_key = os.getenv("OKX_SECRET_KEY") |
||||
okx_passphrase = os.getenv("OKX_PASSPHRASE") |
||||
|
||||
# BitMart Test |
||||
bitmart_api_key = os.getenv("BITMART_API_KEY") |
||||
bitmart_secret_key = os.getenv("BITMART_SECRET_KEY") |
||||
bitmart_memo = os.getenv("BITMART_MEMO") |
||||
|
||||
# BTC Markets Test |
||||
btc_markets_api_key = os.getenv("BTC_MARKETS_API_KEY") |
||||
btc_markets_secret_key = os.getenv("BTC_MARKETS_SECRET_KEY") |
||||
|
||||
# HitBTC Tests |
||||
hitbtc_api_key = os.getenv("HITBTC_API_KEY") |
||||
hitbtc_secret_key = os.getenv("HITBTC_SECRET_KEY") |
||||
|
||||
# Gate.io Tests |
||||
gate_io_api_key = os.getenv("GATE_IO_API_KEY") |
||||
gate_io_secret_key = os.getenv("GATE_IO_SECRET_KEY") |
||||
|
||||
# Wallet Tests |
||||
test_erc20_token_address = os.getenv("TEST_ERC20_TOKEN_ADDRESS") |
||||
web3_test_private_key_a = os.getenv("TEST_WALLET_PRIVATE_KEY_A") |
||||
web3_test_private_key_b = os.getenv("TEST_WALLET_PRIVATE_KEY_B") |
||||
web3_test_private_key_c = os.getenv("TEST_WALLET_PRIVATE_KEY_C") |
||||
|
||||
coinalpha_order_book_api_username = "***REMOVED***" |
||||
coinalpha_order_book_api_password = "***REMOVED***" |
||||
""" |
@ -0,0 +1 @@ |
||||
*.yml |
@ -0,0 +1 @@ |
||||
*.yml |
@ -0,0 +1,47 @@ |
||||
version: "3.9" |
||||
services: |
||||
hummingbot: |
||||
container_name: hummingbot |
||||
build: |
||||
context: . |
||||
dockerfile: Dockerfile |
||||
volumes: |
||||
- ./conf:/home/hummingbot/conf |
||||
- ./conf/connectors:/home/hummingbot/conf/connectors |
||||
- ./conf/strategies:/home/hummingbot/conf/strategies |
||||
- ./logs:/home/hummingbot/logs |
||||
- ./data:/home/hummingbot/data |
||||
- ./scripts:/home/hummingbot/scripts |
||||
logging: |
||||
driver: "json-file" |
||||
options: |
||||
max-size: "10m" |
||||
max-file: "5" |
||||
tty: true |
||||
stdin_open: true |
||||
network_mode: host |
||||
# environment: |
||||
# - CONFIG_PASSWORD=a |
||||
# - CONFIG_FILE_NAME=simple_pmm_example.py |
||||
|
||||
# dashboard: |
||||
# container_name: dashboard |
||||
# image: hummingbot/dashboard:latest |
||||
# volumes: |
||||
# - ./data:/home/dashboard/data |
||||
# ports: |
||||
# - "8501:8501" |
||||
|
||||
# gateway: |
||||
# container_name: gateway |
||||
# image: hummingbot/gateway:latest |
||||
# ports: |
||||
# - "15888:15888" |
||||
# - "8080:8080" |
||||
# volumes: |
||||
# - "./gateway_files/conf:/home/gateway/conf" |
||||
# - "./gateway_files/logs:/home/gateway/logs" |
||||
# - "./gateway_files/db:/home/gateway/db" |
||||
# - "./certs:/home/gateway/certs" |
||||
# environment: |
||||
# - GATEWAY_PASSPHRASE=a |
@ -0,0 +1 @@ |
||||
hummingbot ALL=(ALL:ALL) NOPASSWD: /bin/chmod, /bin/chown |
@ -0,0 +1,5 @@ |
||||
# Docker Autobuild Hooks |
||||
|
||||
This folder containers hooks for docker autobuild. |
||||
|
||||
[Hummingbot builds](https://hub.docker.com/r/coinalpha/hummingbot/builds) |
@ -0,0 +1,6 @@ |
||||
#!/usr/bin/env sh |
||||
|
||||
# Get the build time stamp |
||||
BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") |
||||
|
||||
docker build --build-arg COMMIT=$GIT_TAG --build-arg BRANCH=$SOURCE_BRANCH --build-arg BUILD_DATE=$BUILD_DATE -t $IMAGE_NAME -f Dockerfile . |
@ -0,0 +1,29 @@ |
||||
# Hummingbot Source Code |
||||
|
||||
This folder contains the main source code for Hummingbot. |
||||
|
||||
## Project Breakdown |
||||
``` |
||||
hummingbot |
||||
├── client # CLI related files |
||||
├── core |
||||
│ ├── cpp # high performance data types written in .cpp |
||||
│ ├── data_type # key data |
||||
│ ├── event # defined events and event-tracking related files |
||||
│ └── utils # helper functions and bot plugins |
||||
├── data_feed # price feeds such as CoinCap |
||||
├── logger # handles logging functionality |
||||
├── market # connectors to individual exchanges |
||||
│ └── <market_name> # folder for specific exchange ("market") |
||||
│ ├── *_market # handles trade execution (buy/sell/cancel) |
||||
│ ├── *_data_source # initializes and maintains a websocket connection |
||||
│ ├── *_order_book # takes order book data and formats it with a standard API |
||||
│ ├── *_order_book_tracker # maintains a copy of the market's real-time order book |
||||
│ ├── *_active_order_tracker # for DEXes that require keeping track of |
||||
│ └── *_user_stream_tracker # tracker that process data specific to the user running the bot |
||||
├── notifier # connectors to services that sends notifications such as Telegram |
||||
├── strategy # high level strategies that works with every market |
||||
├── templates # templates for config files: general, strategy, and logging |
||||
└── wallet # files that read from and submit transactions to blockchains |
||||
└── ethereum # files that interact with the ethereum blockchain |
||||
``` |
@ -0,0 +1 @@ |
||||
1.22.0 |
@ -0,0 +1,182 @@ |
||||
import logging |
||||
import subprocess |
||||
import sys |
||||
from concurrent.futures import ThreadPoolExecutor |
||||
from os import listdir, path |
||||
from pathlib import Path |
||||
from typing import TYPE_CHECKING, List, Optional |
||||
|
||||
from hummingbot.logger.struct_logger import StructLogger, StructLogRecord |
||||
|
||||
if TYPE_CHECKING: |
||||
from hummingbot.client.config.config_helpers import ClientConfigAdapter as _ClientConfigAdapter |
||||
|
||||
STRUCT_LOGGER_SET = False |
||||
DEV_STRATEGY_PREFIX = "dev" |
||||
_prefix_path = None |
||||
|
||||
# Do not raise exceptions during log handling |
||||
logging.setLogRecordFactory(StructLogRecord) |
||||
logging.setLoggerClass(StructLogger) |
||||
|
||||
_shared_executor = None |
||||
_data_path = None |
||||
_cert_path = None |
||||
|
||||
|
||||
def root_path() -> Path: |
||||
from os.path import join, realpath |
||||
return Path(realpath(join(__file__, "../../"))) |
||||
|
||||
|
||||
def get_executor() -> ThreadPoolExecutor: |
||||
global _shared_executor |
||||
if _shared_executor is None: |
||||
_shared_executor = ThreadPoolExecutor() |
||||
return _shared_executor |
||||
|
||||
|
||||
def prefix_path() -> str: |
||||
global _prefix_path |
||||
if _prefix_path is None: |
||||
from os.path import join, realpath |
||||
_prefix_path = realpath(join(__file__, "../../")) |
||||
return _prefix_path |
||||
|
||||
|
||||
def set_prefix_path(p: str): |
||||
global _prefix_path |
||||
_prefix_path = p |
||||
|
||||
|
||||
def data_path() -> str: |
||||
global _data_path |
||||
if _data_path is None: |
||||
from os.path import join, realpath |
||||
_data_path = realpath(join(prefix_path(), "data")) |
||||
|
||||
import os |
||||
if not os.path.exists(_data_path): |
||||
os.makedirs(_data_path) |
||||
return _data_path |
||||
|
||||
|
||||
def set_data_path(path: str): |
||||
global _data_path |
||||
_data_path = path |
||||
|
||||
|
||||
_independent_package: Optional[bool] = None |
||||
|
||||
|
||||
def is_independent_package() -> bool: |
||||
global _independent_package |
||||
import os |
||||
if _independent_package is None: |
||||
_independent_package = not os.path.basename(sys.executable).startswith("python") |
||||
return _independent_package |
||||
|
||||
|
||||
def check_dev_mode(): |
||||
try: |
||||
if is_independent_package(): |
||||
return False |
||||
if not path.isdir(".git"): |
||||
return False |
||||
current_branch = subprocess.check_output(["git", "symbolic-ref", "--short", "HEAD"]).decode("utf8").rstrip() |
||||
if current_branch != "master": |
||||
return True |
||||
except Exception: |
||||
return False |
||||
|
||||
|
||||
def chdir_to_data_directory(): |
||||
if not is_independent_package(): |
||||
# Do nothing. |
||||
return |
||||
|
||||
import os |
||||
|
||||
import appdirs |
||||
app_data_dir: str = appdirs.user_data_dir("Hummingbot", "hummingbot.io") |
||||
os.makedirs(os.path.join(app_data_dir, "logs"), 0o711, exist_ok=True) |
||||
os.makedirs(os.path.join(app_data_dir, "conf"), 0o711, exist_ok=True) |
||||
os.makedirs(os.path.join(app_data_dir, "pmm_scripts"), 0o711, exist_ok=True) |
||||
os.makedirs(os.path.join(app_data_dir, "certs"), 0o711, exist_ok=True) |
||||
os.makedirs(os.path.join(app_data_dir, "scripts"), 0o711, exist_ok=True) |
||||
os.chdir(app_data_dir) |
||||
set_prefix_path(app_data_dir) |
||||
|
||||
|
||||
def get_logging_conf(conf_filename: str = 'hummingbot_logs.yml'): |
||||
import io |
||||
from os.path import join |
||||
from typing import Dict |
||||
|
||||
from ruamel.yaml import YAML |
||||
|
||||
file_path: str = join(prefix_path(), "conf", conf_filename) |
||||
yaml_parser: YAML = YAML() |
||||
if not path.exists(file_path): |
||||
return {} |
||||
with open(file_path) as fd: |
||||
yml_source: str = fd.read() |
||||
io_stream: io.StringIO = io.StringIO(yml_source) |
||||
config_dict: Dict = yaml_parser.load(io_stream) |
||||
return config_dict |
||||
|
||||
|
||||
def init_logging(conf_filename: str, |
||||
client_config_map: "_ClientConfigAdapter", |
||||
override_log_level: Optional[str] = None, |
||||
strategy_file_path: str = "hummingbot"): |
||||
import io |
||||
import logging.config |
||||
from os.path import join |
||||
from typing import Dict |
||||
|
||||
import pandas as pd |
||||
from ruamel.yaml import YAML |
||||
|
||||
from hummingbot.logger.struct_logger import StructLogger, StructLogRecord |
||||
global STRUCT_LOGGER_SET |
||||
if not STRUCT_LOGGER_SET: |
||||
logging.setLogRecordFactory(StructLogRecord) |
||||
logging.setLoggerClass(StructLogger) |
||||
STRUCT_LOGGER_SET = True |
||||
|
||||
# Do not raise exceptions during log handling |
||||
logging.raiseExceptions = False |
||||
|
||||
file_path: str = join(prefix_path(), "conf", conf_filename) |
||||
yaml_parser: YAML = YAML() |
||||
with open(file_path) as fd: |
||||
yml_source: str = fd.read() |
||||
yml_source = yml_source.replace("$PROJECT_DIR", prefix_path()) |
||||
yml_source = yml_source.replace("$DATETIME", pd.Timestamp.now().strftime("%Y-%m-%d-%H-%M-%S")) |
||||
yml_source = yml_source.replace("$STRATEGY_FILE_PATH", strategy_file_path.replace(".yml", "")) |
||||
io_stream: io.StringIO = io.StringIO(yml_source) |
||||
config_dict: Dict = yaml_parser.load(io_stream) |
||||
if override_log_level is not None and "loggers" in config_dict: |
||||
for logger in config_dict["loggers"]: |
||||
if logger in client_config_map.logger_override_whitelist: |
||||
config_dict["loggers"][logger]["level"] = override_log_level |
||||
logging.config.dictConfig(config_dict) |
||||
|
||||
|
||||
def get_strategy_list() -> List[str]: |
||||
""" |
||||
Search `hummingbot.strategy` folder for all available strategies |
||||
Automatically hide all strategies that starts with "dev" if on master branch |
||||
""" |
||||
try: |
||||
folder = path.realpath(path.join(__file__, "../strategy")) |
||||
# Only include valid directories |
||||
strategies = [d for d in listdir(folder) if path.isdir(path.join(folder, d)) and not d.startswith("__")] |
||||
on_dev_mode = check_dev_mode() |
||||
if not on_dev_mode: |
||||
strategies = [s for s in strategies if not s.startswith(DEV_STRATEGY_PREFIX)] |
||||
return sorted(strategies) |
||||
except Exception as e: |
||||
logging.getLogger().warning(f"Error getting strategy set: {str(e)}") |
||||
return [] |
@ -0,0 +1,26 @@ |
||||
import logging |
||||
|
||||
import pandas as pd |
||||
import decimal |
||||
|
||||
FLOAT_PRINTOUT_PRECISION = 8 |
||||
|
||||
|
||||
def format_decimal(n): |
||||
""" |
||||
Convert the given float to a string without scientific notation |
||||
""" |
||||
try: |
||||
with decimal.localcontext() as ctx: |
||||
if isinstance(n, float): |
||||
n = ctx.create_decimal(n) |
||||
if isinstance(n, decimal.Decimal): |
||||
n = round(n, FLOAT_PRINTOUT_PRECISION) |
||||
return format(n.normalize(), 'f') |
||||
else: |
||||
return str(n) |
||||
except Exception as e: |
||||
logging.getLogger().error(str(e)) |
||||
|
||||
|
||||
pd.options.display.float_format = lambda x: format_decimal(x) |
@ -0,0 +1,43 @@ |
||||
from .balance_command import BalanceCommand |
||||
from .config_command import ConfigCommand |
||||
from .connect_command import ConnectCommand |
||||
from .create_command import CreateCommand |
||||
from .exit_command import ExitCommand |
||||
from .export_command import ExportCommand |
||||
from .gateway_command import GatewayCommand |
||||
from .help_command import HelpCommand |
||||
from .history_command import HistoryCommand |
||||
from .import_command import ImportCommand |
||||
from .mqtt_command import MQTTCommand |
||||
from .order_book_command import OrderBookCommand |
||||
from .pmm_script_command import PMMScriptCommand |
||||
from .previous_strategy_command import PreviousCommand |
||||
from .rate_command import RateCommand |
||||
from .silly_commands import SillyCommands |
||||
from .start_command import StartCommand |
||||
from .status_command import StatusCommand |
||||
from .stop_command import StopCommand |
||||
from .ticker_command import TickerCommand |
||||
|
||||
__all__ = [ |
||||
BalanceCommand, |
||||
ConfigCommand, |
||||
ConnectCommand, |
||||
CreateCommand, |
||||
ExitCommand, |
||||
ExportCommand, |
||||
GatewayCommand, |
||||
HelpCommand, |
||||
HistoryCommand, |
||||
ImportCommand, |
||||
OrderBookCommand, |
||||
PMMScriptCommand, |
||||
PreviousCommand, |
||||
RateCommand, |
||||
SillyCommands, |
||||
StartCommand, |
||||
StatusCommand, |
||||
StopCommand, |
||||
TickerCommand, |
||||
MQTTCommand, |
||||
] |
@ -0,0 +1,219 @@ |
||||
import asyncio |
||||
import threading |
||||
from decimal import Decimal |
||||
from typing import TYPE_CHECKING, Dict, List |
||||
|
||||
import pandas as pd |
||||
|
||||
from hummingbot.client.config.config_validators import validate_decimal, validate_exchange |
||||
from hummingbot.client.performance import PerformanceMetrics |
||||
from hummingbot.client.settings import AllConnectorSettings |
||||
from hummingbot.core.rate_oracle.rate_oracle import RateOracle |
||||
from hummingbot.core.utils.async_utils import safe_ensure_future |
||||
from hummingbot.user.user_balances import UserBalances |
||||
|
||||
if TYPE_CHECKING: |
||||
from hummingbot.client.hummingbot_application import HummingbotApplication # noqa: F401 |
||||
|
||||
OPTIONS = [ |
||||
"limit", |
||||
"paper" |
||||
] |
||||
|
||||
|
||||
class BalanceCommand: |
||||
def balance(self, # type: HummingbotApplication |
||||
option: str = None, |
||||
args: List[str] = None |
||||
): |
||||
if threading.current_thread() != threading.main_thread(): |
||||
self.ev_loop.call_soon_threadsafe(self.balance, option, args) |
||||
return |
||||
|
||||
self.app.clear_input() |
||||
if option is None: |
||||
safe_ensure_future(self.show_balances()) |
||||
|
||||
elif option in OPTIONS: |
||||
if option == "limit": |
||||
balance_asset_limit = self.client_config_map.balance_asset_limit |
||||
if args is None or len(args) == 0: |
||||
safe_ensure_future(self.show_asset_limits()) |
||||
return |
||||
if len(args) != 3 or validate_exchange(args[0]) is not None or validate_decimal(args[2]) is not None: |
||||
self.notify("Error: Invalid command arguments") |
||||
self.notify_balance_limit_set() |
||||
return |
||||
exchange = args[0] |
||||
asset = args[1].upper() |
||||
amount = float(args[2]) |
||||
if balance_asset_limit.get(exchange) is None: |
||||
balance_asset_limit[exchange] = {} |
||||
if amount < 0 and asset in balance_asset_limit[exchange].keys(): |
||||
balance_asset_limit[exchange].pop(asset) |
||||
self.notify(f"Limit for {asset} on {exchange} exchange removed.") |
||||
elif amount >= 0: |
||||
balance_asset_limit[exchange][asset] = amount |
||||
self.notify(f"Limit for {asset} on {exchange} exchange set to {amount}") |
||||
self.save_client_config() |
||||
|
||||
elif option == "paper": |
||||
paper_balances = self.client_config_map.paper_trade.paper_trade_account_balance |
||||
if args is None or len(args) == 0: |
||||
safe_ensure_future(self.show_paper_account_balance()) |
||||
return |
||||
if len(args) != 2 or validate_decimal(args[1]) is not None: |
||||
self.notify("Error: Invalid command arguments") |
||||
self.notify_balance_paper_set() |
||||
return |
||||
asset = args[0].upper() |
||||
amount = float(args[1]) |
||||
paper_balances[asset] = amount |
||||
self.notify(f"Paper balance for {asset} token set to {amount}") |
||||
self.save_client_config() |
||||
|
||||
async def show_balances( |
||||
self # type: HummingbotApplication |
||||
): |
||||
global_token_symbol = self.client_config_map.global_token.global_token_symbol |
||||
total_col_name = f"Total ({global_token_symbol})" |
||||
sum_not_for_show_name = "sum_not_for_show" |
||||
self.notify("Updating balances, please wait...") |
||||
network_timeout = float(self.client_config_map.commands_timeout.other_commands_timeout) |
||||
try: |
||||
all_ex_bals = await asyncio.wait_for( |
||||
UserBalances.instance().all_balances_all_exchanges(self.client_config_map), network_timeout |
||||
) |
||||
except asyncio.TimeoutError: |
||||
self.notify("\nA network error prevented the balances to update. See logs for more details.") |
||||
raise |
||||
all_ex_avai_bals = UserBalances.instance().all_available_balances_all_exchanges() |
||||
|
||||
exchanges_total = 0 |
||||
|
||||
for exchange, bals in all_ex_bals.items(): |
||||
self.notify(f"\n{exchange}:") |
||||
df, allocated_total = await self.exchange_balances_extra_df(exchange, bals, all_ex_avai_bals.get(exchange, {})) |
||||
if df.empty: |
||||
self.notify("You have no balance on this exchange.") |
||||
else: |
||||
lines = [ |
||||
" " + line for line in df.drop(sum_not_for_show_name, axis=1).to_string(index=False).split("\n") |
||||
] |
||||
self.notify("\n".join(lines)) |
||||
self.notify(f"\n Total: {global_token_symbol} " |
||||
f"{PerformanceMetrics.smart_round(df[total_col_name].sum())}") |
||||
allocated_percentage = 0 |
||||
if df[sum_not_for_show_name].sum() != Decimal("0"): |
||||
allocated_percentage = allocated_total / df[sum_not_for_show_name].sum() |
||||
self.notify(f"Allocated: {allocated_percentage:.2%}") |
||||
exchanges_total += df[total_col_name].sum() |
||||
|
||||
self.notify(f"\n\nExchanges Total: {global_token_symbol} {exchanges_total:.0f} ") |
||||
|
||||
async def exchange_balances_extra_df(self, # type: HummingbotApplication |
||||
exchange: str, |
||||
ex_balances: Dict[str, Decimal], |
||||
ex_avai_balances: Dict[str, Decimal]): |
||||
conn_setting = AllConnectorSettings.get_connector_settings()[exchange] |
||||
global_token_symbol = self.client_config_map.global_token.global_token_symbol |
||||
total_col_name = f"Total ({global_token_symbol})" |
||||
allocated_total = Decimal("0") |
||||
rows = [] |
||||
for token, bal in ex_balances.items(): |
||||
avai = Decimal(ex_avai_balances.get(token.upper(), 0)) if ex_avai_balances is not None else Decimal(0) |
||||
# show zero balances if it is a gateway connector (the user manually |
||||
# chose to show those values with 'gateway connector-tokens') |
||||
if conn_setting.uses_gateway_generic_connector(): |
||||
if bal == Decimal(0): |
||||
allocated = "0%" |
||||
else: |
||||
allocated = f"{(bal - avai) / bal:.0%}" |
||||
else: |
||||
# the exchange is CEX. Only show balance if non-zero. |
||||
if bal == Decimal(0): |
||||
continue |
||||
allocated = f"{(bal - avai) / bal:.0%}" |
||||
|
||||
rate = await RateOracle.get_instance().get_rate(base_token=token) |
||||
rate = Decimal("0") if rate is None else rate |
||||
global_value = rate * bal |
||||
allocated_total += rate * (bal - avai) |
||||
rows.append({"Asset": token.upper(), |
||||
"Total": round(bal, 4), |
||||
total_col_name: PerformanceMetrics.smart_round(global_value), |
||||
"sum_not_for_show": global_value, |
||||
"Allocated": allocated, |
||||
}) |
||||
df = pd.DataFrame(data=rows, columns=["Asset", "Total", total_col_name, "sum_not_for_show", "Allocated"]) |
||||
df.sort_values(by=["Asset"], inplace=True) |
||||
return df, allocated_total |
||||
|
||||
async def asset_limits_df(self, |
||||
asset_limit_conf: Dict[str, str]): |
||||
rows = [] |
||||
for token, amount in asset_limit_conf.items(): |
||||
rows.append({"Asset": token, "Limit": round(Decimal(amount), 4)}) |
||||
|
||||
df = pd.DataFrame(data=rows, columns=["Asset", "Limit"]) |
||||
df.sort_values(by=["Asset"], inplace=True) |
||||
return df |
||||
|
||||
async def show_asset_limits( |
||||
self # type: HummingbotApplication |
||||
): |
||||
exchange_limit_conf = self.client_config_map.balance_asset_limit |
||||
|
||||
if not any(list(exchange_limit_conf.values())): |
||||
self.notify("You have not set any limits.") |
||||
self.notify_balance_limit_set() |
||||
return |
||||
|
||||
self.notify("Balance Limits per exchange...") |
||||
|
||||
for exchange, asset_limit_config in exchange_limit_conf.items(): |
||||
if asset_limit_config is None: |
||||
continue |
||||
|
||||
self.notify(f"\n{exchange}") |
||||
df = await self.asset_limits_df(asset_limit_config) |
||||
if df.empty: |
||||
self.notify("You have no limits on this exchange.") |
||||
else: |
||||
lines = [" " + line for line in df.to_string(index=False).split("\n")] |
||||
self.notify("\n".join(lines)) |
||||
self.notify("\n") |
||||
return |
||||
|
||||
async def paper_acccount_balance_df(self, paper_balances: Dict[str, Decimal]): |
||||
rows = [] |
||||
for asset, balance in paper_balances.items(): |
||||
rows.append({"Asset": asset, "Balance": round(Decimal(str(balance)), 4)}) |
||||
df = pd.DataFrame(data=rows, columns=["Asset", "Balance"]) |
||||
df.sort_values(by=["Asset"], inplace=True) |
||||
return df |
||||
|
||||
def notify_balance_limit_set(self): |
||||
self.notify("To set a balance limit (how much the bot can use): \n" |
||||
" balance limit [EXCHANGE] [ASSET] [AMOUNT]\n" |
||||
"e.g. balance limit binance BTC 0.1") |
||||
|
||||
def notify_balance_paper_set(self): |
||||
self.notify("To set a paper account balance: \n" |
||||
" balance paper [ASSET] [AMOUNT]\n" |
||||
"e.g. balance paper BTC 0.1") |
||||
|
||||
async def show_paper_account_balance( |
||||
self # type: HummingbotApplication |
||||
): |
||||
paper_balances = self.client_config_map.paper_trade.paper_trade_account_balance |
||||
if not paper_balances: |
||||
self.notify("You have not set any paper account balance.") |
||||
self.notify_balance_paper_set() |
||||
return |
||||
self.notify("Paper account balances:") |
||||
df = await self.paper_acccount_balance_df(paper_balances) |
||||
lines = [" " + line for line in df.to_string(index=False).split("\n")] |
||||
self.notify("\n".join(lines)) |
||||
self.notify("\n") |
||||
return |
@ -0,0 +1,488 @@ |
||||
import asyncio |
||||
from decimal import Decimal |
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union |
||||
|
||||
import pandas as pd |
||||
from prompt_toolkit.utils import is_windows |
||||
|
||||
from hummingbot.client.command.gateway_command import GatewayCommand |
||||
from hummingbot.client.config.config_helpers import ( |
||||
ClientConfigAdapter, |
||||
missing_required_configs_legacy, |
||||
save_to_yml, |
||||
save_to_yml_legacy, |
||||
) |
||||
from hummingbot.client.config.config_validators import validate_bool, validate_decimal |
||||
from hummingbot.client.config.config_var import ConfigVar |
||||
from hummingbot.client.config.security import Security |
||||
from hummingbot.client.config.strategy_config_data_types import BaseTradingStrategyConfigMap |
||||
from hummingbot.client.settings import CLIENT_CONFIG_PATH, STRATEGIES_CONF_DIR_PATH |
||||
from hummingbot.client.ui.interface_utils import format_df_for_printout |
||||
from hummingbot.client.ui.style import load_style |
||||
from hummingbot.connector.utils import split_hb_trading_pair |
||||
from hummingbot.core.utils import map_df_to_str |
||||
from hummingbot.core.utils.async_utils import safe_ensure_future |
||||
from hummingbot.model.inventory_cost import InventoryCost |
||||
from hummingbot.strategy.perpetual_market_making import PerpetualMarketMakingStrategy |
||||
from hummingbot.strategy.pure_market_making import PureMarketMakingStrategy |
||||
from hummingbot.user.user_balances import UserBalances |
||||
|
||||
if TYPE_CHECKING: |
||||
from hummingbot.client.hummingbot_application import HummingbotApplication # noqa: F401 |
||||
|
||||
no_restart_pmm_keys_in_percentage = ["bid_spread", "ask_spread", "order_level_spread", "inventory_target_base_pct"] |
||||
no_restart_pmm_keys = ["order_amount", |
||||
"order_levels", |
||||
"filled_order_delay", |
||||
"inventory_skew_enabled", |
||||
"inventory_range_multiplier", |
||||
"price_ceiling", |
||||
"price_floor", |
||||
"moving_price_band_enabled", |
||||
"price_ceiling_pct", |
||||
"price_floor_pct", |
||||
"price_band_refresh_time" |
||||
"order_optimization_enabled", |
||||
"bid_order_optimization_depth", |
||||
"ask_order_optimization_depth" |
||||
] |
||||
client_configs_to_display = ["autofill_import", |
||||
"kill_switch_mode", |
||||
"kill_switch_rate", |
||||
"telegram_mode", |
||||
"telegram_token", |
||||
"telegram_chat_id", |
||||
"mqtt_bridge", |
||||
"mqtt_host", |
||||
"mqtt_port", |
||||
"mqtt_namespace", |
||||
"mqtt_username", |
||||
"mqtt_password", |
||||
"mqtt_ssl", |
||||
"mqtt_logger", |
||||
"mqtt_notifier", |
||||
"mqtt_commands", |
||||
"mqtt_events", |
||||
"mqtt_external_events", |
||||
"mqtt_autostart", |
||||
"instance_id", |
||||
"send_error_logs", |
||||
"pmm_script_mode", |
||||
"pmm_script_file_path", |
||||
"ethereum_chain_name", |
||||
"gateway", |
||||
"gateway_api_host", |
||||
"gateway_api_port", |
||||
"rate_oracle_source", |
||||
"extra_tokens", |
||||
"fetch_pairs_from_all_exchanges", |
||||
"global_token", |
||||
"global_token_name", |
||||
"global_token_symbol", |
||||
"rate_limits_share_pct", |
||||
"commands_timeout", |
||||
"create_command_timeout", |
||||
"other_commands_timeout", |
||||
"tables_format", |
||||
"tick_size", |
||||
"market_data_collection", |
||||
"market_data_collection_enabled", |
||||
"market_data_collection_interval", |
||||
"market_data_collection_depth", |
||||
] |
||||
color_settings_to_display = ["top_pane", |
||||
"bottom_pane", |
||||
"output_pane", |
||||
"input_pane", |
||||
"logs_pane", |
||||
"terminal_primary"] |
||||
columns = ["Key", "Value"] |
||||
|
||||
|
||||
class ConfigCommand: |
||||
def config(self, # type: HummingbotApplication |
||||
key: str = None, |
||||
value: str = None): |
||||
self.app.clear_input() |
||||
if key is None: |
||||
self.list_configs() |
||||
return |
||||
else: |
||||
if key not in self.configurable_keys(): |
||||
self.notify("Invalid key, please choose from the list.") |
||||
return |
||||
safe_ensure_future(self._config_single_key(key, value), loop=self.ev_loop) |
||||
|
||||
def list_configs(self, # type: HummingbotApplication |
||||
): |
||||
self.list_client_configs() |
||||
self.list_strategy_configs() |
||||
|
||||
def list_client_configs( |
||||
self, # type: HummingbotApplication |
||||
): |
||||
data = self.build_model_df_data(self.client_config_map, to_print=client_configs_to_display) |
||||
df = map_df_to_str(pd.DataFrame(data=data, columns=columns)) |
||||
self.notify("\nGlobal Configurations:") |
||||
lines = [" " + line for line in format_df_for_printout( |
||||
df, |
||||
table_format=self.client_config_map.tables_format, |
||||
max_col_width=50).split("\n")] |
||||
self.notify("\n".join(lines)) |
||||
|
||||
data = self.build_model_df_data(self.client_config_map, to_print=color_settings_to_display) |
||||
df = map_df_to_str(pd.DataFrame(data=data, columns=columns)) |
||||
self.notify("\nColor Settings:") |
||||
lines = [" " + line for line in format_df_for_printout( |
||||
df, |
||||
table_format=self.client_config_map.tables_format, |
||||
max_col_width=50).split("\n")] |
||||
self.notify("\n".join(lines)) |
||||
|
||||
def list_strategy_configs( |
||||
self, # type: HummingbotApplication |
||||
): |
||||
if self.strategy_name is not None: |
||||
config_map = self.strategy_config_map |
||||
data = self.build_df_data_from_config_map(config_map) |
||||
df = map_df_to_str(pd.DataFrame(data=data, columns=columns)) |
||||
self.notify("\nStrategy Configurations:") |
||||
lines = [" " + line for line in format_df_for_printout( |
||||
df, |
||||
table_format=self.client_config_map.tables_format, |
||||
max_col_width=50).split("\n")] |
||||
self.notify("\n".join(lines)) |
||||
|
||||
def build_df_data_from_config_map( |
||||
self, # type: HummingbotApplication |
||||
config_map: Union[ClientConfigAdapter, Dict[str, ConfigVar]] |
||||
) -> List[Tuple[str, Any]]: |
||||
if isinstance(config_map, ClientConfigAdapter): |
||||
data = self.build_model_df_data(config_map) |
||||
else: # legacy |
||||
data = [[cv.printable_key or cv.key, cv.value] for cv in self.strategy_config_map.values() if |
||||
not cv.is_secure] |
||||
return data |
||||
|
||||
@staticmethod |
||||
def build_model_df_data( |
||||
config_map: ClientConfigAdapter, to_print: Optional[List[str]] = None |
||||
) -> List[Tuple[str, Any]]: |
||||
model_data = [] |
||||
for traversal_item in config_map.traverse(): |
||||
if to_print is not None and traversal_item.attr not in to_print: |
||||
continue |
||||
attr_printout = ( |
||||
" " * (traversal_item.depth - 1) |
||||
+ (u"\u221F " if not is_windows() else " ") |
||||
+ traversal_item.attr |
||||
) if traversal_item.depth else traversal_item.attr |
||||
model_data.append((attr_printout, traversal_item.printable_value)) |
||||
return model_data |
||||
|
||||
def configurable_keys(self, # type: HummingbotApplication |
||||
) -> List[str]: |
||||
""" |
||||
Returns a list of configurable keys - using config command, excluding exchanges api keys |
||||
as they are set from connect command. |
||||
""" |
||||
keys = [ |
||||
traversal_item.config_path |
||||
for traversal_item in self.client_config_map.traverse() |
||||
if (traversal_item.client_field_data is not None and traversal_item.client_field_data.prompt is not None) |
||||
] |
||||
if self.strategy_config_map is not None: |
||||
if isinstance(self.strategy_config_map, ClientConfigAdapter): |
||||
keys.extend([ |
||||
traversal_item.config_path |
||||
for traversal_item in self.strategy_config_map.traverse() |
||||
if (traversal_item.client_field_data is not None |
||||
and traversal_item.client_field_data.prompt is not None) |
||||
]) |
||||
else: # legacy |
||||
keys.extend( |
||||
[c.key for c in self.strategy_config_map.values() if c.prompt is not None and c.key != 'strategy']) |
||||
return keys |
||||
|
||||
async def check_password(self, # type: HummingbotApplication |
||||
): |
||||
password = await self.app.prompt(prompt="Enter your password >>> ", is_password=True) |
||||
if password != Security.secrets_manager.password.get_secret_value(): |
||||
self.notify("Invalid password, please try again.") |
||||
return False |
||||
else: |
||||
return True |
||||
|
||||
# Make this function static so unit testing can be performed. |
||||
@staticmethod |
||||
def update_running_mm(mm_strategy, key: str, new_value: Any): |
||||
if key in no_restart_pmm_keys_in_percentage: |
||||
setattr(mm_strategy, key, new_value / Decimal("100")) |
||||
return True |
||||
elif key in no_restart_pmm_keys: |
||||
setattr(mm_strategy, key, new_value) |
||||
return True |
||||
return False |
||||
|
||||
async def _config_single_key(self, # type: HummingbotApplication |
||||
key: str, |
||||
input_value): |
||||
""" |
||||
Configure a single variable only. |
||||
Prompt the user to finish all configurations if there are remaining empty configs at the end. |
||||
""" |
||||
|
||||
self.placeholder_mode = True |
||||
self.app.hide_input = True |
||||
|
||||
try: |
||||
if ( |
||||
not isinstance(self.strategy_config_map, (type(None), ClientConfigAdapter)) |
||||
and key in self.strategy_config_map |
||||
): |
||||
await self._config_single_key_legacy(key, input_value) |
||||
else: |
||||
client_config_key = key in self.client_config_map.config_paths() |
||||
if client_config_key: |
||||
config_map = self.client_config_map |
||||
file_path = CLIENT_CONFIG_PATH |
||||
elif self.strategy is not None: |
||||
self.notify("Configuring the strategy while it is running is not currently supported.") |
||||
return |
||||
else: |
||||
config_map = self.strategy_config_map |
||||
if self.strategy_file_name is not None: |
||||
file_path = STRATEGIES_CONF_DIR_PATH / self.strategy_file_name |
||||
else: |
||||
self.notify("Strategy file name is not configured.") |
||||
return |
||||
|
||||
if input_value is None: |
||||
self.notify("Please follow the prompt to complete configurations: ") |
||||
if key == "inventory_target_base_pct": |
||||
await self.asset_ratio_maintenance_prompt(config_map, input_value) |
||||
elif key == "inventory_price": |
||||
await self.inventory_price_prompt(config_map, input_value) |
||||
else: |
||||
await self.prompt_a_config(config_map, key, input_value, assign_default=False) |
||||
if self.app.to_stop_config: |
||||
self.app.to_stop_config = False |
||||
return |
||||
save_to_yml(file_path, config_map) |
||||
self.notify("\nNew configuration saved.") |
||||
if client_config_key: |
||||
self.list_client_configs() |
||||
else: |
||||
self.list_strategy_configs() |
||||
self.app.style = load_style(self.client_config_map) |
||||
except asyncio.TimeoutError: |
||||
self.logger().error("Prompt timeout") |
||||
except Exception as err: |
||||
self.logger().error(str(err), exc_info=True) |
||||
finally: |
||||
self.app.hide_input = False |
||||
self.placeholder_mode = False |
||||
self.app.change_prompt(prompt=">>> ") |
||||
|
||||
async def _config_single_key_legacy( |
||||
self, # type: HummingbotApplication |
||||
key: str, |
||||
input_value: Any, |
||||
): # pragma: no cover |
||||
config_var, config_map, file_path = None, None, None |
||||
if self.strategy_config_map is not None and key in self.strategy_config_map: |
||||
config_map = self.strategy_config_map |
||||
file_path = STRATEGIES_CONF_DIR_PATH / self.strategy_file_name |
||||
config_var = config_map[key] |
||||
if config_var.key == "strategy": |
||||
self.notify("You cannot change the strategy of a loaded configuration.") |
||||
self.notify("Please use 'import xxx.yml' or 'create' to configure the intended strategy") |
||||
return |
||||
if input_value is None: |
||||
self.notify("Please follow the prompt to complete configurations: ") |
||||
if config_var.key == "inventory_target_base_pct": |
||||
await self.asset_ratio_maintenance_prompt_legacy(config_map, input_value) |
||||
elif config_var.key == "inventory_price": |
||||
await self.inventory_price_prompt_legacy(config_map, input_value) |
||||
else: |
||||
await self.prompt_a_config_legacy(config_var, input_value=input_value, assign_default=False) |
||||
if self.app.to_stop_config: |
||||
self.app.to_stop_config = False |
||||
return |
||||
missings = missing_required_configs_legacy(config_map) |
||||
if missings: |
||||
self.notify("\nThere are other configuration required, please follow the prompt to complete them.") |
||||
missings = await self._prompt_missing_configs(config_map) |
||||
save_to_yml_legacy(str(file_path), config_map) |
||||
self.notify("\nNew configuration saved:") |
||||
self.notify(f"{key}: {str(config_var.value)}") |
||||
self.app.app.style = load_style(self.client_config_map) |
||||
for config in missings: |
||||
self.notify(f"{config.key}: {str(config.value)}") |
||||
if ( |
||||
isinstance(self.strategy, PureMarketMakingStrategy) or |
||||
isinstance(self.strategy, PerpetualMarketMakingStrategy) |
||||
): |
||||
updated = ConfigCommand.update_running_mm(self.strategy, key, config_var.value) |
||||
if updated: |
||||
self.notify(f"\nThe current {self.strategy_name} strategy has been updated " |
||||
f"to reflect the new configuration.") |
||||
|
||||
async def _prompt_missing_configs(self, # type: HummingbotApplication |
||||
config_map): |
||||
missings = missing_required_configs_legacy(config_map) |
||||
for config in missings: |
||||
await self.prompt_a_config_legacy(config) |
||||
if self.app.to_stop_config: |
||||
self.app.to_stop_config = False |
||||
return |
||||
if missing_required_configs_legacy(config_map): |
||||
return missings + (await self._prompt_missing_configs(config_map)) |
||||
return missings |
||||
|
||||
async def asset_ratio_maintenance_prompt( |
||||
self, # type: HummingbotApplication |
||||
config_map: BaseTradingStrategyConfigMap, |
||||
input_value: Any = None, |
||||
): # pragma: no cover |
||||
if input_value: |
||||
config_map.inventory_target_base_pct = input_value |
||||
else: |
||||
exchange = config_map.exchange |
||||
market = config_map.market |
||||
base, quote = split_hb_trading_pair(market) |
||||
if UserBalances.instance().is_gateway_market(exchange): |
||||
balances = await GatewayCommand.balance(self, exchange, config_map, base, quote) |
||||
else: |
||||
balances = await UserBalances.instance().balances(exchange, config_map, base, quote) |
||||
if balances is None: |
||||
return |
||||
base_ratio = await UserBalances.base_amount_ratio(exchange, market, balances) |
||||
if base_ratio is None: |
||||
return |
||||
base_ratio = round(base_ratio, 3) |
||||
quote_ratio = 1 - base_ratio |
||||
|
||||
cvar = ConfigVar(key="temp_config", |
||||
prompt=f"On {exchange}, you have {balances.get(base, 0):.4f} {base} and " |
||||
f"{balances.get(quote, 0):.4f} {quote}. By market value, " |
||||
f"your current inventory split is {base_ratio:.1%} {base} " |
||||
f"and {quote_ratio:.1%} {quote}." |
||||
f" Would you like to keep this ratio? (Yes/No) >>> ", |
||||
required_if=lambda: True, |
||||
type_str="bool", |
||||
validator=validate_bool) |
||||
await self.prompt_a_config_legacy(cvar) |
||||
if cvar.value: |
||||
config_map.inventory_target_base_pct = round(base_ratio * Decimal('100'), 1) |
||||
elif self.app.to_stop_config: |
||||
self.app.to_stop_config = False |
||||
else: |
||||
await self.prompt_a_config(config_map, config="inventory_target_base_pct") |
||||
|
||||
async def asset_ratio_maintenance_prompt_legacy( |
||||
self, # type: HummingbotApplication |
||||
config_map, |
||||
input_value=None, |
||||
): |
||||
if input_value: |
||||
config_map['inventory_target_base_pct'].value = Decimal(input_value) |
||||
else: |
||||
exchange = config_map['exchange'].value |
||||
market = config_map["market"].value |
||||
base, quote = market.split("-") |
||||
if UserBalances.instance().is_gateway_market(exchange): |
||||
balances = await GatewayCommand.balance(self, exchange, config_map, base, quote) |
||||
else: |
||||
balances = await UserBalances.instance().balances(exchange, config_map, base, quote) |
||||
if balances is None: |
||||
return |
||||
base_ratio = await UserBalances.base_amount_ratio(exchange, market, balances) |
||||
if base_ratio is None: |
||||
return |
||||
base_ratio = round(base_ratio, 3) |
||||
quote_ratio = 1 - base_ratio |
||||
base, quote = config_map["market"].value.split("-") |
||||
|
||||
cvar = ConfigVar(key="temp_config", |
||||
prompt=f"On {exchange}, you have {balances.get(base, 0):.4f} {base} and " |
||||
f"{balances.get(quote, 0):.4f} {quote}. By market value, " |
||||
f"your current inventory split is {base_ratio:.1%} {base} " |
||||
f"and {quote_ratio:.1%} {quote}." |
||||
f" Would you like to keep this ratio? (Yes/No) >>> ", |
||||
required_if=lambda: True, |
||||
type_str="bool", |
||||
validator=validate_bool) |
||||
await self.prompt_a_config_legacy(cvar) |
||||
if cvar.value: |
||||
config_map['inventory_target_base_pct'].value = round(base_ratio * Decimal('100'), 1) |
||||
else: |
||||
if self.app.to_stop_config: |
||||
self.app.to_stop_config = False |
||||
return |
||||
await self.prompt_a_config_legacy(config_map["inventory_target_base_pct"]) |
||||
|
||||
async def inventory_price_prompt( |
||||
self, # type: HummingbotApplication |
||||
model: BaseTradingStrategyConfigMap, |
||||
input_value=None, |
||||
): |
||||
""" |
||||
Not currently used. |
||||
""" |
||||
raise NotImplementedError |
||||
|
||||
async def inventory_price_prompt_legacy( |
||||
self, # type: HummingbotApplication |
||||
config_map, |
||||
input_value=None, |
||||
): |
||||
key = "inventory_price" |
||||
if input_value: |
||||
config_map[key].value = Decimal(input_value) |
||||
else: |
||||
exchange = config_map["exchange"].value |
||||
market = config_map["market"].value |
||||
base_asset, quote_asset = market.split("-") |
||||
|
||||
if exchange.endswith("paper_trade"): |
||||
balances = self.client_config_map.paper_trade.paper_trade_account_balance |
||||
elif UserBalances.instance().is_gateway_market(exchange): |
||||
balances = await GatewayCommand.balance(self, exchange, config_map, base_asset, quote_asset) |
||||
else: |
||||
balances = await UserBalances.instance().balances( |
||||
exchange, base_asset, quote_asset |
||||
) |
||||
if balances.get(base_asset) is None: |
||||
return |
||||
|
||||
cvar = ConfigVar( |
||||
key="temp_config", |
||||
prompt=f"On {exchange}, you have {balances[base_asset]:.4f} {base_asset}. " |
||||
f"What was the price for this amount in {quote_asset}? >>> ", |
||||
required_if=lambda: True, |
||||
type_str="decimal", |
||||
validator=lambda v: validate_decimal( |
||||
v, min_value=Decimal("0"), inclusive=True |
||||
), |
||||
) |
||||
await self.prompt_a_config_legacy(cvar) |
||||
config_map[key].value = cvar.value |
||||
|
||||
try: |
||||
quote_volume = balances[base_asset] * cvar.value |
||||
except TypeError: |
||||
# TypeError: unsupported operand type(s) for *: 'decimal.Decimal' and 'NoneType' - bad input / no input |
||||
self.notify("Inventory price not updated due to bad input") |
||||
return |
||||
|
||||
with self.trade_fill_db.get_new_session() as session: |
||||
with session.begin(): |
||||
InventoryCost.add_volume( |
||||
session, |
||||
base_asset=base_asset, |
||||
quote_asset=quote_asset, |
||||
base_volume=balances[base_asset], |
||||
quote_volume=quote_volume, |
||||
overwrite=True, |
||||
) |
@ -0,0 +1,151 @@ |
||||
import asyncio |
||||
from typing import TYPE_CHECKING, Dict, Optional |
||||
|
||||
import pandas as pd |
||||
|
||||
from hummingbot.client.config.config_helpers import ClientConfigAdapter |
||||
from hummingbot.client.config.security import Security |
||||
from hummingbot.client.settings import AllConnectorSettings |
||||
from hummingbot.client.ui.interface_utils import format_df_for_printout |
||||
from hummingbot.connector.connector_status import get_connector_status |
||||
from hummingbot.core.utils.async_utils import safe_ensure_future |
||||
from hummingbot.core.utils.trading_pair_fetcher import TradingPairFetcher |
||||
from hummingbot.user.user_balances import UserBalances |
||||
|
||||
if TYPE_CHECKING: |
||||
from hummingbot.client.hummingbot_application import HummingbotApplication # noqa: F401 |
||||
|
||||
OPTIONS = {cs.name for cs in AllConnectorSettings.get_connector_settings().values() |
||||
if not cs.use_ethereum_wallet and not cs.uses_gateway_generic_connector() if cs.name != "probit_kr"} |
||||
|
||||
|
||||
class ConnectCommand: |
||||
def connect(self, # type: HummingbotApplication |
||||
option: str): |
||||
if option is None: |
||||
safe_ensure_future(self.show_connections()) |
||||
else: |
||||
safe_ensure_future(self.connect_exchange(option)) |
||||
|
||||
async def connect_exchange(self, # type: HummingbotApplication |
||||
connector_name): |
||||
# instruct users to use gateway connect if connector is a gateway connector |
||||
if AllConnectorSettings.get_connector_settings()[connector_name].uses_gateway_generic_connector(): |
||||
self.notify("This is a gateway connector. Use `gateway connect` command instead.") |
||||
return |
||||
|
||||
self.app.clear_input() |
||||
self.placeholder_mode = True |
||||
self.app.hide_input = True |
||||
if connector_name == "kraken": |
||||
self.notify("Reminder: Please ensure your Kraken API Key Nonce Window is at least 10.") |
||||
connector_config = ClientConfigAdapter(AllConnectorSettings.get_connector_config_keys(connector_name)) |
||||
if Security.connector_config_file_exists(connector_name): |
||||
await Security.wait_til_decryption_done() |
||||
api_key_config = [ |
||||
c.printable_value for c in connector_config.traverse(secure=False) if "api_key" in c.attr |
||||
] |
||||
if api_key_config: |
||||
api_key = api_key_config[0] |
||||
prompt = ( |
||||
f"Would you like to replace your existing {connector_name} API key {api_key} (Yes/No)? >>> " |
||||
) |
||||
else: |
||||
prompt = f"Would you like to replace your existing {connector_name} key (Yes/No)? >>> " |
||||
answer = await self.app.prompt(prompt=prompt) |
||||
if self.app.to_stop_config: |
||||
self.app.to_stop_config = False |
||||
return |
||||
if answer.lower() in ("yes", "y"): |
||||
previous_keys = Security.api_keys(connector_name) |
||||
await self._perform_connect(connector_config, previous_keys) |
||||
else: |
||||
await self._perform_connect(connector_config) |
||||
self.placeholder_mode = False |
||||
self.app.hide_input = False |
||||
self.app.change_prompt(prompt=">>> ") |
||||
|
||||
async def show_connections(self # type: HummingbotApplication |
||||
): |
||||
self.notify("\nTesting connections, please wait...") |
||||
df, failed_msgs = await self.connection_df() |
||||
lines = [" " + line for line in format_df_for_printout( |
||||
df, |
||||
table_format=self.client_config_map.tables_format).split("\n")] |
||||
if failed_msgs: |
||||
lines.append("\nFailed connections:") |
||||
lines.extend([" " + k + ": " + v for k, v in failed_msgs.items()]) |
||||
self.notify("\n".join(lines)) |
||||
|
||||
async def connection_df(self # type: HummingbotApplication |
||||
): |
||||
await Security.wait_til_decryption_done() |
||||
columns = ["Exchange", " Keys Added", " Keys Confirmed", " Tier"] |
||||
data = [] |
||||
failed_msgs = {} |
||||
network_timeout = float(self.client_config_map.commands_timeout.other_commands_timeout) |
||||
try: |
||||
err_msgs = await asyncio.wait_for( |
||||
UserBalances.instance().update_exchanges(self.client_config_map, reconnect=True), network_timeout |
||||
) |
||||
except asyncio.TimeoutError: |
||||
self.notify("\nA network error prevented the connection table to populate. See logs for more details.") |
||||
raise |
||||
for option in sorted(OPTIONS): |
||||
keys_added = "No" |
||||
keys_confirmed = "No" |
||||
status = get_connector_status(option) |
||||
|
||||
api_keys = ( |
||||
Security.api_keys(option).values() |
||||
if not UserBalances.instance().is_gateway_market(option) |
||||
else {} |
||||
) |
||||
if len(api_keys) > 0: |
||||
keys_added = "Yes" |
||||
err_msg = err_msgs.get(option) |
||||
if err_msg is not None: |
||||
failed_msgs[option] = err_msg |
||||
else: |
||||
keys_confirmed = "Yes" |
||||
data.append([option, keys_added, keys_confirmed, status]) |
||||
return pd.DataFrame(data=data, columns=columns), failed_msgs |
||||
|
||||
async def validate_n_connect_connector( |
||||
self, # type: HummingbotApplication |
||||
connector_name: str, |
||||
) -> Optional[str]: |
||||
await Security.wait_til_decryption_done() |
||||
api_keys = Security.api_keys(connector_name) |
||||
network_timeout = float(self.client_config_map.commands_timeout.other_commands_timeout) |
||||
try: |
||||
err_msg = await asyncio.wait_for( |
||||
UserBalances.instance().add_exchange(connector_name, self.client_config_map, **api_keys), |
||||
network_timeout, |
||||
) |
||||
except asyncio.TimeoutError: |
||||
self.notify( |
||||
"\nA network error prevented the connection to complete. See logs for more details.") |
||||
self.placeholder_mode = False |
||||
self.app.hide_input = False |
||||
self.app.change_prompt(prompt=">>> ") |
||||
raise |
||||
return err_msg |
||||
|
||||
async def _perform_connect(self, connector_config: ClientConfigAdapter, previous_keys: Optional[Dict] = None): |
||||
connector_name = connector_config.connector |
||||
original_config = connector_config.full_copy() |
||||
await self.prompt_for_model_config(connector_config) |
||||
self.app.change_prompt(prompt=">>> ") |
||||
if self.app.to_stop_config: |
||||
self.app.to_stop_config = False |
||||
return |
||||
Security.update_secure_config(connector_config) |
||||
err_msg = await self.validate_n_connect_connector(connector_name) |
||||
if err_msg is None: |
||||
self.notify(f"\nYou are now connected to {connector_name}.") |
||||
safe_ensure_future(TradingPairFetcher.get_instance(client_config_map=ClientConfigAdapter).fetch_all(client_config_map=ClientConfigAdapter)) |
||||
else: |
||||
self.notify(f"\nError: {err_msg}") |
||||
if previous_keys is not None: |
||||
Security.update_secure_config(original_config) |
@ -0,0 +1,257 @@ |
||||
import asyncio |
||||
import copy |
||||
import os |
||||
import shutil |
||||
from pathlib import Path |
||||
from typing import TYPE_CHECKING, Dict, Optional |
||||
|
||||
from hummingbot.client.config.config_helpers import ( |
||||
ClientConfigAdapter, |
||||
ConfigValidationError, |
||||
default_strategy_file_path, |
||||
format_config_file_name, |
||||
get_strategy_config_map, |
||||
get_strategy_template_path, |
||||
parse_config_default_to_text, |
||||
parse_cvar_value, |
||||
save_previous_strategy_value, |
||||
save_to_yml, |
||||
save_to_yml_legacy, |
||||
) |
||||
from hummingbot.client.config.config_var import ConfigVar |
||||
from hummingbot.client.config.strategy_config_data_types import BaseStrategyConfigMap |
||||
from hummingbot.client.settings import STRATEGIES_CONF_DIR_PATH, required_exchanges |
||||
from hummingbot.client.ui.completer import load_completer |
||||
from hummingbot.core.utils.async_utils import safe_ensure_future |
||||
|
||||
if TYPE_CHECKING: |
||||
from hummingbot.client.hummingbot_application import HummingbotApplication # noqa: F401 |
||||
|
||||
|
||||
class CreateCommand: |
||||
def create(self, # type: HummingbotApplication |
||||
file_name): |
||||
if file_name is not None: |
||||
file_name = format_config_file_name(file_name) |
||||
if (STRATEGIES_CONF_DIR_PATH / file_name).exists(): |
||||
self.notify(f"{file_name} already exists.") |
||||
return |
||||
|
||||
safe_ensure_future(self.prompt_for_configuration(file_name)) |
||||
|
||||
async def prompt_for_configuration( |
||||
self, # type: HummingbotApplication |
||||
file_name, |
||||
): |
||||
self.app.clear_input() |
||||
self.placeholder_mode = True |
||||
self.app.hide_input = True |
||||
required_exchanges.clear() |
||||
|
||||
strategy = await self.get_strategy_name() |
||||
|
||||
if self.app.to_stop_config: |
||||
return |
||||
|
||||
config_map = get_strategy_config_map(strategy) |
||||
self.notify(f"Please see https://docs.hummingbot.org/strategies/{strategy.replace('_', '-')}/ " |
||||
f"while setting up these below configuration.") |
||||
|
||||
if isinstance(config_map, ClientConfigAdapter): |
||||
await self.prompt_for_model_config(config_map) |
||||
if not self.app.to_stop_config: |
||||
file_name = await self.save_config_to_file(file_name, config_map) |
||||
elif config_map is not None: |
||||
file_name = await self.prompt_for_configuration_legacy(file_name, strategy, config_map) |
||||
else: |
||||
self.app.to_stop_config = True |
||||
|
||||
if self.app.to_stop_config: |
||||
return |
||||
|
||||
save_previous_strategy_value(file_name, self.client_config_map) |
||||
self.strategy_file_name = file_name |
||||
self.strategy_name = strategy |
||||
self.strategy_config_map = config_map |
||||
# Reload completer here otherwise the new file will not appear |
||||
self.app.input_field.completer = load_completer(self) |
||||
self.notify(f"A new config file has been created: {self.strategy_file_name}") |
||||
self.placeholder_mode = False |
||||
self.app.hide_input = False |
||||
|
||||
await self.verify_status() |
||||
|
||||
async def get_strategy_name( |
||||
self, # type: HummingbotApplication |
||||
) -> Optional[str]: |
||||
strategy = None |
||||
strategy_config = ClientConfigAdapter(BaseStrategyConfigMap.construct()) |
||||
await self.prompt_for_model_config(strategy_config) |
||||
if not self.app.to_stop_config: |
||||
strategy = strategy_config.strategy |
||||
return strategy |
||||
|
||||
async def prompt_for_model_config( |
||||
self, # type: HummingbotApplication |
||||
config_map: ClientConfigAdapter, |
||||
): |
||||
for key in config_map.keys(): |
||||
client_data = config_map.get_client_data(key) |
||||
if ( |
||||
client_data is not None |
||||
and (client_data.prompt_on_new or config_map.is_required(key)) |
||||
): |
||||
await self.prompt_a_config(config_map, key) |
||||
if self.app.to_stop_config: |
||||
break |
||||
|
||||
async def prompt_for_configuration_legacy( |
||||
self, # type: HummingbotApplication |
||||
file_name, |
||||
strategy: str, |
||||
config_map: Dict, |
||||
): |
||||
config_map_backup = copy.deepcopy(config_map) |
||||
# assign default values and reset those not required |
||||
for config in config_map.values(): |
||||
if config.required: |
||||
config.value = config.default |
||||
else: |
||||
config.value = None |
||||
for config in config_map.values(): |
||||
if config.prompt_on_new and config.required: |
||||
if not self.app.to_stop_config: |
||||
await self.prompt_a_config_legacy(config) |
||||
else: |
||||
break |
||||
else: |
||||
config.value = config.default |
||||
|
||||
if self.app.to_stop_config: |
||||
self.restore_config_legacy(config_map, config_map_backup) |
||||
self.app.set_text("") |
||||
return |
||||
|
||||
if file_name is None: |
||||
file_name = await self.prompt_new_file_name(strategy) |
||||
if self.app.to_stop_config: |
||||
self.restore_config_legacy(config_map, config_map_backup) |
||||
self.app.set_text("") |
||||
return |
||||
self.app.change_prompt(prompt=">>> ") |
||||
strategy_path = STRATEGIES_CONF_DIR_PATH / file_name |
||||
template = get_strategy_template_path(strategy) |
||||
shutil.copy(template, strategy_path) |
||||
save_to_yml_legacy(str(strategy_path), config_map) |
||||
return file_name |
||||
|
||||
async def prompt_a_config( |
||||
self, # type: HummingbotApplication |
||||
model: ClientConfigAdapter, |
||||
config: str, |
||||
input_value=None, |
||||
assign_default=True, |
||||
): |
||||
config_path = config.split(".") |
||||
while len(config_path) != 1: |
||||
sub_model_attr = config_path.pop(0) |
||||
model = getattr(model, sub_model_attr) |
||||
config = config_path[0] |
||||
if input_value is None: |
||||
prompt = await model.get_client_prompt(config) |
||||
if prompt is not None: |
||||
if assign_default: |
||||
default = model.get_default_str_repr(attr_name=config) |
||||
self.app.set_text(default) |
||||
prompt = f"{prompt} >>> " |
||||
client_data = model.get_client_data(config) |
||||
input_value = await self.app.prompt(prompt=prompt, is_password=client_data.is_secure) |
||||
|
||||
new_config_value = None |
||||
if not self.app.to_stop_config and input_value is not None: |
||||
try: |
||||
setattr(model, config, input_value) |
||||
new_config_value = getattr(model, config) |
||||
except ConfigValidationError as e: |
||||
self.notify(str(e)) |
||||
new_config_value = await self.prompt_a_config(model, config) |
||||
|
||||
if not self.app.to_stop_config and isinstance(new_config_value, ClientConfigAdapter): |
||||
await self.prompt_for_model_config(new_config_value) |
||||
|
||||
async def prompt_a_config_legacy( |
||||
self, # type: HummingbotApplication |
||||
config: ConfigVar, |
||||
input_value=None, |
||||
assign_default=True, |
||||
): |
||||
if config.key == "inventory_price": |
||||
await self.inventory_price_prompt_legacy(self.strategy_config_map, input_value) |
||||
return |
||||
if input_value is None: |
||||
if assign_default: |
||||
self.app.set_text(parse_config_default_to_text(config)) |
||||
prompt = await config.get_prompt() |
||||
input_value = await self.app.prompt(prompt=prompt, is_password=config.is_secure) |
||||
|
||||
if self.app.to_stop_config: |
||||
return |
||||
value = parse_cvar_value(config, input_value) |
||||
err_msg = await config.validate(input_value) |
||||
if err_msg is not None: |
||||
self.notify(err_msg) |
||||
config.value = None |
||||
await self.prompt_a_config_legacy(config) |
||||
else: |
||||
config.value = value |
||||
|
||||
async def save_config_to_file( |
||||
self, # type: HummingbotApplication |
||||
file_name: Optional[str], |
||||
config_map: ClientConfigAdapter, |
||||
) -> str: |
||||
if file_name is None: |
||||
file_name = await self.prompt_new_file_name(config_map.strategy) |
||||
if self.app.to_stop_config: |
||||
self.app.set_text("") |
||||
return |
||||
self.app.change_prompt(prompt=">>> ") |
||||
strategy_path = Path(STRATEGIES_CONF_DIR_PATH) / file_name |
||||
save_to_yml(strategy_path, config_map) |
||||
return file_name |
||||
|
||||
async def prompt_new_file_name(self, # type: HummingbotApplication |
||||
strategy): |
||||
file_name = default_strategy_file_path(strategy) |
||||
self.app.set_text(file_name) |
||||
input = await self.app.prompt(prompt="Enter a new file name for your configuration >>> ") |
||||
input = format_config_file_name(input) |
||||
file_path = os.path.join(STRATEGIES_CONF_DIR_PATH, input) |
||||
if input is None or input == "": |
||||
self.notify("Value is required.") |
||||
return await self.prompt_new_file_name(strategy) |
||||
elif os.path.exists(file_path): |
||||
self.notify(f"{input} file already exists, please enter a new name.") |
||||
return await self.prompt_new_file_name(strategy) |
||||
else: |
||||
return input |
||||
|
||||
async def verify_status( |
||||
self # type: HummingbotApplication |
||||
): |
||||
try: |
||||
timeout = float(self.client_config_map.commands_timeout.create_command_timeout) |
||||
all_status_go = await asyncio.wait_for(self.status_check_all(), timeout) |
||||
except asyncio.TimeoutError: |
||||
self.notify("\nA network error prevented the connection check to complete. See logs for more details.") |
||||
self.strategy_file_name = None |
||||
self.strategy_name = None |
||||
self.strategy_config = None |
||||
raise |
||||
if all_status_go: |
||||
self.notify("\nEnter \"start\" to start market making.") |
||||
|
||||
@staticmethod |
||||
def restore_config_legacy(config_map: Dict[str, ConfigVar], config_map_backup: Dict[str, ConfigVar]): |
||||
for key in config_map: |
||||
config_map[key] = config_map_backup[key] |
@ -0,0 +1,39 @@ |
||||
#!/usr/bin/env python |
||||
|
||||
import asyncio |
||||
from typing import TYPE_CHECKING |
||||
|
||||
from hummingbot.core.utils.async_utils import safe_ensure_future |
||||
|
||||
if TYPE_CHECKING: |
||||
from hummingbot.client.hummingbot_application import HummingbotApplication # noqa: F401 |
||||
|
||||
|
||||
class ExitCommand: |
||||
def exit(self, # type: HummingbotApplication |
||||
force: bool = False): |
||||
safe_ensure_future(self.exit_loop(force), loop=self.ev_loop) |
||||
|
||||
async def exit_loop(self, # type: HummingbotApplication |
||||
force: bool = False): |
||||
if self.strategy_task is not None and not self.strategy_task.cancelled(): |
||||
self.strategy_task.cancel() |
||||
if force is False and self._trading_required: |
||||
success = await self._cancel_outstanding_orders() |
||||
if not success: |
||||
self.notify('Wind down process terminated: Failed to cancel all outstanding orders. ' |
||||
'\nYou may need to manually cancel remaining orders by logging into your chosen exchanges' |
||||
'\n\nTo force exit the app, enter "exit -f"') |
||||
return |
||||
# Freeze screen 1 second for better UI |
||||
await asyncio.sleep(1) |
||||
|
||||
if self._gateway_monitor is not None: |
||||
self._gateway_monitor.stop() |
||||
|
||||
self.notify("Winding down notifiers...") |
||||
for notifier in self.notifiers: |
||||
notifier.stop() |
||||
|
||||
self.app.exit() |
||||
self.mqtt_stop() |