├── .github └── workflows │ └── main.yml ├── .gitignore ├── .prospector.yml ├── .readthedocs.yaml ├── Dockerfile ├── LICENSE ├── README.md ├── api-keys.json.example ├── broker_codes.hjson ├── changelog.md ├── configs ├── approved_coins_pumpdump.json ├── approved_coins_top20mcap.json ├── examples │ ├── all_approved.json │ └── top20mcap.json └── template.json ├── docker-compose.yml ├── docs ├── backtesting.md ├── configuration.md ├── hyperliquid_guide.md ├── images │ ├── initial_entry.png │ ├── logo.png │ ├── logo_pbot.svg │ ├── logo_pbot_dark.png │ ├── passivbot_grid_parameters.jpeg │ ├── pbot_logo_full.svg │ ├── pbot_logo_square.svg │ └── reentry.png ├── installation.md ├── live.md ├── optimizing.md ├── overrides │ └── main.html ├── risk_management.md └── tools.md ├── mkdocs.yml ├── notebooks ├── notebook_setup.py └── notes_backtest.ipynb ├── passivbot-rust ├── Cargo.lock ├── Cargo.toml └── src │ ├── backtest.rs │ ├── closes.rs │ ├── constants.rs │ ├── entries.rs │ ├── lib.rs │ ├── python.rs │ ├── types.rs │ └── utils.rs ├── requirements.txt ├── setup.py └── src ├── __init__.py ├── auto_profit_transfer.py ├── backtest.py ├── balance_overview.ipynb ├── correlation.ipynb ├── downloader.py ├── exchanges ├── binance.py ├── bitget.py ├── bybit.py ├── gateio.py ├── hyperliquid.py └── okx.py ├── forager.py ├── inspect_opt_results.py ├── interactive_plot.py ├── main.py ├── min_costs.ipynb ├── njit_clock.py ├── njit_funcs.py ├── njit_funcs_neat_grid.py ├── njit_funcs_recursive_grid.py ├── njit_multisymbol.py ├── notes_inspect_multiopt.ipynb ├── optimize.py ├── passivbot.py ├── plotting.py ├── procedures.py ├── pure_funcs.py └── tools ├── extract_best_config.py └── generate_mcap_list.py /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - run: 'true' 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Linux ### 2 | *~ 3 | 4 | # temporary files which can be created if a process still has a handle open of a deleted file 5 | .fuse_hidden* 6 | 7 | # KDE directory preferences 8 | .directory 9 | 10 | # Linux trash folder which might appear on any partition or disk 11 | .Trash-* 12 | 13 | # .nfs files are created when an open file is removed but is still being accessed 14 | .nfs* 15 | 16 | ### macOS ### 17 | # General 18 | .DS_Store 19 | .AppleDouble 20 | .LSOverride 21 | 22 | # Icon must end with two \r 23 | Icon 24 | 25 | 26 | # Thumbnails 27 | ._* 28 | 29 | # Files that might appear in the root of a volume 30 | .DocumentRevisions-V100 31 | .fseventsd 32 | .Spotlight-V100 33 | .TemporaryItems 34 | .Trashes 35 | .VolumeIcon.icns 36 | .com.apple.timemachine.donotpresent 37 | 38 | # Directories potentially created on remote AFP share 39 | .AppleDB 40 | .AppleDesktop 41 | Network Trash Folder 42 | Temporary Items 43 | .apdisk 44 | 45 | ### PyCharm ### 46 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 47 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 48 | 49 | # User-specific stuff 50 | .idea/**/workspace.xml 51 | .idea/**/tasks.xml 52 | .idea/**/usage.statistics.xml 53 | .idea/**/dictionaries 54 | .idea/**/shelf 55 | 56 | # Generated files 57 | .idea/**/contentModel.xml 58 | 59 | # Sensitive or high-churn files 60 | .idea/**/dataSources/ 61 | .idea/**/dataSources.ids 62 | .idea/**/dataSources.local.xml 63 | .idea/**/sqlDataSources.xml 64 | .idea/**/dynamic.xml 65 | .idea/**/uiDesigner.xml 66 | .idea/**/dbnavigator.xml 67 | 68 | # Gradle 69 | .idea/**/gradle.xml 70 | .idea/**/libraries 71 | 72 | # Gradle and Maven with auto-import 73 | # When using Gradle or Maven with auto-import, you should exclude module files, 74 | # since they will be recreated, and may cause churn. Uncomment if using 75 | # auto-import. 76 | # .idea/artifacts 77 | # .idea/compiler.xml 78 | # .idea/jarRepositories.xml 79 | # .idea/modules.xml 80 | # .idea/*.iml 81 | # .idea/modules 82 | # *.iml 83 | # *.ipr 84 | 85 | # CMake 86 | cmake-build-*/ 87 | 88 | # Mongo Explorer plugin 89 | .idea/**/mongoSettings.xml 90 | 91 | # File-based project format 92 | *.iws 93 | 94 | # IntelliJ 95 | out/ 96 | 97 | # mpeltonen/sbt-idea plugin 98 | .idea_modules/ 99 | 100 | # JIRA plugin 101 | atlassian-ide-plugin.xml 102 | 103 | # Cursive Clojure plugin 104 | .idea/replstate.xml 105 | 106 | # Crashlytics plugin (for Android Studio and IntelliJ) 107 | com_crashlytics_export_strings.xml 108 | crashlytics.properties 109 | crashlytics-build.properties 110 | fabric.properties 111 | 112 | # Editor-based Rest Client 113 | .idea/httpRequests 114 | 115 | # Android studio 3.1+ serialized cache file 116 | .idea/caches/build_file_checksums.ser 117 | 118 | ### PyCharm Patch ### 119 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 120 | 121 | # *.iml 122 | # modules.xml 123 | # .idea/misc.xml 124 | # *.ipr 125 | 126 | # Sonarlint plugin 127 | # https://plugins.jetbrains.com/plugin/7973-sonarlint 128 | .idea/**/sonarlint/ 129 | 130 | # SonarQube Plugin 131 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin 132 | .idea/**/sonarIssues.xml 133 | 134 | # Markdown Navigator plugin 135 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced 136 | .idea/**/markdown-navigator.xml 137 | .idea/**/markdown-navigator-enh.xml 138 | .idea/**/markdown-navigator/ 139 | 140 | # Cache file creation bug 141 | # See https://youtrack.jetbrains.com/issue/JBR-2257 142 | .idea/$CACHE_FILE$ 143 | 144 | # CodeStream plugin 145 | # https://plugins.jetbrains.com/plugin/12206-codestream 146 | .idea/codestream.xml 147 | 148 | ### Python ### 149 | # Byte-compiled / optimized / DLL files 150 | __pycache__/ 151 | *.py[cod] 152 | *$py.class 153 | 154 | # C extensions 155 | *.so 156 | 157 | # Distribution / packaging 158 | .Python 159 | build/ 160 | develop-eggs/ 161 | dist/ 162 | downloads/ 163 | eggs/ 164 | .eggs/ 165 | lib/ 166 | lib64/ 167 | parts/ 168 | sdist/ 169 | var/ 170 | wheels/ 171 | pip-wheel-metadata/ 172 | share/python-wheels/ 173 | *.egg-info/ 174 | .installed.cfg 175 | *.egg 176 | MANIFEST 177 | 178 | # PyCharm 179 | .idea/* 180 | 181 | # PyInstaller 182 | # Usually these files are written by a python script from a template 183 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 184 | *.manifest 185 | *.spec 186 | 187 | # Installer logs 188 | pip-log.txt 189 | pip-delete-this-directory.txt 190 | 191 | # Unit test / coverage reports 192 | htmlcov/ 193 | .tox/ 194 | .nox/ 195 | .coverage 196 | .coverage.* 197 | .cache 198 | nosetests.xml 199 | coverage.xml 200 | *.cover 201 | *.py,cover 202 | .hypothesis/ 203 | .pytest_cache/ 204 | pytestdebug.log 205 | 206 | # Translations 207 | *.mo 208 | *.pot 209 | 210 | # Django stuff: 211 | *.log 212 | local_settings.py 213 | db.sqlite3 214 | db.sqlite3-journal 215 | 216 | # Flask stuff: 217 | instance/ 218 | .webassets-cache 219 | 220 | # Scrapy stuff: 221 | .scrapy 222 | 223 | # Sphinx documentation 224 | docs/_build/ 225 | doc/_build/ 226 | 227 | # PyBuilder 228 | target/ 229 | 230 | # Jupyter Notebook 231 | .ipynb_checkpoints 232 | 233 | # IPython 234 | profile_default/ 235 | ipython_config.py 236 | 237 | # pyenv 238 | .python-version 239 | 240 | # pipenv 241 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 242 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 243 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 244 | # install all needed dependencies. 245 | #Pipfile.lock 246 | 247 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 248 | __pypackages__/ 249 | 250 | # Celery stuff 251 | celerybeat-schedule 252 | celerybeat.pid 253 | 254 | # SageMath parsed files 255 | *.sage.py 256 | 257 | # Environments 258 | .env 259 | .venv 260 | env/ 261 | venv/ 262 | ENV/ 263 | env.bak/ 264 | venv.bak/ 265 | pythonenv* 266 | 267 | # Spyder project settings 268 | .spyderproject 269 | .spyproject 270 | 271 | # Rope project settings 272 | .ropeproject 273 | 274 | # mkdocs documentation 275 | /site 276 | 277 | # mypy 278 | .mypy_cache/ 279 | .dmypy.json 280 | dmypy.json 281 | 282 | # Pyre type checker 283 | .pyre/ 284 | 285 | # pytype static type analyzer 286 | .pytype/ 287 | 288 | # profiling data 289 | .prof 290 | 291 | ### venv ### 292 | # Virtualenv 293 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 294 | [Bb]in 295 | [Ii]nclude 296 | [Ll]ib 297 | [Ll]ib64 298 | [Ll]ocal 299 | [Ss]cripts 300 | pyvenv.cfg 301 | pip-selfcheck.json 302 | 303 | ### VisualStudioCode ### 304 | .vscode/* 305 | !.vscode/tasks.json 306 | !.vscode/launch.json 307 | *.code-workspace 308 | 309 | ### VisualStudioCode Patch ### 310 | # Ignore all local history of files 311 | .history 312 | .ionide 313 | 314 | ### Windows ### 315 | # Windows thumbnail cache files 316 | Thumbs.db 317 | Thumbs.db:encryptable 318 | ehthumbs.db 319 | ehthumbs_vista.db 320 | 321 | # Dump file 322 | *.stackdump 323 | 324 | # Folder config file 325 | [Dd]esktop.ini 326 | 327 | # Recycle Bin used on file shares 328 | $RECYCLE.BIN/ 329 | 330 | # Windows Installer files 331 | *.cab 332 | *.msi 333 | *.msix 334 | *.msm 335 | *.msp 336 | 337 | # Windows shortcuts 338 | *.lnk 339 | 340 | # Backtest files 341 | backtests/ 342 | !config/optimize/default.hjson 343 | !config/backtest/default.hjson 344 | config/ 345 | historical_data/ 346 | results* 347 | 348 | # Config files 349 | api-keys.json 350 | !live_configs/binance_default.json 351 | !live_configs/bybit_default.json 352 | live_configs/* 353 | configs/live/multigrid.json 354 | # Reports 355 | reports/* 356 | -------------------------------------------------------------------------------- /.prospector.yml: -------------------------------------------------------------------------------- 1 | # Prospector config: https://prospector.landscape.io/en/master/usage.html 2 | strictness: medium # verylow, low, medium (the default), high and veryhigh 3 | test-warnings: true 4 | doc-warnings: false 5 | autodetect: true 6 | max-line-length: 140 7 | ignore-paths: 8 | - historical_data 9 | - backtest_results 10 | - logs 11 | 12 | pyflakes: 13 | run: true 14 | disable: 15 | - F405 # Currently disable to lower the warnings output. It's warn on import may be undefined, or defined from star imports (passiv bot is wildcard included from backtest and download) 16 | 17 | pep8: 18 | run: true 19 | full: false # Currently deactivated since it's not the more important 20 | disable: # https://pypi.org/project/pep8-naming/ 21 | - N803 # argument name should be lowercase 22 | - N806 # variable in function should be lowercase 23 | - N812 # lowercase imported as non lowercase 24 | pylint: 25 | run: true 26 | disable: # https://github.com/PyCQA/prospector/blob/9a8c0e365f1d9efb19797a8f45edb6f4ea6b8818/prospector/profiles/profiles/strictness_low.yaml#L7 27 | - too-many-locals 28 | - arguments-differ 29 | - no-else-return 30 | - inconsistent-return-statements 31 | - missing-module-docstring 32 | - too-many-arguments # Should not be disabled but not mandatory 33 | - consider-using-set-comprehension # Disable due to mathematical operations 34 | 35 | pep257: 36 | run: true 37 | disable: # https://pep257.readthedocs.io/en/latest/error_codes.html 38 | - D203 # 1 blank line required before class docstring 39 | - D212 # Multi-line docstring summary should start at the first line 40 | - D213 # Multi-line docstring summary should start at the second line 41 | - D107 # Missing docstring in __init__ 42 | - D205 # blank line required between summary line and description -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | mkdocs: 9 | configuration: mkdocs.yml 10 | 11 | formats: [] 12 | 13 | # Optionally set the version of Python and requirements required to build your docs 14 | python: 15 | version: 3.8 16 | install: 17 | - requirements: requirements.txt -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-slim-bullseye 2 | 3 | COPY ./ /passivbot/ 4 | 5 | WORKDIR /passivbot 6 | 7 | RUN pip install -r requirements_liveonly.txt 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Released freely without conditions. 2 | Anybody may copy, distribute, modify, use or misuse for commercial, 3 | non-commercial, educational or non-educational purposes, censor, 4 | claim as one's own or otherwise do whatever without permission from anybody. 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Passivbot](docs/images/pbot_logo_full.svg) 2 | 3 | # Trading bot running on Bybit, OKX, Bitget, GateIO, Binance and Hyperliquid 4 | 5 | :warning: **Used at one's own risk** :warning: 6 | 7 | v7.2.16 8 | 9 | 10 | ## Overview 11 | 12 | Passivbot is a cryptocurrency trading bot written in Python and Rust, intended to require minimal user intervention. 13 | 14 | It operates on perpetual futures derivatives markets, automatically creating and cancelling limit buy and sell orders on behalf of the user. It does not try to predict future price movements, it does not use technical indicators, nor does it follow trends. Rather, it is a contrarian market maker, providing resistance to price changes in both directions, thereby "serving the market" as a price stabilizer. 15 | 16 | Passivbot's behavior may be backtested on historical price data, using the included backtester whose CPU heavy functions are written in Rust for speed. Also included is an optimizer, which finds better configurations by iterating thousands of backtests with different candidates, converging on the optimal ones with an evolutionary algorithm. 17 | 18 | ## Strategy 19 | 20 | Inspired by the Martingale betting strategy, the robot will make a small initial entry and double down on its losing positions multiple times to bring the average entry price closer to current price action. The orders are placed in a grid, ready to absorb sudden price movements. After each re-entry, the robot quickly updates its closing orders at a set take-profit markup. This way, if there is even a minor market reversal, or "bounce", the position can be closed in profit, and it starts over. 21 | 22 | ### Trailing Orders 23 | In addition to grid-based entries and closes, Passivbot may be configured to utilize trailing entries and trailing closes. 24 | 25 | For trailing entries, the bot waits for the price to move beyond a specified threshold and then retrace by a defined percentage before placing a re-entry order. Similarly, for trailing closes, the bot waits before placing its closing orders until after the price has moved favorably by a threshold percentage and then retraced by a specified percentage. This may result in the bot locking in profits more effectively by exiting positions when the market shows signs of reversing instead of at a fixed distance from average entry price. 26 | 27 | Grid and trailing orders may be combined, such that the robot enters or closes a whole or a part of the position as grid orders and/or as trailing orders. 28 | 29 | ### Forager 30 | The Forager feature dynamically chooses the most volatile markets on which to open positions. Volatility is defined as the mean of the normalized relative range for the most recent 1m candles, i.e. `mean((ohlcv.high - ohlcv.low) / ohlcv.close)`. 31 | 32 | ### Unstucking Mechanism 33 | Passivbot manages underperforming, or "stuck", positions by realizing small losses over time. If multiple positions are stuck, the bot prioritizes positions with the smallest gap between the entry price and current market price for "unstucking". Losses are limited by ensuring that the account balance does not fall under a set percentage below the past peak balance. 34 | 35 | ## Installation 36 | 37 | To install Passivbot and its dependencies, follow the steps below. 38 | 39 | ### Step 1: Clone the Repository 40 | 41 | First, clone the Passivbot repository to the local machine: 42 | 43 | ```sh 44 | git clone https://github.com/enarjord/passivbot.git 45 | cd passivbot 46 | ``` 47 | 48 | 49 | ### Step 2: Install Rust 50 | Passivbot uses Rust for some of its components. Install Rust by following these steps: 51 | 52 | Visit https://www.rust-lang.org/tools/install 53 | Follow the instructions to install Rustup, the Rust installer and version management tool. 54 | After installation, restart the terminal or command prompt. 55 | 56 | ### Step 3: Create and Activate a Virtual Environment 57 | 58 | Create a virtual environment to manage dependencies: 59 | 60 | ```sh 61 | python3 -m venv venv 62 | ``` 63 | 64 | Activate the virtual environment: 65 | 66 | ```sh 67 | source venv/bin/activate 68 | ``` 69 | 70 | ### Step 4: Install Python Dependencies 71 | 72 | Install all the required Python dependencies listed in the `requirements.txt` file: 73 | 74 | ```sh 75 | pip install -r requirements.txt 76 | ``` 77 | 78 | ### Step 5 (optional): Build Rust Extensions 79 | 80 | Passivbot will attempt to build the necessary Rust extensions automatically, but they can also be built manually by navigating to the `passivbot-rust` directory and using `maturin`: 81 | 82 | ```sh 83 | cd passivbot-rust 84 | maturin develop --release 85 | cd .. 86 | ``` 87 | 88 | If changes in the Rust source are detected, recompilation is needed, which Passivbot will attempt to do automatically when starting. To manually recompile, use the commands given above. 89 | 90 | ### Step 6: Add API keys 91 | 92 | Make a copy of the api-keys template file: 93 | 94 | ```sh 95 | cp api-keys.json.example api-keys.json 96 | ``` 97 | 98 | Add your keys to api-keys.json. 99 | 100 | ### Step 7: Run Passivbot 101 | 102 | To start the bot with the default settings, run: 103 | 104 | ```sh 105 | python3 src/main.py -u {account_name_from_api-keys.json} 106 | ``` 107 | 108 | or make a new configuration file, using `configs/template.json` as a template, and start the bot with: 109 | 110 | 111 | ```sh 112 | python3 src/main.py path/to/config.json 113 | ``` 114 | 115 | ## Jupyter Lab 116 | 117 | Jupyter lab needs to be run in the same virtual environment as the bot. Activate venv (see installation instructions above, step 3), and launch Jupyter lab from the Passivbot root dir with: 118 | ```shell 119 | python3 -m jupyter lab 120 | ``` 121 | 122 | ## Requirements 123 | 124 | - Python >= 3.8 125 | - [requirements.txt](requirements.txt) dependencies 126 | 127 | ## Pre-optimized configurations 128 | 129 | Coming soon... 130 | 131 | See also https://pbconfigdb.scud.dedyn.io/ 132 | 133 | ## Documentation: 134 | 135 | For more detailed information about Passivbot, see documentation files here: [docs/](docs/) 136 | 137 | ## Support 138 | 139 | [![Discord](https://img.shields.io/badge/Discord-7289DA?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/QAF2H2UmzZ) 140 | 141 | [![Telegram](https://img.shields.io/badge/Telegram-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/passivbot_futures) 142 | 143 | ## Third Party Links, Referrals and Tip Jar 144 | 145 | **Passivbot Manager Service:** 146 | There is a paid manager service to run Passivbot on the user's behalf: 147 | www.passivbotmanager.com 148 | 149 | **Passivbot GUI** 150 | A graphical user interface for Passivbot: 151 | https://github.com/msei99/pbgui 152 | 153 | **Referrals:** 154 | Signing up using these referrals is appreciated: 155 | https://accounts.binance.com/register?ref=TII4B07C 156 | https://partner.bybit.com/b/passivbot 157 | https://partner.bitget.com/bg/Y8FU1W 158 | https://www.okx.com/join/PASSIVBOT (20% rebate) 159 | https://app.hyperliquid.xyz/join/PASSIVBOT 160 | 161 | **Note on Binance** 162 | To support continued Passivbot development, please use a Binance account which 163 | 1) was created after 2024-09-21 and 164 | 2) either: 165 | a) was created without a referral link, or 166 | b) was created with referral ID: "TII4B07C". 167 | 168 | Passivbot receives commissions from trades only for accounts meeting these criteria. 169 | 170 | 171 | **BuyMeACoffee:** 172 | https://www.buymeacoffee.com/enarjord 173 | 174 | **Donations:** 175 | If the robot is profitable, consider donating as showing gratitude for its development: 176 | 177 | - USDT or USDC Binance Smart Chain BEP20: 178 | 0x4b7b5bf6bea228052b775c052843fde1c63ec530 179 | - USDT or USDC Arbitrum One: 180 | 0x4b7b5bf6bea228052b775c052843fde1c63ec530 181 | 182 | Bitcoin (BTC) via Strike: 183 | enarjord@strike.me 184 | 185 | ## License 186 | 187 | Released freely without conditions. 188 | Anybody may copy, distribute, modify, use or misuse for commercial, non-commercial, educational or non-educational purposes, censor, claim as one's own or otherwise do whatever without permission from anybody. 189 | -------------------------------------------------------------------------------- /api-keys.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "referrals": { 3 | "bybit": "https://partner.bybit.com/b/passivbot", 4 | "bitget": "https://partner.bitget.com/bg/Y8FU1W", 5 | "okx": "https://www.okx.com/join/PASSIVBOT", 6 | "binance": "https://accounts.binance.com/register?ref=TII4B07C", 7 | "gateio": "https://www.gate.io/signup/AQCRUVTE", 8 | "hyperliquid": "https://app.hyperliquid.xyz/join/PASSIVBOT" 9 | }, 10 | "binance_01" : { 11 | "exchange": "binance", 12 | "key": "key", 13 | "secret": "secret" 14 | }, 15 | "bybit_01" : { 16 | "exchange": "bybit", 17 | "key": "key", 18 | "secret": "secret" 19 | }, 20 | "bitget_01" : { 21 | "exchange": "bitget", 22 | "key": "key", 23 | "secret": "secret", 24 | "passphrase": "passphrase" 25 | }, 26 | "okx_01" : { 27 | "exchange": "okx", 28 | "key": "key", 29 | "secret": "secret", 30 | "passphrase": "passphrase" 31 | }, 32 | "hyperliquid_01" : { 33 | "exchange": "hyperliquid", 34 | "wallet_address": "wallet_address", 35 | "private_key": "private_key", 36 | "is_vault": false 37 | }, 38 | "gateio_01" : { 39 | "exchange": "gateio", 40 | "key": "key", 41 | "secret": "secret" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /broker_codes.hjson: -------------------------------------------------------------------------------- 1 | { 2 | // Broker codes to exchanges with which Passivbot has a broker agreement. 3 | binance: "etgQucuf" 4 | binance_spot: "URB6VTDQ" 5 | bybit: "passivbotbybit" 6 | bitget: "Passivbot" 7 | okx: "0fe0667832d7BCDE" 8 | kucoin: "" 9 | mexc: "" 10 | hyperliquid: "" 11 | gateio: "passivbot" 12 | } -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [v3.5.2] - 2021-05-10 6 | - walk forward optimization 7 | - more advanced backtest analysis tools 8 | 9 | ## [v3.5.1] - 2021-05-09 10 | - optimize with sliding window 11 | - restructured dirs 12 | - new dirs 13 | - `backtests/{exchange}/{symbol}/optimize/` 14 | - `backtests/{exchange}/{symbol}/plots/` 15 | - `backtests/{exchange}/{symbol}/caches/` 16 | - if end_date = -1 in backtest_config.hjson, downloader will make a new ticks_cache.npy for each session 17 | - to reuse prev ticks cache, set end_date to a static date 18 | - when optimizing, intermediate_best_result.json is dumped as usable live_config 19 | 20 | ## [v3.5.0] - 2021-05-02 21 | - added volatility indicator 22 | - split extract optimize.py from backtest.py 23 | - now `python3 backtest.py backtest_config.hjson live_config.json` will backtest and plot single candidate 24 | - `python3 optimize.py backtest_config.hjson` will optimize 25 | - refactoring of all @njit calcs, separating them into jitted.py 26 | - added telegram bot 27 | 28 | ## [v3.4.0] - 2021-04-14 29 | - added binance USDT-margined backtester with stoploss 30 | - added binance COIN-margined backtester with stoploss 31 | - changed backtester usage -- now needs to specify whole path to .hjson config file 32 | 33 | ## [v3.3.3] - 2021-04-05 34 | 35 | - added stop loss 36 | - todo: backtester with stop loss 37 | 38 | ## [v3.3.2] - 2021-04-05 39 | 40 | - changed api key format. put api key/secret in api-keys.json in main dir. 41 | - changed name of live_settings dir to live_configs, removed subdirs binance/bybit 42 | - changed how to use start_bot.py. see updated startup instructions 43 | - improved backtester multiprocessing memory usage 44 | 45 | ## [v3.1.1] - 2021-04-01 46 | 47 | - Binance inverse futures coin margined markets now supported 48 | 49 | ## [v3.3.0] - 2021-03-30 50 | 51 | - Bybit usdt linear perpetual and Bybit inverse perpetual markets now supported 52 | - new downloader for historical backtesting data 53 | 54 | ## [v3.2.1] - 2021-03-25 55 | 56 | - bug fixes 57 | - Bybit backtester improvements and bug fixes 58 | - numba is now enabled by default, use --nojit to disable numba 59 | - several renamings 60 | 61 | ## [v3.2.0] - 2021-03-23 62 | 63 | - implemented particle swarm optimizationg algorithm, replacing jackrabbit 64 | - Bybit hedge mode with inverse futures 65 | - removed config param close_qty_pct 66 | - removed config param balance_pct 67 | - removed config param max_markup 68 | - added config param markup_range 69 | 70 | ## [v3.1.0] - 2021-03-21 71 | 72 | - removed setting min_close_qty_multiplier 73 | - added setting close_qty_pct, which complements entry_qty_pct 74 | - rewrote iter_long/shrt_closes 75 | - fixed memory leak bug 76 | 77 | ## [v3.0.1] - 2021-03-18 78 | 79 | - hedge mode backtester implemented 80 | - emas added back 81 | 82 | ## [v3.0.0] - 2021-03-12 83 | 84 | - Binance hedge mode implemented (Bybit not yet supported) 85 | - emas removed 86 | - stop loss removed 87 | 88 | ## [v2.0.3] - 2021-03-02 89 | 90 | - new default Bybit config 91 | - behavior change: reentry qtys may now be smaller than initial entry qty 92 | - backtest iterates a numpy array instead of a python list of dicts for reduced ram usage 93 | 94 | ## [v2.0.2] - 2021-03-01 95 | 96 | - more jit'ed calcs 97 | 98 | ## [v2.0.1] - 2021-02-28 99 | 100 | - added optional just-in-time compiling for faster backtesting 101 | 102 | ## [v2.0.0] - 2021-02-23 103 | 104 | - major update to backtester 105 | - new backtest usage syntax 106 | - other changes 107 | 108 | ## [v1.3.3] - 2021-02-18 109 | 110 | - minor refactoring 111 | 112 | ## [v1.3.0] - 2021-02-17 113 | 114 | - added indicator_settings["tick_ema"]["spread"] to live bot and backtester 115 | - optional setting -- ema_spread defaults to 0.0 if not present in config file 116 | 117 | ## [v1.2.1] - 2021-02-17 118 | 119 | - backtester will cache exchange fetched settings after first run 120 | - backtester will prevent using leverage higher than max leverage, in case max leverage set in ranges.json was too high 121 | 122 | ## [v1.2.0] - 2021-02-17 123 | 124 | - bug fixes 125 | - change in backtesting_notes.ipynb 126 | - automatic plot dump 127 | - other changes 128 | - backtester now fetches relevant data from exchanges instead of user having to input them manually 129 | 130 | ## [v1.1.0] - 2021-02-16 131 | 132 | - bug fixes v1.0.2 133 | - updated default Bybit live settings v1.1.0 134 | 135 | ## 2021-02-12 136 | 137 | - added indicator_settings["funding_fee_collect_mode"] 138 | - if true, will enter long only if predicted fundig rate is negative and enter short only if predicted funding rate 139 | is positive 140 | - added indicator rsi (not finished, not active) 141 | - changed entry_qty_pct formula 142 | - before initial_entry_qty = balance_ito_contracts * entry_qty_pct 143 | - now initial_entry_qty = balance_ito_contracts * leverage * entry_qty_pct 144 | - added logging 145 | - added "config_name" and "logging_level" to live settings 146 | - added break_on condition: break if balance + pnl < starting_balance 147 | 148 | ## 2021-02-10 149 | 150 | - renamed settings["default_qty"] to settings["entry_qty_pct"] 151 | - settings["entry_qty_pct"] may now also be a positive value 152 | - renamed settings["balance"] to settings["balance_pct"] 153 | - settings["balance_pct"] may now also be a positive value 154 | - added balance_pct to backtester. backtester will now behave like live bot, taking balance_pct into account 155 | - actual balance is used for liq price calc, otherwise balance * balance_pct is used 156 | 157 | ## 2021-02-09 158 | 159 | - added classic stop loss 160 | 161 | ## 2021-02-08 162 | 163 | - added min_close_qty_multiplier 164 | 165 | ## 2021-02-03 166 | 167 | - backtester break conditions change 168 | - bug fixes 169 | 170 | ## 2021-01-30 171 | 172 | - changed backtesting results formatting 173 | - fixed insufficient margin error 174 | - many other fixes and changes... 175 | - added possibility of running same backtest in two or more terminals for better cpu utilization 176 | 177 | ## 2021-01-23 178 | 179 | - removed static mode 180 | - added indicator ema 181 | - rewrote backtester 182 | 183 | ## 2021-01-19 184 | 185 | - renamed settings["margin_limit"] to settings["balance"] 186 | - bug fixes and changes in trade data downloading 187 | - if there already is historical trade data downloaded, run the script `rename_trade_data_csvs.py` to rename all files 188 | -------------------------------------------------------------------------------- /configs/approved_coins_pumpdump.json: -------------------------------------------------------------------------------- 1 | ["ACE", "ACT", "AEVO", "AI", "AI16Z", "ALT", "AXL", "BAN", "BLZ", "BOME", "BOND", "CAT", "CATI", "CHILLGUY", "CYBER", "DEGEN", "DOGS", "DYM", "FXS", "GOAT", "HFT", "HIPPO", "HOOK", "ILV", "KEY", "KOMA", "LINA", "LOOM", "MANTA", "MAVIA", "MEME", "METIS", "MOODENG", "MYRO", "NEIROETH", "NFP", "OMNI", "PIXEL", "PONKE", "POPCAT", "PORTAL", "RATS", "RDNT", "REZ", "SAGA", "SATS", "STRK", "THE", "TNSR", "UNFI", "USTC", "USUAL", "VANRY", "W", "XAI", "ZEREBRO", "ZETA"] 2 | -------------------------------------------------------------------------------- /configs/approved_coins_top20mcap.json: -------------------------------------------------------------------------------- 1 | ["BTC", "ETH", "XRP", "SOL", "BNB", "DOGE", "ADA", "TRX", "LINK", "AVAX", "SUI", "XLM", "TON", "HBAR", "SHIB", "HYPE", "LTC", "DOT", "BCH", "UNI"] -------------------------------------------------------------------------------- /configs/examples/all_approved.json: -------------------------------------------------------------------------------- 1 | {"backtest": {"base_dir": "backtests", 2 | "combine_ohlcvs": true, 3 | "compress_cache": true, 4 | "end_date": "now", 5 | "exchanges": ["binance", "bybit", "gateio", "bitget"], 6 | "gap_tolerance_ohlcvs_minutes": 120.0, 7 | "start_date": "2023-03-01", 8 | "starting_balance": 100000}, 9 | "bot": {"long": {"close_grid_markup_range": 0.01057, 10 | "close_grid_min_markup": 0.026138, 11 | "close_grid_qty_pct": 0.93767, 12 | "close_trailing_grid_ratio": 0.10339, 13 | "close_trailing_qty_pct": 0.67755, 14 | "close_trailing_retracement_pct": 0.061018, 15 | "close_trailing_threshold_pct": 0.088882, 16 | "ema_span_0": 892.56, 17 | "ema_span_1": 1136.3, 18 | "enforce_exposure_limit": 1.0, 19 | "entry_grid_double_down_factor": 0.34485, 20 | "entry_grid_spacing_pct": 0.028329, 21 | "entry_grid_spacing_weight": 7.1262, 22 | "entry_initial_ema_dist": -0.0021124, 23 | "entry_initial_qty_pct": 0.019051, 24 | "entry_trailing_grid_ratio": 0.068585, 25 | "entry_trailing_retracement_pct": 0.093807, 26 | "entry_trailing_threshold_pct": 0.030388, 27 | "filter_relative_volume_clip_pct": 0.14329, 28 | "filter_rolling_window": 177.65, 29 | "n_positions": 8.8606, 30 | "total_wallet_exposure_limit": 0.94404, 31 | "unstuck_close_pct": 0.090804, 32 | "unstuck_ema_dist": -0.077557, 33 | "unstuck_loss_allowance_pct": 0.044542, 34 | "unstuck_threshold": 0.81942}, 35 | "short": {"close_grid_markup_range": 0, 36 | "close_grid_min_markup": 0.001, 37 | "close_grid_qty_pct": 0.05, 38 | "close_trailing_grid_ratio": 0.0, 39 | "close_trailing_qty_pct": 0.05, 40 | "close_trailing_retracement_pct": 0.0001, 41 | "close_trailing_threshold_pct": 0.0, 42 | "ema_span_0": 200, 43 | "ema_span_1": 200, 44 | "enforce_exposure_limit": 1.0, 45 | "entry_grid_double_down_factor": 0.1, 46 | "entry_grid_spacing_pct": 0.001, 47 | "entry_grid_spacing_weight": 0, 48 | "entry_initial_ema_dist": 0.0, 49 | "entry_initial_qty_pct": 0.004, 50 | "entry_trailing_grid_ratio": 0.0, 51 | "entry_trailing_retracement_pct": 0.0001, 52 | "entry_trailing_threshold_pct": 0.0, 53 | "filter_relative_volume_clip_pct": 0, 54 | "filter_rolling_window": 10, 55 | "n_positions": 0.0, 56 | "total_wallet_exposure_limit": 0.0, 57 | "unstuck_close_pct": 0.001, 58 | "unstuck_ema_dist": 0.0, 59 | "unstuck_loss_allowance_pct": 0.001, 60 | "unstuck_threshold": 0.4}}, 61 | "live": {"approved_coins": "", 62 | "auto_gs": true, 63 | "coin_flags": {}, 64 | "empty_means_all_approved": true, 65 | "execution_delay_seconds": 2.0, 66 | "filter_by_min_effective_cost": true, 67 | "forced_mode_long": "", 68 | "forced_mode_short": "", 69 | "ignored_coins": {"long": [], "short": []}, 70 | "leverage": 10.0, 71 | "market_orders_allowed": true, 72 | "max_n_cancellations_per_batch": 5, 73 | "max_n_creations_per_batch": 3, 74 | "max_n_restarts_per_day": 10, 75 | "minimum_coin_age_days": 30.0, 76 | "ohlcvs_1m_rolling_window_days": 4.0, 77 | "ohlcvs_1m_update_after_minutes": 10.0, 78 | "pnls_max_lookback_days": 30.0, 79 | "price_distance_threshold": 0.002, 80 | "time_in_force": "good_till_cancelled", 81 | "user": "bybit_01"}, 82 | "optimize": {"bounds": {"long_close_grid_markup_range": [0, 0.03], 83 | "long_close_grid_min_markup": [0.001, 0.03], 84 | "long_close_grid_qty_pct": [0.05, 1.0], 85 | "long_close_trailing_grid_ratio": [-1, 1], 86 | "long_close_trailing_qty_pct": [0.05, 1.0], 87 | "long_close_trailing_retracement_pct": [0.0001, 0.1], 88 | "long_close_trailing_threshold_pct": [-0.01, 0.1], 89 | "long_ema_span_0": [200, 1440], 90 | "long_ema_span_1": [200, 1440], 91 | "long_entry_grid_double_down_factor": [0.1, 3], 92 | "long_entry_grid_spacing_pct": [0.001, 0.06], 93 | "long_entry_grid_spacing_weight": [0, 10], 94 | "long_entry_initial_ema_dist": [-0.1, 0.003], 95 | "long_entry_initial_qty_pct": [0.004, 0.02], 96 | "long_entry_trailing_grid_ratio": [-1, 1], 97 | "long_entry_trailing_retracement_pct": [0.0001, 0.1], 98 | "long_entry_trailing_threshold_pct": [-0.01, 0.1], 99 | "long_filter_relative_volume_clip_pct": [0, 1], 100 | "long_filter_rolling_window": [10, 360], 101 | "long_n_positions": [6.4, 12.0], 102 | "long_total_wallet_exposure_limit": [0.0, 2.0], 103 | "long_unstuck_close_pct": [0.001, 0.1], 104 | "long_unstuck_ema_dist": [-0.1, 0.01], 105 | "long_unstuck_loss_allowance_pct": [0.001, 0.05], 106 | "long_unstuck_threshold": [0.4, 0.95], 107 | "short_close_grid_markup_range": [0, 0.03], 108 | "short_close_grid_min_markup": [0.001, 0.03], 109 | "short_close_grid_qty_pct": [0.05, 1.0], 110 | "short_close_trailing_grid_ratio": [-1, 1], 111 | "short_close_trailing_qty_pct": [0.05, 1.0], 112 | "short_close_trailing_retracement_pct": [0.0001, 0.1], 113 | "short_close_trailing_threshold_pct": [-0.01, 0.1], 114 | "short_ema_span_0": [200, 1440], 115 | "short_ema_span_1": [200, 1440], 116 | "short_entry_grid_double_down_factor": [0.1, 3], 117 | "short_entry_grid_spacing_pct": [0.001, 0.06], 118 | "short_entry_grid_spacing_weight": [0, 10], 119 | "short_entry_initial_ema_dist": [-0.1, 0.003], 120 | "short_entry_initial_qty_pct": [0.004, 0.02], 121 | "short_entry_trailing_grid_ratio": [-1, 1], 122 | "short_entry_trailing_retracement_pct": [0.0001, 0.1], 123 | "short_entry_trailing_threshold_pct": [-0.01, 0.1], 124 | "short_filter_relative_volume_clip_pct": [0, 1], 125 | "short_filter_rolling_window": [10, 360], 126 | "short_n_positions": [0.0], 127 | "short_total_wallet_exposure_limit": [0.0, 2.0], 128 | "short_unstuck_close_pct": [0.001, 0.1], 129 | "short_unstuck_ema_dist": [-0.1, 0.01], 130 | "short_unstuck_loss_allowance_pct": [0.001, 0.05], 131 | "short_unstuck_threshold": [0.4, 0.95]}, 132 | "compress_results_file": true, 133 | "crossover_probability": 0.7, 134 | "iters": 300000, 135 | "limits": {"lower_bound_drawdown_worst": 0.333, 136 | "lower_bound_drawdown_worst_mean_1pct": 0.2, 137 | "lower_bound_equity_balance_diff_neg_max": 0.4, 138 | "lower_bound_equity_balance_diff_neg_mean": 0.01, 139 | "lower_bound_equity_balance_diff_pos_max": 0.5, 140 | "lower_bound_equity_balance_diff_pos_mean": 0.02, 141 | "lower_bound_loss_profit_ratio": 0.5}, 142 | "mutation_probability": 0.2, 143 | "n_cpus": 5, 144 | "population_size": 500, 145 | "scoring": ["mdg", "sterling_ratio"]}} 146 | -------------------------------------------------------------------------------- /configs/examples/top20mcap.json: -------------------------------------------------------------------------------- 1 | {"backtest": {"base_dir": "backtests", 2 | "combine_ohlcvs": true, 3 | "compress_cache": true, 4 | "end_date": "now", 5 | "exchanges": ["binance", "bybit", "gateio", "bitget"], 6 | "gap_tolerance_ohlcvs_minutes": 120.0, 7 | "start_date": "2021-04-01", 8 | "starting_balance": 100000}, 9 | "bot": {"long": {"close_grid_markup_range": 0.0049469, 10 | "close_grid_min_markup": 0.0010319, 11 | "close_grid_qty_pct": 0.98174, 12 | "close_trailing_grid_ratio": -0.051773, 13 | "close_trailing_qty_pct": 0.62389, 14 | "close_trailing_retracement_pct": 0.068376, 15 | "close_trailing_threshold_pct": 0.05532, 16 | "ema_span_0": 592.85, 17 | "ema_span_1": 378.27, 18 | "enforce_exposure_limit": 1.0, 19 | "entry_grid_double_down_factor": 1.7327, 20 | "entry_grid_spacing_pct": 0.02953, 21 | "entry_grid_spacing_weight": 1.2199, 22 | "entry_initial_ema_dist": -0.095891, 23 | "entry_initial_qty_pct": 0.017586, 24 | "entry_trailing_grid_ratio": -0.1159, 25 | "entry_trailing_retracement_pct": 0.0050836, 26 | "entry_trailing_threshold_pct": 0.073096, 27 | "filter_relative_volume_clip_pct": 0.14551, 28 | "filter_rolling_window": 302.6, 29 | "n_positions": 6.4006, 30 | "total_wallet_exposure_limit": 1.9999, 31 | "unstuck_close_pct": 0.059038, 32 | "unstuck_ema_dist": -0.088984, 33 | "unstuck_loss_allowance_pct": 0.0089268, 34 | "unstuck_threshold": 0.42893}, 35 | "short": {"close_grid_markup_range": 0.027581, 36 | "close_grid_min_markup": 0.0099447, 37 | "close_grid_qty_pct": 0.66597, 38 | "close_trailing_grid_ratio": 0.37308, 39 | "close_trailing_qty_pct": 0.75008, 40 | "close_trailing_retracement_pct": 0.022447, 41 | "close_trailing_threshold_pct": 0.0038198, 42 | "ema_span_0": 626.32, 43 | "ema_span_1": 806.82, 44 | "enforce_exposure_limit": 1.0, 45 | "entry_grid_double_down_factor": 0.6284, 46 | "entry_grid_spacing_pct": 0.014462, 47 | "entry_grid_spacing_weight": 6.6924, 48 | "entry_initial_ema_dist": -0.01775, 49 | "entry_initial_qty_pct": 0.016089, 50 | "entry_trailing_grid_ratio": 0.26904, 51 | "entry_trailing_retracement_pct": 0.026272, 52 | "entry_trailing_threshold_pct": 0.056207, 53 | "filter_relative_volume_clip_pct": 0.43246, 54 | "filter_rolling_window": 283.51, 55 | "n_positions": 6.4908, 56 | "total_wallet_exposure_limit": 0.0, 57 | "unstuck_close_pct": 0.09014, 58 | "unstuck_ema_dist": -0.096692, 59 | "unstuck_loss_allowance_pct": 0.039485, 60 | "unstuck_threshold": 0.45197}}, 61 | "live": {"approved_coins": "configs/approved_coins_top20mcap.json", 62 | "auto_gs": true, 63 | "coin_flags": {}, 64 | "empty_means_all_approved": true, 65 | "execution_delay_seconds": 2.0, 66 | "filter_by_min_effective_cost": true, 67 | "forced_mode_long": "", 68 | "forced_mode_short": "", 69 | "ignored_coins": {"long": [], "short": []}, 70 | "leverage": 10.0, 71 | "market_orders_allowed": true, 72 | "max_n_cancellations_per_batch": 5, 73 | "max_n_creations_per_batch": 3, 74 | "max_n_restarts_per_day": 10, 75 | "minimum_coin_age_days": 30.0, 76 | "ohlcvs_1m_rolling_window_days": 4.0, 77 | "ohlcvs_1m_update_after_minutes": 10.0, 78 | "pnls_max_lookback_days": 30.0, 79 | "price_distance_threshold": 0.002, 80 | "time_in_force": "good_till_cancelled", 81 | "user": "bybit_01"}, 82 | "optimize": {"bounds": {"long_close_grid_markup_range": [0, 0.03], 83 | "long_close_grid_min_markup": [0.001, 0.03], 84 | "long_close_grid_qty_pct": [0.05, 1.0], 85 | "long_close_trailing_grid_ratio": [-1, 1], 86 | "long_close_trailing_qty_pct": [0.05, 1.0], 87 | "long_close_trailing_retracement_pct": [0.0001, 0.1], 88 | "long_close_trailing_threshold_pct": [-0.01, 0.1], 89 | "long_ema_span_0": [200, 1440], 90 | "long_ema_span_1": [200, 1440], 91 | "long_entry_grid_double_down_factor": [0.1, 3], 92 | "long_entry_grid_spacing_pct": [0.001, 0.06], 93 | "long_entry_grid_spacing_weight": [0, 10], 94 | "long_entry_initial_ema_dist": [-0.1, 0.003], 95 | "long_entry_initial_qty_pct": [0.004, 0.02], 96 | "long_entry_trailing_grid_ratio": [-1, 1], 97 | "long_entry_trailing_retracement_pct": [0.0001, 0.1], 98 | "long_entry_trailing_threshold_pct": [-0.01, 0.1], 99 | "long_filter_relative_volume_clip_pct": [0, 1], 100 | "long_filter_rolling_window": [10, 360], 101 | "long_n_positions": [6.4, 12.0], 102 | "long_total_wallet_exposure_limit": [0.0, 2.0], 103 | "long_unstuck_close_pct": [0.001, 0.1], 104 | "long_unstuck_ema_dist": [-0.1, 0.01], 105 | "long_unstuck_loss_allowance_pct": [0.001, 0.05], 106 | "long_unstuck_threshold": [0.4, 0.95], 107 | "short_close_grid_markup_range": [0, 0.03], 108 | "short_close_grid_min_markup": [0.001, 0.03], 109 | "short_close_grid_qty_pct": [0.05, 1.0], 110 | "short_close_trailing_grid_ratio": [-1, 1], 111 | "short_close_trailing_qty_pct": [0.05, 1.0], 112 | "short_close_trailing_retracement_pct": [0.0001, 0.1], 113 | "short_close_trailing_threshold_pct": [-0.01, 0.1], 114 | "short_ema_span_0": [200, 1440], 115 | "short_ema_span_1": [200, 1440], 116 | "short_entry_grid_double_down_factor": [0.1, 3], 117 | "short_entry_grid_spacing_pct": [0.001, 0.06], 118 | "short_entry_grid_spacing_weight": [0, 10], 119 | "short_entry_initial_ema_dist": [-0.1, 0.003], 120 | "short_entry_initial_qty_pct": [0.004, 0.02], 121 | "short_entry_trailing_grid_ratio": [-1, 1], 122 | "short_entry_trailing_retracement_pct": [0.0001, 0.1], 123 | "short_entry_trailing_threshold_pct": [-0.01, 0.1], 124 | "short_filter_relative_volume_clip_pct": [0, 1], 125 | "short_filter_rolling_window": [10, 360], 126 | "short_n_positions": [6.4, 12.0], 127 | "short_total_wallet_exposure_limit": [0.0, 2.0], 128 | "short_unstuck_close_pct": [0.001, 0.1], 129 | "short_unstuck_ema_dist": [-0.1, 0.01], 130 | "short_unstuck_loss_allowance_pct": [0.001, 0.05], 131 | "short_unstuck_threshold": [0.4, 0.95]}, 132 | "compress_results_file": true, 133 | "crossover_probability": 0.7, 134 | "iters": 300000, 135 | "limits": {"lower_bound_drawdown_worst": 0.333, 136 | "lower_bound_drawdown_worst_mean_1pct": 0.2, 137 | "lower_bound_equity_balance_diff_neg_max": 0.4, 138 | "lower_bound_equity_balance_diff_neg_mean": 0.01, 139 | "lower_bound_equity_balance_diff_pos_max": 0.5, 140 | "lower_bound_equity_balance_diff_pos_mean": 0.02, 141 | "lower_bound_loss_profit_ratio": 0.5}, 142 | "mutation_probability": 0.2, 143 | "n_cpus": 5, 144 | "population_size": 500, 145 | "scoring": ["mdg", "sterling_ratio"]}} 146 | -------------------------------------------------------------------------------- /configs/template.json: -------------------------------------------------------------------------------- 1 | {"backtest": {"base_dir": "backtests", 2 | "combine_ohlcvs": true, 3 | "compress_cache": true, 4 | "end_date": "now", 5 | "exchanges": ["binance", "bybit", "gateio", "bitget"], 6 | "gap_tolerance_ohlcvs_minutes": 120.0, 7 | "start_date": "2021-04-01", 8 | "starting_balance": 100000}, 9 | "bot": {"long": {"close_grid_markup_range": 0.0049503, 10 | "close_grid_min_markup": 0.0010443, 11 | "close_grid_qty_pct": 0.99471, 12 | "close_trailing_grid_ratio": -0.051765, 13 | "close_trailing_qty_pct": 0.39506, 14 | "close_trailing_retracement_pct": 0.062717, 15 | "close_trailing_threshold_pct": 0.057498, 16 | "ema_span_0": 590.45, 17 | "ema_span_1": 373.88, 18 | "enforce_exposure_limit": true, 19 | "entry_grid_double_down_factor": 1.7316, 20 | "entry_grid_spacing_pct": 0.02953, 21 | "entry_grid_spacing_weight": 1.2209, 22 | "entry_initial_ema_dist": -0.095546, 23 | "entry_initial_qty_pct": 0.017584, 24 | "entry_trailing_grid_ratio": -0.11597, 25 | "entry_trailing_retracement_pct": 0.019594, 26 | "entry_trailing_threshold_pct": 0.071729, 27 | "filter_relative_volume_clip_pct": 0.0013778, 28 | "filter_rolling_window": 302.6, 29 | "n_positions": 6.4, 30 | "total_wallet_exposure_limit": 2.0, 31 | "unstuck_close_pct": 0.057285, 32 | "unstuck_ema_dist": -0.088691, 33 | "unstuck_loss_allowance_pct": 0.0068704, 34 | "unstuck_threshold": 0.4334}, 35 | "short": {"close_grid_markup_range": 0.024008, 36 | "close_grid_min_markup": 0.015672, 37 | "close_grid_qty_pct": 0.57896, 38 | "close_trailing_grid_ratio": 0.35219, 39 | "close_trailing_qty_pct": 0.81739, 40 | "close_trailing_retracement_pct": 0.023998, 41 | "close_trailing_threshold_pct": 0.0067008, 42 | "ema_span_0": 1263.1, 43 | "ema_span_1": 870.84, 44 | "enforce_exposure_limit": true, 45 | "entry_grid_double_down_factor": 0.91587, 46 | "entry_grid_spacing_pct": 0.019287, 47 | "entry_grid_spacing_weight": 6.9186, 48 | "entry_initial_ema_dist": -0.018062, 49 | "entry_initial_qty_pct": 0.01726, 50 | "entry_trailing_grid_ratio": 0.50074, 51 | "entry_trailing_retracement_pct": 0.028065, 52 | "entry_trailing_threshold_pct": 0.061895, 53 | "filter_relative_volume_clip_pct": 0.47467, 54 | "filter_rolling_window": 224.88, 55 | "n_positions": 7.8386, 56 | "total_wallet_exposure_limit": 0.0, 57 | "unstuck_close_pct": 0.090505, 58 | "unstuck_ema_dist": 0.0076126, 59 | "unstuck_loss_allowance_pct": 0.040375, 60 | "unstuck_threshold": 0.51196}}, 61 | "live": {"approved_coins": "configs/approved_coins_top20mcap.json", 62 | "auto_gs": true, 63 | "coin_flags": {}, 64 | "empty_means_all_approved": true, 65 | "execution_delay_seconds": 2.0, 66 | "filter_by_min_effective_cost": true, 67 | "forced_mode_long": "", 68 | "forced_mode_short": "", 69 | "ignored_coins": {"long": [], "short": []}, 70 | "leverage": 10.0, 71 | "market_orders_allowed": true, 72 | "max_n_cancellations_per_batch": 5, 73 | "max_n_creations_per_batch": 3, 74 | "max_n_restarts_per_day": 10, 75 | "minimum_coin_age_days": 30.0, 76 | "ohlcvs_1m_rolling_window_days": 4.0, 77 | "ohlcvs_1m_update_after_minutes": 10.0, 78 | "pnls_max_lookback_days": 30.0, 79 | "price_distance_threshold": 0.002, 80 | "time_in_force": "good_till_cancelled", 81 | "user": "bybit_01"}, 82 | "optimize": {"bounds": {"long_close_grid_markup_range": [0, 0.03], 83 | "long_close_grid_min_markup": [0.001, 0.03], 84 | "long_close_grid_qty_pct": [0.05, 1.0], 85 | "long_close_trailing_grid_ratio": [-1, 1], 86 | "long_close_trailing_qty_pct": [0.05, 1.0], 87 | "long_close_trailing_retracement_pct": [0.0001, 0.1], 88 | "long_close_trailing_threshold_pct": [-0.01, 0.1], 89 | "long_ema_span_0": [200, 1440], 90 | "long_ema_span_1": [200, 1440], 91 | "long_entry_grid_double_down_factor": [0.1, 3], 92 | "long_entry_grid_spacing_pct": [0.001, 0.06], 93 | "long_entry_grid_spacing_weight": [0, 10], 94 | "long_entry_initial_ema_dist": [-0.1, 0.003], 95 | "long_entry_initial_qty_pct": [0.004, 0.02], 96 | "long_entry_trailing_grid_ratio": [-1, 1], 97 | "long_entry_trailing_retracement_pct": [0.0001, 0.1], 98 | "long_entry_trailing_threshold_pct": [-0.01, 0.1], 99 | "long_filter_relative_volume_clip_pct": [0, 1], 100 | "long_filter_rolling_window": [10, 360], 101 | "long_n_positions": [6.4, 12.0], 102 | "long_total_wallet_exposure_limit": [0.0, 2.0], 103 | "long_unstuck_close_pct": [0.001, 0.1], 104 | "long_unstuck_ema_dist": [-0.1, 0.01], 105 | "long_unstuck_loss_allowance_pct": [0.001, 0.05], 106 | "long_unstuck_threshold": [0.4, 0.95], 107 | "short_close_grid_markup_range": [0, 0.03], 108 | "short_close_grid_min_markup": [0.001, 0.03], 109 | "short_close_grid_qty_pct": [0.05, 1.0], 110 | "short_close_trailing_grid_ratio": [-1, 1], 111 | "short_close_trailing_qty_pct": [0.05, 1.0], 112 | "short_close_trailing_retracement_pct": [0.0001, 0.1], 113 | "short_close_trailing_threshold_pct": [-0.01, 0.1], 114 | "short_ema_span_0": [200, 1440], 115 | "short_ema_span_1": [200, 1440], 116 | "short_entry_grid_double_down_factor": [0.1, 3], 117 | "short_entry_grid_spacing_pct": [0.001, 0.06], 118 | "short_entry_grid_spacing_weight": [0, 10], 119 | "short_entry_initial_ema_dist": [-0.1, 0.003], 120 | "short_entry_initial_qty_pct": [0.004, 0.02], 121 | "short_entry_trailing_grid_ratio": [-1, 1], 122 | "short_entry_trailing_retracement_pct": [0.0001, 0.1], 123 | "short_entry_trailing_threshold_pct": [-0.01, 0.1], 124 | "short_filter_relative_volume_clip_pct": [0, 1], 125 | "short_filter_rolling_window": [10, 360], 126 | "short_n_positions": [6.4, 12.0], 127 | "short_total_wallet_exposure_limit": [0.0, 2.0], 128 | "short_unstuck_close_pct": [0.001, 0.1], 129 | "short_unstuck_ema_dist": [-0.1, 0.01], 130 | "short_unstuck_loss_allowance_pct": [0.001, 0.05], 131 | "short_unstuck_threshold": [0.4, 0.95]}, 132 | "compress_results_file": true, 133 | "crossover_probability": 0.7, 134 | "iters": 300000, 135 | "limits": {"lower_bound_drawdown_worst": 0.333, 136 | "lower_bound_drawdown_worst_mean_1pct": 0.2, 137 | "lower_bound_equity_balance_diff_neg_max": 0.4, 138 | "lower_bound_equity_balance_diff_neg_mean": 0.01, 139 | "lower_bound_equity_balance_diff_pos_max": 0.5, 140 | "lower_bound_equity_balance_diff_pos_mean": 0.02, 141 | "lower_bound_loss_profit_ratio": 0.5, 142 | "lower_bound_position_held_hours_max": 336.0}, 143 | "mutation_probability": 0.2, 144 | "n_cpus": 5, 145 | "population_size": 500, 146 | "scoring": ["mdg", "sterling_ratio"]}} 147 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | passivbot: 3 | build: . 4 | container_name: passivbot 5 | restart: unless-stopped 6 | volumes: 7 | - ./:/app/ 8 | command: python passivbot.py binance_01 XMRUSDT live_configs/binance_default.json 9 | 10 | -------------------------------------------------------------------------------- /docs/backtesting.md: -------------------------------------------------------------------------------- 1 | # Backtesting 2 | 3 | Passivbot includes a backtester which will simulate the bot's behavior on past price data. Historical 1m candlestick data is automatically downloaded and cached for all coins. 4 | 5 | ## Usage 6 | 7 | ```shell 8 | python3 src/backtest.py 9 | ``` 10 | Or 11 | ```shell 12 | python3 src/backtest.py path/to/config.json 13 | ``` 14 | If no config is specified, it will default to `configs/template.json` 15 | 16 | ## Backtest Results 17 | 18 | Metrics and plots are dumped to `backtests/{exchange}/`. 19 | 20 | ## Backtest CLI args 21 | 22 | - `-dp` to disable individual coin plotting. 23 | - `-co` to combine the ohlcv data from multiple exchanges into a single array. Otherwise, backtest for each exchange individually. 24 | 25 | For a comprehensive list of CLI args: 26 | ```shell 27 | python3 src/backtest.py -h 28 | ``` 29 | 30 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Passivbot Parameters Explanation 2 | 3 | Here follows an overview of the parameters found in `config/template.json`. 4 | 5 | ## Backtest Settings 6 | 7 | - `base_dir`: Location to save backtest results. 8 | - `compress_cache`: set to true to save disk space. Set to false to load faster. 9 | - `end_date`: End date of backtest, e.g., 2024-06-23. Set to 'now' to use today's date as end date. 10 | - `exchanges`: Exchanges from which to fetch 1m OHLCV data for backtesting and optimizing. 11 | - `start_date`: Start date of backtest. 12 | - `starting_balance`: Starting balance in USD at the beginning of backtest. 13 | - `symbols`: Coins which were backtested for each exchange. Note: coins for backtesting are live.approved_coins minus live.ignored_coins. 14 | 15 | ## Bot Settings 16 | 17 | ### General Parameters for Long and Short 18 | 19 | - `ema_span_0`, `ema_span_1`: 20 | - Spans are given in minutes. 21 | - `next_EMA = prev_EMA * (1 - alpha) + new_val * alpha` 22 | - Where `alpha = 2 / (span + 1)`. 23 | - One more EMA span is added in between `ema_span_0` and `ema_span_1`: 24 | - `EMA_spans = [ema_span_0, (ema_span_0 * ema_span_1)**0.5, ema_span_1]` 25 | - These three EMAs are used to make an upper and a lower EMA band: 26 | - `ema_band_lower = min(emas)` 27 | - `ema_band_upper = max(emas)` 28 | - These are used for initial entries and auto unstuck closes. 29 | - `n_positions`: Maximum number of positions to open. Set to zero to disable long/short. 30 | - `total_wallet_exposure_limit`: Maximum exposure allowed. 31 | - For example, `total_wallet_exposure_limit = 0.75` means 75% of (unleveraged) wallet balance is used. 32 | - For example, `total_wallet_exposure_limit = 1.6` means 160% of (unleveraged) wallet balance is used. 33 | - Each position is given equal share of total exposure limit, i.e., `wallet_exposure_limit = total_wallet_exposure_limit / n_positions`. 34 | - See more: `docs/risk_management.md`. 35 | - `enforce_exposure_limit`: If true, will enforce exposure limits for each position. 36 | - E.g. if for any reason a position's exposure exceeds 1% of the limit, reduce the position at market price to exposure limit. 37 | - Useful for risk management if, for example, user withdraws balance or changes settings. 38 | 39 | ### Grid Entry Parameters 40 | 41 | Passivbot may be configured to make a grid of entry orders, the prices and quantities of which are determined by the following parameters: 42 | 43 | - `entry_grid_double_down_factor`: 44 | - Quantity of next grid entry is position size times double down factor. For example, if position size is 1.4 and `double_down_factor` is 0.9, then next entry quantity is `1.4 * 0.9 == 1.26`. 45 | - Also applies to trailing entries. 46 | - `entry_grid_spacing_pct`, `entry_grid_spacing_weight`: 47 | - Grid re-entry prices are determined as follows: 48 | - `next_reentry_price_long = pos_price * (1 - entry_grid_spacing_pct * modifier)` 49 | - `next_reentry_price_short = pos_price * (1 + entry_grid_spacing_pct * modifier)` 50 | - Where `modifier = (1 + ratio * entry_grid_spacing_weight)` 51 | - And where `ratio = wallet_exposure / wallet_exposure_limit` 52 | - `entry_initial_ema_dist`: 53 | - Offset from lower/upper EMA band. 54 | - Long initial entry/short unstuck close prices are lower EMA band minus offset. 55 | - Short initial entry/long unstuck close prices are upper EMA band plus offset. 56 | - See `ema_span_0`/`ema_span_1`. 57 | - `entry_initial_qty_pct`: 58 | - `initial_entry_cost = balance * wallet_exposure_limit * initial_qty_pct` 59 | 60 | ### Trailing Parameters 61 | 62 | The same logic applies to both trailing entries and trailing closes. 63 | 64 | - `trailing_grid_ratio`: 65 | - Set trailing and grid allocations. 66 | - If `trailing_grid_ratio == 0.0`, grid orders only. 67 | - If `trailing_grid_ratio == 1.0` or `trailing_grid_ratio == -1.0`, trailing orders only. 68 | - If `trailing_grid_ratio > 0.0`, trailing orders first, then grid orders. 69 | - If `trailing_grid_ratio < 0.0`, grid orders first, then trailing orders. 70 | - For example, `trailing_grid_ratio = 0.3`: trailing orders until position is 30% full, then grid orders for the rest. 71 | - For example, `trailing_grid_ratio = -0.9`: grid orders until position is (1 - 0.9) == 10% full, then trailing orders for the rest. 72 | - For example, `trailing_grid_ratio = -0.12`: grid orders until position is (1 - 0.12) == 88% full, then trailing orders for the rest. 73 | - `trailing_retracement_pct`, `trailing_threshold_pct`: 74 | - There are two conditions to trigger a trailing order: 1) threshold and 2) retracement. 75 | - If `trailing_threshold_pct <= 0.0`, threshold condition is always triggered. 76 | - Otherwise, the logic is as follows, considering long positions: 77 | - `if highest price since position open > position price * (1 + trailing_threshold_pct)`, the first condition is met. 78 | - And `if lowest price since highest price < highest price since position open * (1 - trailing_retracement_pct)`, the second condition is met. Place order. 79 | 80 | ### Grid Close Parameters 81 | 82 | - `close_grid_markup_range`, `close_grid_min_markup`, `close_grid_qty_pct`: 83 | - Take Profit (TP) prices are spread out from: 84 | - `pos_price * (1 + min_markup)` to `pos_price * (1 + min_markup + markup_range)` for long. 85 | - `pos_price * (1 - min_markup)` to `pos_price * (1 - min_markup - markup_range)` for short. 86 | - For example, if `long_pos_price == 100`, `min_markup = 0.01`, `markup_range = 0.02`, and `close_grid_qty_pct = 0.2`, there are at most `1 / 0.2 == 5` TP orders, and TP prices are `[101, 101.5, 102, 102.5, 103]`. 87 | - Quantity per order is `full pos size * close_grid_qty_pct`. 88 | - Note that full position size is when position is maxed out. If position is less than full, fewer than `1 / close_grid_qty_pct` may be created. 89 | - The TP grid is built from the top down: 90 | - First TP at 103 up to 20% of full position size. 91 | - Next TP at 102.5 from 20% to 40% of full position size. 92 | - Next TP at 102.0 from 40% to 60% of full position size. 93 | - Etc. 94 | - For example, if `full_pos_size = 100` and `long_pos_size == 55`, then TP orders are `[15@102.0, 20@102.5, 20@103.0]`. 95 | - If position is greater than full position size, the leftovers are added to the lowest TP order. 96 | - For example, if `long_pos_size == 130`, then TP orders are `[50@101.0, 20@101.5, 20@102.0, 20@102.5, 20@103.0]`. 97 | 98 | ### Trailing Close Parameters 99 | 100 | - `close_trailing_grid_ratio`: See Trailing Parameters above. 101 | - `close_trailing_qty_pct`: Close quantity is `full pos size * close_trailing_qty_pct`. 102 | - `close_trailing_retracement_pct`: See Trailing Parameters above. 103 | - `close_trailing_threshold_pct`: See Trailing Parameters above. 104 | 105 | ### Unstuck Parameters 106 | 107 | If a position is stuck, the bot will use profits made on other positions to realize losses for the stuck position. If multiple positions are stuck, the stuck position whose price action distance is the lowest is selected for unstucking. 108 | 109 | - `unstuck_close_pct`: 110 | - Percentage of `full pos size * wallet_exposure_limit` to close for each unstucking order. 111 | - `unstuck_ema_dist`: 112 | - Distance from EMA band to place unstucking order: 113 | - `long_unstuck_close_price = upper_EMA_band * (1 + unstuck_ema_dist)` 114 | - `short_unstuck_close_price = lower_EMA_band * (1 - unstuck_ema_dist)` 115 | - `unstuck_loss_allowance_pct`: 116 | - Weighted percentage below past peak balance to allow losses. 117 | - `loss_allowance = past_peak_balance * (1 - unstuck_loss_allowance_pct * total_wallet_exposure_limit)` 118 | - For example, if past peak balance was $10,000, `unstuck_loss_allowance_pct = 0.02`, and `total_wallet_exposure_limit = 1.5`, the bot will stop taking losses when balance reaches `$10,000 * (1 - 0.02 * 1.5) == $9,700`. 119 | - `unstuck_threshold`: 120 | - If a position is bigger than a threshold, consider it stuck and activate unstucking. 121 | - `if wallet_exposure / wallet_exposure_limit > unstuck_threshold: unstucking enabled` 122 | - For example, if a position size is $500 and max allowed position size is $1000, then position is 50% full. If `unstuck_threshold == 0.45`, then unstuck the position until its size is $450. 123 | 124 | ### Filter Parameters 125 | 126 | Coins selected for trading are filtered by volume and noisiness. First, filter coins by volume, dropping a percentage of the lowest volume coins. Then, sort the eligible coins by noisiness and select the top noisiest coins for trading. 127 | 128 | - `filter_relative_volume_clip_pct`: Volume filter; disapprove the lowest relative volume coins. For example, `filter_relative_volume_clip_pct = 0.1` drops the 10% lowest volume coins. Set to zero to allow all. 129 | - `filter_rolling_window`: Number of minutes to look into the past to compute volume and noisiness, used for dynamic coin selection in forager mode. 130 | - Noisiness is normalized relative range of 1m OHLCVs: `mean((high - low) / close)`. 131 | - In forager mode, the bot will select coins with highest noisiness for opening positions. 132 | 133 | ## Live Trading Settings 134 | 135 | - `approved_coins`: 136 | - List of coins approved for trading. If empty, see live.empty_means_all_approved. 137 | - Backtester and optimizer are live.approved_coins minus live.ignored_coins. 138 | - May be given as path to external file which is read by Passivbot continuously. 139 | - May be split into long and short by giving a json on the form: 140 | - `{"long": ["COIN1", "COIN2"], "short": ["COIN2", "COIN3"]}` 141 | - `auto_gs`: Automatically enable graceful stop for positions on disapproved coins. 142 | - Graceful stop means the bot will continue trading as normal, but not open a new position after the current position is fully closed. 143 | - If auto_gs=false, positions on disapproved coins are put on manual mode. 144 | - `coin_flags`: 145 | - Specify flags for individual coins, overriding values from bot config. 146 | - For example, `coin_flags: {"ETH": "-sm n -lm gs", "XRP": "-lm p -lc path/to/other_config.json"}` will force short mode to normal and long mode to graceful stop for ETH; it will set long mode to panic and use other config for XRP. 147 | - Flags: 148 | - `-lm` or `-sm`: Long or short mode. Choices: [n (normal), m (manual), gs (graceful_stop), p (panic), t (take_profit_only)]. 149 | - Normal mode: passivbot manages the position as normal. 150 | - Manual mode: passivbot ignores the position. 151 | - Graceful stop: if there is a position, passivbot will manage it; otherwise, passivbot will not make new positions. 152 | - Take profit only mode: passivbot will only manage closing orders. 153 | - Panic mode: passivbot will close the position immediately. 154 | - `-lw` or `-sw`: Long or short wallet exposure limit. 155 | - `-lev`: Leverage. 156 | - `-lc`: Path to live config. Load all of another config's bot parameters except `[n_positions, total_wallet_exposure_limit, unstuck_loss_allowance_pct, unstuck_close_pct]`. 157 | - empty_means_all_approved: 158 | - If true, will interpret approved_coins=[] as all coins approved. 159 | - If false, will interpret approved_coins=[] as no coins approved. 160 | - `execution_delay_seconds`: Wait x seconds after executing to exchange. 161 | - `filter_by_min_effective_cost`: If true, will disallow coins where `balance * WE_limit * initial_qty_pct < min_effective_cost`. 162 | - For example, if exchange's effective minimum cost for a coin is $5, but bot wants to make an order of $2, disallow that coin. 163 | - `forced_mode_long`, `forced_mode_short`: Force all coins long/short to a given mode. 164 | - Choices: [m (manual), gs (graceful_stop), p (panic), t (take_profit_only)]. 165 | - `ignored_coins`: 166 | - List of coins bot will not make positions on. If there are positions on that coin, turn on graceful stop or manual mode. 167 | - May be given as path to external file which is read by Passivbot continuously. 168 | - May be split into long and short by giving a json on the form: 169 | - `{"long": ["COIN1", "COIN2"], "short": ["COIN2", "COIN3"]}` 170 | - `leverage`: Leverage set on exchange. Default is 10. 171 | - `market_orders_allowed`: If true, allow Passivbot to place market orders when order price is very close to current market price. If false, will only place limit orders. Default is true. 172 | - `max_n_cancellations_per_batch`: Will cancel n open orders per execution. 173 | - `max_n_creations_per_batch`: Will create n new orders per execution. 174 | - `max_n_restarts_per_day`: If the bot crashes for any reason, restart the bot up to n times per day before stopping completely. 175 | - `minimum_coin_age_days`: Disallow coins younger than a given number of days. 176 | - `ohlcvs_1m_rolling_window_days`: How many days worth of OHLCVs for the bot to keep in memory. Reduce this number if RAM consumption becomes an issue. 177 | - `ohlcvs_1m_update_after_minutes`: How many minutes old OHLCVs for a coin may be before the bot will fetch fresh ones from the exchange. Increase this number if rate limiting becomes an issue. 178 | - `pnls_max_lookback_days`: How far into the past to fetch PnL history. 179 | - `price_distance_threshold`: Minimum distance to current price action required for EMA-based limit orders. 180 | - `time_in_force`: Default is Good-Till-Cancelled. 181 | - `user`: Fetch API key/secret from `api-keys.json`. 182 | 183 | ## Optimization Settings 184 | 185 | ### Bounds 186 | 187 | When optimizing, parameter values are within the lower and upper bounds. 188 | 189 | ### Other Optimization Parameters 190 | 191 | - `compress_results_file`: If true, will compress optimize output results file to save space. 192 | - `crossover_probability`: The probability of performing crossover between two individuals in the genetic algorithm. It determines how often parents will exchange genetic information to create offspring. 193 | - `iters`: Number of backtests per optimize session. 194 | - `mutation_probability`: The probability of mutating an individual in the genetic algorithm. It determines how often random changes will be introduced to the population to maintain diversity. 195 | - `n_cpus`: Number of CPU cores utilized in parallel. 196 | - `population_size`: Size of population for genetic optimization algorithm. 197 | - `scoring`: 198 | - The optimizer uses two objectives and finds the Pareto front. 199 | - Finally chooses the optimal candidate based on lowest Euclidean distance to the ideal point. 200 | - Default values are median daily gain and Sharpe ratio. 201 | - The script uses the NSGA-II algorithm (Non-dominated Sorting Genetic Algorithm II) for multi-objective optimization. 202 | - The fitness function is set up to minimize both objectives (converted to negative values internally). 203 | - Options: adg, mdg, sharpe_ratio, sortino_ratio, omega_ratio, calmar_ratio, sterling_ratio 204 | - Examples: ["mdg", "sharpe_ratio"], ["adg", "sortino_ratio"], ["sortino_ratio", "omega_ratio"] 205 | 206 | ### Optimization Limits 207 | 208 | The optimizer will penalize backtests whose metrics exceed the given values. If multiple exchanges are optimized, it will select the worst of them. 209 | 210 | - `lower_bound_drawdown_worst`: Lowest drawdown during backtest. 211 | - `lower_bound_equity_balance_diff_mean`: Mean of the difference between equity and balance. 212 | - `lower_bound_loss_profit_ratio`: `abs(sum(losses)) / sum(profit)` 213 | - `equity_balance_diff_neg_max`: greatest distance between balance and equity when equity is less than balance 214 | - `equity_balance_diff_neg_mean`: mean distance between balance and equity when equity is less than balance 215 | - `equity_balance_diff_pos_max`: greatest distance between balance and equity when equity is greater than balance 216 | - `equity_balance_diff_pos_mean`: mean distance between balance and equity when equity is greater than balance 217 | -------------------------------------------------------------------------------- /docs/hyperliquid_guide.md: -------------------------------------------------------------------------------- 1 | ## Quick Guide for Passivbot on Hyperliquid 2 | 3 | ### Hyperliquid account creation 4 | 5 | 1. Go to https://app.hyperliquid.xyz/ and create an account by connecting a third party wallet wallet (more secure) or using email (less secure). 6 | 2. Click the "Deposit" button to transfer USDC (not USDT, USDC.E or any other stablecoin) via the Arbitrum network to your account's address. 7 | 3. Navigate to "More" -> "API" and follow instructions to create an API wallet (agent wallet). 8 | 9 | ### Passivbot setup 10 | 11 | 1. If not already installed, install Passivbot. Otherwise, pull latest master branch: `git pull`. 12 | 2. Update the requirements: 13 | ```bash 14 | pip install -r requirements.txt 15 | ``` 16 | 3. Add your Hyperliquid wallet address and API wallet to `api-keys.json`: 17 | ```json 18 | "hyperliquid_01": { 19 | "exchange": "hyperliquid", 20 | "wallet_address": "YOUR_USDC_PUBLIC_WALLET_ADDRESS", 21 | "private_key": "API_PRIVATE_KEY", 22 | "is_vault": false 23 | } 24 | ``` 25 | For the `"private_key"`, use the API wallet created in the API section on Hyperliquid. 26 | 27 | Now Passivbot may be run as normal. Note that Hyperliquid has a minimum $10 order size: 28 | `initial_entry_cost = balance * (total_wallet_exposure_limit / n_positions) * initial_qty_pct` 29 | 30 | #### HyperLiquid with a Vault (CopyTrading-like) 31 | 1. In HyperLiquid, navigate to "Vaults" in the top menu and create a new vault. 32 | 2. Set the name and description (limited to 250 characters). 33 | 3. To find the wallet address of the vault, navigate to "Vaults" again and find your vault in the public vault list. 34 | 4. Click on your vault and get the public address of the vault. This address will be your `YOUR_VAULT_PUBLIC_ADDRESS` in `api-keys.json`. Remember to set `"is_vault"` to true. 35 | 36 | Update `api-keys.json`: 37 | ```json 38 | "hyperliquid_01": { 39 | "exchange": "hyperliquid", 40 | "wallet_address": "YOUR_VAULT_PUBLIC_ADDRESS", 41 | "private_key": "API_PRIVATE_KEY", 42 | "is_vault": true 43 | } 44 | ``` 45 | 46 | Refer to Hyperliquid's documentation for more details. 47 | -------------------------------------------------------------------------------- /docs/images/initial_entry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/postnext/Bitget/e704209a092f0063f248b656679db752e441f279/docs/images/initial_entry.png -------------------------------------------------------------------------------- /docs/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/postnext/Bitget/e704209a092f0063f248b656679db752e441f279/docs/images/logo.png -------------------------------------------------------------------------------- /docs/images/logo_pbot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /docs/images/logo_pbot_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/postnext/Bitget/e704209a092f0063f248b656679db752e441f279/docs/images/logo_pbot_dark.png -------------------------------------------------------------------------------- /docs/images/passivbot_grid_parameters.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/postnext/Bitget/e704209a092f0063f248b656679db752e441f279/docs/images/passivbot_grid_parameters.jpeg -------------------------------------------------------------------------------- /docs/images/pbot_logo_full.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /docs/images/pbot_logo_square.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /docs/images/reentry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/postnext/Bitget/e704209a092f0063f248b656679db752e441f279/docs/images/reentry.png -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installing passivbot 2 | 3 | ## Troubleshooting 4 | 5 | If there's an error running the command `maturin develop --release`: `error: linker cc not found`, try this: 6 | 7 | `apt update` 8 | `apt install build-essential` 9 | `apt install python3-dev` 10 | 11 | and try again. 12 | 13 | More info coming soon... 14 | -------------------------------------------------------------------------------- /docs/live.md: -------------------------------------------------------------------------------- 1 | # Running the bot live 2 | 3 | Coming soon... -------------------------------------------------------------------------------- /docs/optimizing.md: -------------------------------------------------------------------------------- 1 | # Optimizing 2 | 3 | Passivbot's configuration can be automatically optimized through iterative backtesting to find optimal parameters. 4 | 5 | ## Usage 6 | 7 | ```shell 8 | python3 src/optimize.py [path/to/config.json] 9 | ``` 10 | Defaults to `configs/template.json` if no config specified. 11 | 12 | ## Results Storage 13 | 14 | Optimization results are stored in `optimize_results/`` with filenames containing date, exchanges, number of coins, and unique identifier. Each result is appended as a single-line JSON string containing analysis and configuration. 15 | 16 | ## Analysis 17 | The script automatically runs `src/tools/extract_best_config.py` after optimization to identify the best performing configuration, saving the best candidate and the pareto front to `optimize_results_analysis/`. 18 | 19 | 20 | Manual analysis: 21 | 22 | ```shell 23 | python3 src/tools/extract_best_config.py path/to/results_file.txt 24 | ``` 25 | 26 | ## Performance Metrics 27 | 28 | Based on daily equity changes: `daily_eqs = equity.groupby(day).pct_change()` 29 | 30 | ### Key Metrics: 31 | 32 | - adg: Average daily gain (`daily_eqs.mean()`) 33 | - mdg: Median daily gain 34 | - gain: Final gain (`balance[-1] / balance[0]`) 35 | - drawdown_worst: Maximum peak-to-trough equity decline 36 | - drawdown_worst_mean_1pct: Mean of the 1% worst drawdowns on daily equity samples 37 | - expected_shortfall_1pct: Average of worst 1% losses (CVaR) 38 | 39 | ### Risk Ratios: 40 | 41 | - sharpe_ratio: Risk-adjusted return (`adg / daily_eqs.std()`) 42 | - sortino_ratio: Downside risk-adjusted return (`adg / downside_eqs.std()`) 43 | - calmar_ratio: Return to max drawdown ratio (`adg / drawdown_worst`) 44 | - sterling_ratio: Return to average worst 1% drawdowns ratio (`adg / drawdown_worst_mean_1pct`) 45 | - omega_ratio: Ratio of gains to losses 46 | - loss_profit_ratio: Absolute loss sum to profit sum ratio 47 | - equity_balance_diff_neg_max: greatest distance between balance and equity when equity is less than balance 48 | - equity_balance_diff_neg_mean: mean distance between balance and equity when equity is less than balance 49 | - equity_balance_diff_pos_max: greatest distance between balance and equity when equity is greater than balance 50 | - equity_balance_diff_pos_mean: mean distance between balance and equity when equity is greater than balance 51 | 52 | Suffix `_w` indicates weighted mean across 10 temporal subsets (whole, last_half, last_third, ... last_tenth). 53 | -------------------------------------------------------------------------------- /docs/overrides/main.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block footer %} 4 | {{ super() }} 5 | 6 | 7 | 9 | {% endblock %} -------------------------------------------------------------------------------- /docs/risk_management.md: -------------------------------------------------------------------------------- 1 | # Risk Management 2 | 3 | ## Leverage 4 | 5 | On perpetual futures markets with leverage, Passivbot may expose more than 100% of the wallet's funds. 6 | 7 | Passivbot uses only (unleveraged) wallet balance in its calculations, so adjusting leverage on exchange will make no difference on risk, profit or bot behavior, as long as leverage is set high enough for the bot to make its orders according to the configuration. Practically, this means that leverage should be set at least higher than total_wallet_exposure_limit_long + total_wallet_exposure_limit_short for the bot to have enough margin to make its orders. Otherwise, the bot might encounter "insufficient margin" errors. 8 | 9 | ## Wallet Exposure 10 | 11 | To measure a position's risk, Passivbot finds the ratio of position size (in quote) to total unleveraged balance. 12 | The formula for wallet exposure is 13 | 14 | `wallet_exposure = (position_size * position_price) / unleveraged_wallet_balance` 15 | 16 | wallet_exposure==0.0 means no position 17 | wallet_exposure==1.0 means 100% of unleveraged wallet balance is in position. 18 | wallet_exposure==4.0 means 400% of unleveraged wallet balance is in position. 19 | 20 | E.g. if wallet balance is $1000, long position size is 100.0 and position price is 35.0, 21 | then wallet_exposure is `100 * 35 / 1000 == 3.5` 22 | 23 | ## Wallet Exposure Limit 24 | 25 | The wallet_exposure_limit is the limit beyond than which the bot will not allow a position's wallet_exposure to grow. 26 | 27 | For example, if wallet_exposure_limit=0.6, the bot will not make any more entries when a position's wallet_exposure >= 0.6. 28 | 29 | ## Total Wallet Exposure Limit 30 | 31 | User sets total_wallet_exposure_limit separately for long and short. A single position's wallet_exposure_limit is `total_wallet_exposure_limit / n_positions`. 32 | 33 | ## Bankruptcy and liquidation 34 | 35 | Bankruptcy is defined as when `equity == (balance + unrealized_pnl) == 0.0`, that is, when total debt is equal to total assets. 36 | 37 | Liquidation happens when the exchange force closes a position to avoid it going into negative equity. 38 | This usually happens before actual bankruptcy is reached, in order for exchange to cover slippage costs. 39 | 40 | Bankruptcy price may be calculated from position and balance. 41 | 42 | E.g. 43 | If wallet_exposure==1.0, bankruptcy price is zero. 44 | If wallet_exposure==2.0, bankruptcy price is 50% lower than pos price. 45 | If wallet_exposure==3.0, bankruptcy price is 33.33% lower than pos price. 46 | If wallet_exposure==10.0, bankruptcy price is 10% lower than pos price. 47 | 48 | 49 | ## Getting stuck 50 | 51 | When a bot has no more entries left and wallet_exposure_limit is reached or exceeded, it is termed "getting stuck". 52 | 53 | If a long position is stuck and the price keeps falling, the distance between position price and market price grows larger, and closing the position in profit becomes less likely. Therefore it is desirable to bring position price closer to price action such that the position may be closed in profit on a small bounce. 54 | To achieve this, the position must be increased. However, the larger the position size, the higher the risk of liquidation, should the price keep moving the wrong way. 55 | 56 | ## Diversification 57 | 58 | While correlation is observed in most markets in general and in crypto markets in particular (e.g. if the price of bitcoin crashes, most other cryptos tend to follow closely), it is also observed that the "dead cat" often bounces at slightly different times and at different heights. Therefore, diversifying to multiple coins helps reduce the risk of a single bad coin destroying the whole account. 59 | 60 | A thousand coin flips will converge on 500 heads and 500 tails. One single coin flip will be either heads or tails. So it may be more desirable to end up with 3 out of 10 bots stuck, each with wallet_exposure==0.1, than with 1 single bot stuck with wallet_exposure==1.0. 61 | 62 | -------------------------------------------------------------------------------- /docs/tools.md: -------------------------------------------------------------------------------- 1 | # Tools 2 | 3 | ## Extract Pareto Frontier and best config from optimize output 4 | 5 | The pareto front and best config extracted will be dumped in `optimize_results_analysis/`. Results from an optimize session are usually dumped in `optimize_results/`. 6 | 7 | ```shell 8 | python3 src/tools/extract_best_config.py path/to/all_results.txt 9 | ``` 10 | 11 | ## Copy ohlcv data from old location to new location 12 | 13 | In Passivbot v7.2.13 the location of ohlcv data changed. Run this script to copy data already downloaded in earlier versions. 14 | 15 | ```shell 16 | python3 src/tools/copy_ohlcvs_from_v7.2.12.py 17 | ``` 18 | 19 | ## Generate list of approved coins based on market cap 20 | 21 | ```shell 22 | python3 src/tools/generate_mcap_list.py 23 | ``` 24 | 25 | Output from `python3 src/tools/generate_mcap_list.py -h`: 26 | ``` 27 | --n_coins N_COINS, -n N_COINS 28 | Maxiumum number of top market cap coins. Default=100 29 | --minimum_market_cap_dollars MINIMUM_MARKET_CAP_MILLIONS, -m MINIMUM_MARKET_CAP_MILLIONS 30 | Minimum market cap in millions of USD. Default=300.0 31 | --exchange EXCHANGE, -e EXCHANGE 32 | Optional: filter by coins available on exchange. Comma separated values. Default=None 33 | --output OUTPUT, -o OUTPUT 34 | Optional: Output path. Default=configs/approved_coins_{n_coins}_{min_mcap}.json 35 | ``` 36 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Passivbot 2 | site_url: https://www.passivbot.com 3 | repo_url: https://github.com/enarjord/passivbot 4 | use_directory_urls: True 5 | docs_dir: docs 6 | nav: 7 | - Home: README.md 8 | - Configuration: configuration.md 9 | - Running live: live.md 10 | - Backtesting: backtesting.md 11 | - Optimizing: optimizing.md 12 | theme: 13 | name: material 14 | logo: 'images/logo.png' 15 | favicon: 'images/logo.png' 16 | custom_dir: 'docs/overrides' 17 | palette: 18 | primary: 'blue grey' 19 | accent: 'tear' 20 | markdown_extensions: 21 | - attr_list 22 | - admonition 23 | - toc: 24 | permalink: True 25 | - pymdownx.inlinehilite 26 | - pymdownx.superfences 27 | extra: 28 | version: 29 | provider: mike 30 | -------------------------------------------------------------------------------- /notebooks/notebook_setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | # Change to the project root directory 5 | project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) 6 | os.chdir(project_root) 7 | 8 | # Add the project root and src directories to Python's path 9 | if project_root not in sys.path: 10 | sys.path.insert(0, project_root) 11 | if os.path.join(project_root, "src") not in sys.path: 12 | sys.path.insert(0, os.path.join(project_root, "src")) 13 | 14 | print(f"Working directory set to: {os.getcwd()}") 15 | print(f"Python path: {sys.path}") 16 | -------------------------------------------------------------------------------- /notebooks/notes_backtest.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "6a925c67-ca70-4352-a481-ab08a57122f9", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "%load_ext autoreload\n", 11 | "%autoreload 2\n", 12 | "%run notebook_setup.py" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": null, 18 | "id": "c351e684-c495-465a-bfde-7e03888aff15", 19 | "metadata": {}, 20 | "outputs": [], 21 | "source": [ 22 | "import sys\n", 23 | "import os\n", 24 | "from backtest import *" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": null, 30 | "id": "97f846a3-874c-48f4-93d8-829edc0b4bce", 31 | "metadata": { 32 | "scrolled": true 33 | }, 34 | "outputs": [], 35 | "source": [ 36 | "config = load_config('configs/template.json')\n", 37 | "{k: config[k] for k in ['backtest', 'bot', 'live']}" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": null, 43 | "id": "60155aa0-a1d3-4763-b314-5e015643b527", 44 | "metadata": {}, 45 | "outputs": [], 46 | "source": [ 47 | "config['backtest']['start_date'] = '2024-01-01'\n", 48 | "#config['backtest']['end_date'] = '2025-01-25'\n", 49 | "config['backtest']['exchanges'] = ['binance', 'bybit']\n", 50 | "exchange = 'combined' if config['backtest']['combine_ohlcvs'] else config['backtest']['exchanges'][0]" 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": null, 56 | "id": "4b7b1beb-b661-4871-9750-687d8eeafd3f", 57 | "metadata": {}, 58 | "outputs": [], 59 | "source": [ 60 | "coins, hlcvs, mss, results_path, cache_dir = await prepare_hlcvs_mss(config, exchange)\n", 61 | "config['backtest']['coins'] = {exchange: coins}" 62 | ] 63 | }, 64 | { 65 | "cell_type": "code", 66 | "execution_count": null, 67 | "id": "2f473002-2ba5-41e3-9f06-ff56aaea2318", 68 | "metadata": {}, 69 | "outputs": [], 70 | "source": [ 71 | "#config['bot']['long']['n_positions'] = 3\n", 72 | "#config['bot']['long']['filter_rolling_window'] = 10\n", 73 | "#config['bot']['long']['filter_relative_volume_clip_pct'] = 0.5\n", 74 | "#config['bot']['short']['n_positions'] = 0.0" 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": null, 80 | "id": "ef8d0f05-37ab-4a56-ab17-07a3cd0d70b7", 81 | "metadata": {}, 82 | "outputs": [], 83 | "source": [ 84 | "fills, equities, analysis = run_backtest(hlcvs, mss, config, exchange)" 85 | ] 86 | }, 87 | { 88 | "cell_type": "code", 89 | "execution_count": null, 90 | "id": "a8d185d1-1ef2-4b9f-973f-c6c13831ef9e", 91 | "metadata": {}, 92 | "outputs": [], 93 | "source": [ 94 | "analysis = expand_analysis(analysis, fills, config)" 95 | ] 96 | }, 97 | { 98 | "cell_type": "code", 99 | "execution_count": null, 100 | "id": "855f5f2d-df4b-4c09-9e18-06085981ae0f", 101 | "metadata": {}, 102 | "outputs": [], 103 | "source": [] 104 | }, 105 | { 106 | "cell_type": "code", 107 | "execution_count": null, 108 | "id": "b0a86c90-e834-4016-abfa-8158e2d227cb", 109 | "metadata": {}, 110 | "outputs": [], 111 | "source": [ 112 | "sts = utc_ms()\n", 113 | "fdf = process_forager_fills(fills)\n", 114 | "print(f'elapsed {utc_ms() - sts}')\n", 115 | "sts = utc_ms()\n", 116 | "equities = pd.Series(equities)\n", 117 | "analysis_py, balance_and_equity = analyze_fills_forager(config['backtest']['coins'], hlcvs, fdf, equities)\n", 118 | "for k in analysis_py:\n", 119 | " if k not in analysis:\n", 120 | " analysis[k] = analysis_py[k]\n", 121 | "print(f'elapsed {utc_ms() - sts}')\n", 122 | "pprint.pprint(analysis)\n", 123 | "balance_and_equity.plot()" 124 | ] 125 | }, 126 | { 127 | "cell_type": "code", 128 | "execution_count": null, 129 | "id": "7aa52402-c254-4ffa-8393-cef7f9428b1c", 130 | "metadata": {}, 131 | "outputs": [], 132 | "source": [ 133 | "coins_sorted_by_volume = fdf.groupby('coin').fee_paid.sum().sort_values().index.to_list()\n", 134 | "for i, coin in enumerate(coins_sorted_by_volume[:5]):\n", 135 | " print(f\"Plotting fills for {coin}\")\n", 136 | " hlcvs_df = pd.DataFrame(hlcvs[:, coins.index(coin), :], columns=[\"high\", \"low\", \"close\", \"volume\"])\n", 137 | " fdfc = fdf[fdf.coin == coin]\n", 138 | " plt.clf()\n", 139 | " plot_fills_forager(fdfc, hlcvs_df)\n", 140 | " plt.title(f\"Fills {coin}\")\n", 141 | " plt.xlabel = \"time\"\n", 142 | " plt.ylabel = \"price\"\n", 143 | " plt.show()\n" 144 | ] 145 | }, 146 | { 147 | "cell_type": "code", 148 | "execution_count": null, 149 | "id": "93a8c7de-b1a3-4cda-a4ed-4f1a43bf5578", 150 | "metadata": {}, 151 | "outputs": [], 152 | "source": [ 153 | "# performers worst to best\n", 154 | "for x in fdf.groupby('coin').pnl.sum().sort_values().to_dict().items():\n", 155 | " print(x)" 156 | ] 157 | }, 158 | { 159 | "cell_type": "code", 160 | "execution_count": null, 161 | "id": "ad2bb976-de7a-4e4f-a467-bb401c25ae65", 162 | "metadata": {}, 163 | "outputs": [], 164 | "source": [] 165 | } 166 | ], 167 | "metadata": { 168 | "kernelspec": { 169 | "display_name": "Python (venv)", 170 | "language": "python", 171 | "name": "venv" 172 | }, 173 | "language_info": { 174 | "codemirror_mode": { 175 | "name": "ipython", 176 | "version": 3 177 | }, 178 | "file_extension": ".py", 179 | "mimetype": "text/x-python", 180 | "name": "python", 181 | "nbconvert_exporter": "python", 182 | "pygments_lexer": "ipython3", 183 | "version": "3.9.18" 184 | } 185 | }, 186 | "nbformat": 4, 187 | "nbformat_minor": 5 188 | } 189 | -------------------------------------------------------------------------------- /passivbot-rust/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "autocfg" 7 | version = "1.3.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 10 | 11 | [[package]] 12 | name = "bitflags" 13 | version = "2.5.0" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" 16 | 17 | [[package]] 18 | name = "cfg-if" 19 | version = "1.0.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 22 | 23 | [[package]] 24 | name = "heck" 25 | version = "0.4.1" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 28 | 29 | [[package]] 30 | name = "indoc" 31 | version = "2.0.5" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" 34 | 35 | [[package]] 36 | name = "libc" 37 | version = "0.2.155" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" 40 | 41 | [[package]] 42 | name = "lock_api" 43 | version = "0.4.12" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 46 | dependencies = [ 47 | "autocfg", 48 | "scopeguard", 49 | ] 50 | 51 | [[package]] 52 | name = "matrixmultiply" 53 | version = "0.3.8" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "7574c1cf36da4798ab73da5b215bbf444f50718207754cb522201d78d1cd0ff2" 56 | dependencies = [ 57 | "autocfg", 58 | "rawpointer", 59 | ] 60 | 61 | [[package]] 62 | name = "memmap" 63 | version = "0.7.0" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "6585fd95e7bb50d6cc31e20d4cf9afb4e2ba16c5846fc76793f11218da9c475b" 66 | dependencies = [ 67 | "libc", 68 | "winapi", 69 | ] 70 | 71 | [[package]] 72 | name = "memoffset" 73 | version = "0.9.1" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" 76 | dependencies = [ 77 | "autocfg", 78 | ] 79 | 80 | [[package]] 81 | name = "ndarray" 82 | version = "0.15.6" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32" 85 | dependencies = [ 86 | "matrixmultiply", 87 | "num-complex", 88 | "num-integer", 89 | "num-traits", 90 | "rawpointer", 91 | ] 92 | 93 | [[package]] 94 | name = "num-complex" 95 | version = "0.4.6" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" 98 | dependencies = [ 99 | "num-traits", 100 | ] 101 | 102 | [[package]] 103 | name = "num-integer" 104 | version = "0.1.46" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 107 | dependencies = [ 108 | "num-traits", 109 | ] 110 | 111 | [[package]] 112 | name = "num-traits" 113 | version = "0.2.19" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 116 | dependencies = [ 117 | "autocfg", 118 | ] 119 | 120 | [[package]] 121 | name = "numpy" 122 | version = "0.21.0" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "ec170733ca37175f5d75a5bea5911d6ff45d2cd52849ce98b685394e4f2f37f4" 125 | dependencies = [ 126 | "libc", 127 | "ndarray", 128 | "num-complex", 129 | "num-integer", 130 | "num-traits", 131 | "pyo3", 132 | "rustc-hash", 133 | ] 134 | 135 | [[package]] 136 | name = "once_cell" 137 | version = "1.19.0" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 140 | 141 | [[package]] 142 | name = "parking_lot" 143 | version = "0.12.3" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 146 | dependencies = [ 147 | "lock_api", 148 | "parking_lot_core", 149 | ] 150 | 151 | [[package]] 152 | name = "parking_lot_core" 153 | version = "0.9.10" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 156 | dependencies = [ 157 | "cfg-if", 158 | "libc", 159 | "redox_syscall", 160 | "smallvec", 161 | "windows-targets", 162 | ] 163 | 164 | [[package]] 165 | name = "passivbot_rust" 166 | version = "0.1.0" 167 | dependencies = [ 168 | "memmap", 169 | "ndarray", 170 | "numpy", 171 | "pyo3", 172 | ] 173 | 174 | [[package]] 175 | name = "portable-atomic" 176 | version = "1.6.0" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" 179 | 180 | [[package]] 181 | name = "proc-macro2" 182 | version = "1.0.84" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6" 185 | dependencies = [ 186 | "unicode-ident", 187 | ] 188 | 189 | [[package]] 190 | name = "pyo3" 191 | version = "0.21.2" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "a5e00b96a521718e08e03b1a622f01c8a8deb50719335de3f60b3b3950f069d8" 194 | dependencies = [ 195 | "cfg-if", 196 | "indoc", 197 | "libc", 198 | "memoffset", 199 | "parking_lot", 200 | "portable-atomic", 201 | "pyo3-build-config", 202 | "pyo3-ffi", 203 | "pyo3-macros", 204 | "unindent", 205 | ] 206 | 207 | [[package]] 208 | name = "pyo3-build-config" 209 | version = "0.21.2" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "7883df5835fafdad87c0d888b266c8ec0f4c9ca48a5bed6bbb592e8dedee1b50" 212 | dependencies = [ 213 | "once_cell", 214 | "target-lexicon", 215 | ] 216 | 217 | [[package]] 218 | name = "pyo3-ffi" 219 | version = "0.21.2" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "01be5843dc60b916ab4dad1dca6d20b9b4e6ddc8e15f50c47fe6d85f1fb97403" 222 | dependencies = [ 223 | "libc", 224 | "pyo3-build-config", 225 | ] 226 | 227 | [[package]] 228 | name = "pyo3-macros" 229 | version = "0.21.2" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "77b34069fc0682e11b31dbd10321cbf94808394c56fd996796ce45217dfac53c" 232 | dependencies = [ 233 | "proc-macro2", 234 | "pyo3-macros-backend", 235 | "quote", 236 | "syn", 237 | ] 238 | 239 | [[package]] 240 | name = "pyo3-macros-backend" 241 | version = "0.21.2" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "08260721f32db5e1a5beae69a55553f56b99bd0e1c3e6e0a5e8851a9d0f5a85c" 244 | dependencies = [ 245 | "heck", 246 | "proc-macro2", 247 | "pyo3-build-config", 248 | "quote", 249 | "syn", 250 | ] 251 | 252 | [[package]] 253 | name = "quote" 254 | version = "1.0.36" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 257 | dependencies = [ 258 | "proc-macro2", 259 | ] 260 | 261 | [[package]] 262 | name = "rawpointer" 263 | version = "0.2.1" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" 266 | 267 | [[package]] 268 | name = "redox_syscall" 269 | version = "0.5.1" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" 272 | dependencies = [ 273 | "bitflags", 274 | ] 275 | 276 | [[package]] 277 | name = "rustc-hash" 278 | version = "1.1.0" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 281 | 282 | [[package]] 283 | name = "scopeguard" 284 | version = "1.2.0" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 287 | 288 | [[package]] 289 | name = "smallvec" 290 | version = "1.13.2" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 293 | 294 | [[package]] 295 | name = "syn" 296 | version = "2.0.66" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" 299 | dependencies = [ 300 | "proc-macro2", 301 | "quote", 302 | "unicode-ident", 303 | ] 304 | 305 | [[package]] 306 | name = "target-lexicon" 307 | version = "0.12.14" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" 310 | 311 | [[package]] 312 | name = "unicode-ident" 313 | version = "1.0.12" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 316 | 317 | [[package]] 318 | name = "unindent" 319 | version = "0.2.3" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" 322 | 323 | [[package]] 324 | name = "winapi" 325 | version = "0.3.9" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 328 | dependencies = [ 329 | "winapi-i686-pc-windows-gnu", 330 | "winapi-x86_64-pc-windows-gnu", 331 | ] 332 | 333 | [[package]] 334 | name = "winapi-i686-pc-windows-gnu" 335 | version = "0.4.0" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 338 | 339 | [[package]] 340 | name = "winapi-x86_64-pc-windows-gnu" 341 | version = "0.4.0" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 344 | 345 | [[package]] 346 | name = "windows-targets" 347 | version = "0.52.5" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 350 | dependencies = [ 351 | "windows_aarch64_gnullvm", 352 | "windows_aarch64_msvc", 353 | "windows_i686_gnu", 354 | "windows_i686_gnullvm", 355 | "windows_i686_msvc", 356 | "windows_x86_64_gnu", 357 | "windows_x86_64_gnullvm", 358 | "windows_x86_64_msvc", 359 | ] 360 | 361 | [[package]] 362 | name = "windows_aarch64_gnullvm" 363 | version = "0.52.5" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" 366 | 367 | [[package]] 368 | name = "windows_aarch64_msvc" 369 | version = "0.52.5" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" 372 | 373 | [[package]] 374 | name = "windows_i686_gnu" 375 | version = "0.52.5" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" 378 | 379 | [[package]] 380 | name = "windows_i686_gnullvm" 381 | version = "0.52.5" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" 384 | 385 | [[package]] 386 | name = "windows_i686_msvc" 387 | version = "0.52.5" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" 390 | 391 | [[package]] 392 | name = "windows_x86_64_gnu" 393 | version = "0.52.5" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" 396 | 397 | [[package]] 398 | name = "windows_x86_64_gnullvm" 399 | version = "0.52.5" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" 402 | 403 | [[package]] 404 | name = "windows_x86_64_msvc" 405 | version = "0.52.5" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" 408 | -------------------------------------------------------------------------------- /passivbot-rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "passivbot_rust" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | name = "passivbot_rust" 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | pyo3 = { version = "0.21.2", features = ["extension-module"] } 12 | ndarray = "0.15.6" 13 | numpy = "0.21.0" 14 | memmap = "0.7.0" -------------------------------------------------------------------------------- /passivbot-rust/src/constants.rs: -------------------------------------------------------------------------------- 1 | pub const HIGH: usize = 0; 2 | pub const LOW: usize = 1; 3 | pub const CLOSE: usize = 2; 4 | pub const VOLUME: usize = 3; 5 | 6 | pub const LONG: usize = 0; 7 | pub const SHORT: usize = 1; 8 | pub const NO_POS: usize = 2; 9 | -------------------------------------------------------------------------------- /passivbot-rust/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod backtest; 2 | mod closes; 3 | mod constants; 4 | mod entries; 5 | mod python; 6 | mod types; 7 | mod utils; 8 | 9 | use backtest::*; 10 | use closes::*; 11 | use entries::*; 12 | use pyo3::prelude::*; 13 | use pyo3::wrap_pyfunction; 14 | use python::*; 15 | use utils::*; 16 | 17 | /// A Python module implemented in Rust. 18 | #[pymodule] 19 | fn passivbot_rust(m: &Bound<'_, PyModule>) -> PyResult<()> { 20 | m.add_function(wrap_pyfunction!(round_, m)?)?; 21 | m.add_function(wrap_pyfunction!(round_up, m)?)?; 22 | m.add_function(wrap_pyfunction!(round_dn, m)?)?; 23 | m.add_function(wrap_pyfunction!(round_dynamic, m)?)?; 24 | m.add_function(wrap_pyfunction!(round_dynamic_up, m)?)?; 25 | m.add_function(wrap_pyfunction!(round_dynamic_dn, m)?)?; 26 | m.add_function(wrap_pyfunction!(calc_diff, m)?)?; 27 | m.add_function(wrap_pyfunction!(qty_to_cost, m)?)?; 28 | m.add_function(wrap_pyfunction!(cost_to_qty, m)?)?; 29 | m.add_function(wrap_pyfunction!(calc_pnl_long, m)?)?; 30 | m.add_function(wrap_pyfunction!(calc_pnl_short, m)?)?; 31 | m.add_function(wrap_pyfunction!(calc_wallet_exposure, m)?)?; 32 | m.add_function(wrap_pyfunction!(calc_new_psize_pprice, m)?)?; 33 | m.add_function(wrap_pyfunction!(calc_grid_entry_long_py, m)?)?; 34 | m.add_function(wrap_pyfunction!(calc_trailing_entry_long_py, m)?)?; 35 | m.add_function(wrap_pyfunction!(calc_next_entry_long_py, m)?)?; 36 | m.add_function(wrap_pyfunction!(calc_trailing_close_long_py, m)?)?; 37 | m.add_function(wrap_pyfunction!(calc_grid_close_long_py, m)?)?; 38 | m.add_function(wrap_pyfunction!(calc_next_close_long_py, m)?)?; 39 | m.add_function(wrap_pyfunction!(calc_entries_long_py, m)?)?; 40 | m.add_function(wrap_pyfunction!(calc_next_entry_short_py, m)?)?; 41 | m.add_function(wrap_pyfunction!(calc_next_close_short_py, m)?)?; 42 | m.add_function(wrap_pyfunction!(calc_entries_short_py, m)?)?; 43 | m.add_function(wrap_pyfunction!(calc_closes_long_py, m)?)?; 44 | m.add_function(wrap_pyfunction!(calc_closes_short_py, m)?)?; 45 | m.add_function(wrap_pyfunction!(run_backtest, m)?)?; 46 | m.add_function(wrap_pyfunction!(calc_auto_unstuck_allowance, m)?)?; 47 | Ok(()) 48 | } 49 | -------------------------------------------------------------------------------- /passivbot-rust/src/types.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fmt; 3 | 4 | #[derive(Debug)] 5 | pub struct ExchangeParams { 6 | pub qty_step: f64, 7 | pub price_step: f64, 8 | pub min_qty: f64, 9 | pub min_cost: f64, 10 | pub c_mult: f64, 11 | } 12 | 13 | impl Default for ExchangeParams { 14 | fn default() -> Self { 15 | ExchangeParams { 16 | qty_step: 0.00001, 17 | price_step: 0.00001, 18 | min_qty: 0.00001, 19 | min_cost: 1.0, 20 | c_mult: 1.0, 21 | } 22 | } 23 | } 24 | 25 | #[derive(Clone)] 26 | pub struct BacktestParams { 27 | pub starting_balance: f64, 28 | pub maker_fee: f64, 29 | pub coins: Vec, 30 | } 31 | 32 | #[derive(Default, Debug, Clone, Copy)] 33 | pub struct Position { 34 | pub size: f64, 35 | pub price: f64, 36 | } 37 | 38 | #[derive(Debug, Default)] 39 | pub struct Positions { 40 | pub long: HashMap, 41 | pub short: HashMap, 42 | } 43 | 44 | #[derive(Debug, Default, Clone)] 45 | pub struct EMABands { 46 | pub upper: f64, 47 | pub lower: f64, 48 | } 49 | 50 | #[derive(Debug, Clone, Copy)] 51 | pub struct Order { 52 | pub qty: f64, 53 | pub price: f64, 54 | pub order_type: OrderType, 55 | } 56 | 57 | impl Order { 58 | pub fn new(qty: f64, price: f64, order_type: OrderType) -> Self { 59 | Order { 60 | qty, 61 | price, 62 | order_type, 63 | } 64 | } 65 | } 66 | 67 | impl Default for Order { 68 | fn default() -> Self { 69 | Order { 70 | qty: 0.0, 71 | price: 0.0, 72 | order_type: OrderType::Empty, 73 | } 74 | } 75 | } 76 | 77 | #[derive(Debug, Default, Clone)] 78 | pub struct OrderBook { 79 | pub bid: f64, 80 | pub ask: f64, 81 | } 82 | 83 | #[derive(Debug, Default, Clone)] 84 | pub struct StateParams { 85 | pub balance: f64, 86 | pub order_book: OrderBook, 87 | pub ema_bands: EMABands, 88 | } 89 | 90 | #[derive(Clone, Default, Debug)] 91 | pub struct BotParamsPair { 92 | pub long: BotParams, 93 | pub short: BotParams, 94 | } 95 | 96 | #[derive(Clone, Default, Debug)] 97 | pub struct BotParams { 98 | pub close_grid_markup_range: f64, 99 | pub close_grid_min_markup: f64, 100 | pub close_grid_qty_pct: f64, 101 | pub close_trailing_retracement_pct: f64, 102 | pub close_trailing_grid_ratio: f64, 103 | pub close_trailing_qty_pct: f64, 104 | pub close_trailing_threshold_pct: f64, 105 | pub enforce_exposure_limit: bool, 106 | pub entry_grid_double_down_factor: f64, 107 | pub entry_grid_spacing_weight: f64, 108 | pub entry_grid_spacing_pct: f64, 109 | pub entry_initial_ema_dist: f64, 110 | pub entry_initial_qty_pct: f64, 111 | pub entry_trailing_retracement_pct: f64, 112 | pub entry_trailing_grid_ratio: f64, 113 | pub entry_trailing_threshold_pct: f64, 114 | pub filter_rolling_window: usize, 115 | pub filter_relative_volume_clip_pct: f64, 116 | pub ema_span_0: f64, 117 | pub ema_span_1: f64, 118 | pub n_positions: usize, 119 | pub total_wallet_exposure_limit: f64, 120 | pub wallet_exposure_limit: f64, // is total_wallet_exposure_limit / n_positions 121 | pub unstuck_close_pct: f64, 122 | pub unstuck_ema_dist: f64, 123 | pub unstuck_loss_allowance_pct: f64, 124 | pub unstuck_threshold: f64, 125 | } 126 | 127 | #[derive(Debug)] 128 | pub struct TrailingPriceBundle { 129 | pub min_since_open: f64, 130 | pub max_since_min: f64, 131 | pub max_since_open: f64, 132 | pub min_since_max: f64, 133 | } 134 | impl Default for TrailingPriceBundle { 135 | fn default() -> Self { 136 | TrailingPriceBundle { 137 | min_since_open: f64::MAX, 138 | max_since_min: 0.0, 139 | max_since_open: 0.0, 140 | min_since_max: f64::MAX, 141 | } 142 | } 143 | } 144 | 145 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] 146 | pub enum OrderType { 147 | EntryInitialNormalLong, 148 | EntryInitialPartialLong, 149 | EntryTrailingNormalLong, 150 | EntryTrailingCroppedLong, 151 | EntryGridNormalLong, 152 | EntryGridCroppedLong, 153 | EntryGridInflatedLong, 154 | 155 | CloseGridLong, 156 | CloseTrailingLong, 157 | CloseUnstuckLong, 158 | CloseAutoReduceLong, 159 | 160 | EntryInitialNormalShort, 161 | EntryInitialPartialShort, 162 | EntryTrailingNormalShort, 163 | EntryTrailingCroppedShort, 164 | EntryGridNormalShort, 165 | EntryGridCroppedShort, 166 | EntryGridInflatedShort, 167 | 168 | CloseGridShort, 169 | CloseTrailingShort, 170 | CloseUnstuckShort, 171 | CloseAutoReduceShort, 172 | 173 | Empty, 174 | } 175 | 176 | impl fmt::Display for OrderType { 177 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 178 | match self { 179 | OrderType::EntryInitialNormalLong => write!(f, "entry_initial_normal_long"), 180 | OrderType::EntryInitialPartialLong => write!(f, "entry_initial_partial_long"), 181 | OrderType::EntryTrailingNormalLong => write!(f, "entry_trailing_normal_long"), 182 | OrderType::EntryTrailingCroppedLong => write!(f, "entry_trailing_cropped_long"), 183 | OrderType::EntryGridNormalLong => write!(f, "entry_grid_normal_long"), 184 | OrderType::EntryGridCroppedLong => write!(f, "entry_grid_cropped_long"), 185 | OrderType::EntryGridInflatedLong => write!(f, "entry_grid_inflated_long"), 186 | OrderType::CloseGridLong => write!(f, "close_grid_long"), 187 | OrderType::CloseTrailingLong => write!(f, "close_trailing_long"), 188 | OrderType::CloseUnstuckLong => write!(f, "close_unstuck_long"), 189 | OrderType::CloseAutoReduceLong => write!(f, "close_auto_reduce_long"), 190 | OrderType::EntryInitialNormalShort => write!(f, "entry_initial_normal_short"), 191 | OrderType::EntryInitialPartialShort => write!(f, "entry_initial_partial_short"), 192 | OrderType::EntryTrailingNormalShort => write!(f, "entry_trailing_normal_short"), 193 | OrderType::EntryTrailingCroppedShort => write!(f, "entry_trailing_cropped_short"), 194 | OrderType::EntryGridNormalShort => write!(f, "entry_grid_normal_short"), 195 | OrderType::EntryGridCroppedShort => write!(f, "entry_grid_cropped_short"), 196 | OrderType::EntryGridInflatedShort => write!(f, "entry_grid_inflated_short"), 197 | OrderType::CloseGridShort => write!(f, "close_grid_short"), 198 | OrderType::CloseTrailingShort => write!(f, "close_trailing_short"), 199 | OrderType::CloseUnstuckShort => write!(f, "close_unstuck_short"), 200 | OrderType::CloseAutoReduceShort => write!(f, "close_auto_reduce_short"), 201 | OrderType::Empty => write!(f, "empty"), 202 | } 203 | } 204 | } 205 | 206 | #[derive(Debug, Clone)] 207 | pub struct Fill { 208 | pub index: usize, 209 | pub coin: String, 210 | pub pnl: f64, 211 | pub fee_paid: f64, 212 | pub balance: f64, 213 | pub fill_qty: f64, 214 | pub fill_price: f64, 215 | pub position_size: f64, 216 | pub position_price: f64, 217 | pub order_type: OrderType, 218 | } 219 | 220 | #[derive(Debug, Clone)] 221 | pub struct Analysis { 222 | pub adg: f64, 223 | pub mdg: f64, 224 | pub gain: f64, 225 | pub sharpe_ratio: f64, 226 | pub sortino_ratio: f64, 227 | pub omega_ratio: f64, 228 | pub expected_shortfall_1pct: f64, 229 | pub calmar_ratio: f64, 230 | pub sterling_ratio: f64, 231 | pub drawdown_worst: f64, 232 | pub drawdown_worst_mean_1pct: f64, 233 | pub equity_balance_diff_neg_max: f64, 234 | pub equity_balance_diff_neg_mean: f64, 235 | pub equity_balance_diff_pos_max: f64, 236 | pub equity_balance_diff_pos_mean: f64, 237 | pub loss_profit_ratio: f64, 238 | 239 | pub positions_held_per_day: f64, 240 | pub position_held_hours_mean: f64, 241 | pub position_held_hours_max: f64, 242 | pub position_held_hours_median: f64, 243 | 244 | pub adg_w: f64, 245 | pub mdg_w: f64, 246 | pub sharpe_ratio_w: f64, 247 | pub sortino_ratio_w: f64, 248 | pub omega_ratio_w: f64, 249 | pub calmar_ratio_w: f64, 250 | pub sterling_ratio_w: f64, 251 | pub loss_profit_ratio_w: f64, 252 | } 253 | 254 | impl Default for Analysis { 255 | fn default() -> Self { 256 | Analysis { 257 | adg: 0.0, 258 | mdg: 0.0, 259 | gain: 0.0, 260 | sharpe_ratio: 0.0, 261 | sortino_ratio: 0.0, 262 | omega_ratio: 0.0, 263 | expected_shortfall_1pct: 0.0, 264 | calmar_ratio: 0.0, 265 | sterling_ratio: 0.0, 266 | drawdown_worst: 1.0, 267 | drawdown_worst_mean_1pct: 1.0, 268 | equity_balance_diff_neg_max: 1.0, 269 | equity_balance_diff_neg_mean: 1.0, 270 | equity_balance_diff_pos_max: 1.0, 271 | equity_balance_diff_pos_mean: 1.0, 272 | loss_profit_ratio: 1.0, 273 | positions_held_per_day: 0.0, 274 | position_held_hours_mean: 0.0, 275 | position_held_hours_max: 0.0, 276 | position_held_hours_median: 0.0, 277 | 278 | adg_w: 0.0, 279 | mdg_w: 0.0, 280 | sharpe_ratio_w: 0.0, 281 | sortino_ratio_w: 0.0, 282 | omega_ratio_w: 0.0, 283 | calmar_ratio_w: 0.0, 284 | sterling_ratio_w: 0.0, 285 | loss_profit_ratio_w: 1.0, 286 | } 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /passivbot-rust/src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::{CLOSE, LONG, NO_POS, SHORT}; 2 | use crate::types::ExchangeParams; 3 | use pyo3::prelude::*; 4 | 5 | /// Rounds a number to the specified number of decimal places. 6 | fn round_to_decimal_places(value: f64, decimal_places: usize) -> f64 { 7 | let multiplier = 10f64.powi(decimal_places as i32); 8 | (value * multiplier).round() / multiplier 9 | } 10 | 11 | /// Rounds up a number to the nearest multiple of the given step. 12 | #[pyfunction] 13 | pub fn round_up(n: f64, step: f64) -> f64 { 14 | let result = (n / step).ceil() * step; 15 | round_to_decimal_places(result, 10) 16 | } 17 | 18 | /// Rounds a number to the nearest multiple of the given step. 19 | #[pyfunction] 20 | pub fn round_(n: f64, step: f64) -> f64 { 21 | let result = (n / step).round() * step; 22 | round_to_decimal_places(result, 10) 23 | } 24 | 25 | /// Rounds down a number to the nearest multiple of the given step. 26 | #[pyfunction] 27 | pub fn round_dn(n: f64, step: f64) -> f64 { 28 | let result = (n / step).floor() * step; 29 | round_to_decimal_places(result, 10) 30 | } 31 | 32 | #[pyfunction] 33 | pub fn round_dynamic(n: f64, d: i32) -> f64 { 34 | if n == 0.0 { 35 | return n; 36 | } 37 | let shift = d - (n.abs().log10().floor() as i32) - 1; 38 | let multiplier = 10f64.powi(shift); 39 | let result = (n * multiplier).round() / multiplier; 40 | round_to_decimal_places(result, 10) 41 | } 42 | 43 | #[pyfunction] 44 | pub fn round_dynamic_up(n: f64, d: i32) -> f64 { 45 | if n == 0.0 { 46 | return n; 47 | } 48 | let shift = d - (n.abs().log10().floor() as i32) - 1; 49 | let multiplier = 10f64.powi(shift); 50 | let result = (n * multiplier).ceil() / multiplier; 51 | round_to_decimal_places(result, 10) 52 | } 53 | 54 | #[pyfunction] 55 | pub fn round_dynamic_dn(n: f64, d: i32) -> f64 { 56 | if n == 0.0 { 57 | return n; 58 | } 59 | let shift = d - (n.abs().log10().floor() as i32) - 1; 60 | let multiplier = 10f64.powi(shift); 61 | let result = (n * multiplier).floor() / multiplier; 62 | round_to_decimal_places(result, 10) 63 | } 64 | 65 | #[pyfunction] 66 | pub fn calc_diff(x: f64, y: f64) -> f64 { 67 | if y == 0.0 { 68 | if x == 0.0 { 69 | 0.0 70 | } else { 71 | f64::INFINITY 72 | } 73 | } else { 74 | (x - y).abs() / y.abs() 75 | } 76 | } 77 | 78 | #[pyfunction] 79 | pub fn cost_to_qty(cost: f64, price: f64, c_mult: f64) -> f64 { 80 | if price > 0.0 { 81 | (cost.abs() / price) / c_mult 82 | } else { 83 | 0.0 84 | } 85 | } 86 | 87 | #[pyfunction] 88 | pub fn qty_to_cost(qty: f64, price: f64, c_mult: f64) -> f64 { 89 | (qty.abs() * price) * c_mult 90 | } 91 | 92 | #[pyfunction] 93 | pub fn calc_wallet_exposure( 94 | c_mult: f64, 95 | balance: f64, 96 | position_size: f64, 97 | position_price: f64, 98 | ) -> f64 { 99 | if balance <= 0.0 || position_size == 0.0 { 100 | return 0.0; 101 | } 102 | qty_to_cost(position_size, position_price, c_mult) / balance 103 | } 104 | 105 | pub fn calc_wallet_exposure_if_filled( 106 | balance: f64, 107 | psize: f64, 108 | pprice: f64, 109 | qty: f64, 110 | price: f64, 111 | exchange_params: &ExchangeParams, 112 | ) -> f64 { 113 | let psize = round_(psize.abs(), exchange_params.qty_step); 114 | let qty = round_(qty.abs(), exchange_params.qty_step); 115 | let (new_psize, new_pprice) = 116 | calc_new_psize_pprice(psize, pprice, qty, price, exchange_params.qty_step); 117 | calc_wallet_exposure(exchange_params.c_mult, balance, new_psize, new_pprice) 118 | } 119 | 120 | #[pyfunction] 121 | pub fn calc_new_psize_pprice( 122 | psize: f64, 123 | pprice: f64, 124 | qty: f64, 125 | price: f64, 126 | qty_step: f64, 127 | ) -> (f64, f64) { 128 | if qty == 0.0 { 129 | return (psize, pprice); 130 | } 131 | if psize == 0.0 { 132 | return (qty, price); 133 | } 134 | let new_psize = round_(psize + qty, qty_step); 135 | if new_psize == 0.0 { 136 | return (0.0, 0.0); 137 | } 138 | ( 139 | new_psize, 140 | nan_to_0(pprice) * (psize / new_psize) + price * (qty / new_psize), 141 | ) 142 | } 143 | 144 | fn nan_to_0(value: f64) -> f64 { 145 | if value.is_nan() { 146 | 0.0 147 | } else { 148 | value 149 | } 150 | } 151 | 152 | pub fn interpolate(x: f64, xs: &[f64], ys: &[f64]) -> f64 { 153 | assert_eq!(xs.len(), ys.len(), "xs and ys must have the same length"); 154 | 155 | let n = xs.len(); 156 | let mut result = 0.0; 157 | 158 | for i in 0..n { 159 | let mut term = ys[i]; 160 | for j in 0..n { 161 | if i != j { 162 | term *= (x - xs[j]) / (xs[i] - xs[j]); 163 | } 164 | } 165 | result += term; 166 | } 167 | 168 | result 169 | } 170 | 171 | #[pyfunction] 172 | pub fn calc_pnl_long(entry_price: f64, close_price: f64, qty: f64, c_mult: f64) -> f64 { 173 | qty.abs() * c_mult * (close_price - entry_price) 174 | } 175 | 176 | #[pyfunction] 177 | pub fn calc_pnl_short(entry_price: f64, close_price: f64, qty: f64, c_mult: f64) -> f64 { 178 | qty.abs() * c_mult * (entry_price - close_price) 179 | } 180 | 181 | pub fn calc_pprice_diff_int(pside: usize, pprice: f64, price: f64) -> f64 { 182 | match pside { 183 | LONG => { 184 | // long 185 | if pprice > 0.0 { 186 | 1.0 - price / pprice 187 | } else { 188 | 0.0 189 | } 190 | } 191 | SHORT => { 192 | // short 193 | if pprice > 0.0 { 194 | price / pprice - 1.0 195 | } else { 196 | 0.0 197 | } 198 | } 199 | _ => panic!("unknown pside {}", pside), 200 | } 201 | } 202 | 203 | #[pyfunction] 204 | pub fn calc_auto_unstuck_allowance( 205 | balance: f64, 206 | loss_allowance_pct: f64, 207 | pnl_cumsum_max: f64, 208 | pnl_cumsum_last: f64, 209 | ) -> f64 { 210 | // allow up to x% drop from balance peak for auto unstuck 211 | 212 | let balance_peak = balance + (pnl_cumsum_max - pnl_cumsum_last); 213 | let drop_since_peak_pct = balance / balance_peak - 1.0; 214 | (balance_peak * (loss_allowance_pct + drop_since_peak_pct)).max(0.0) 215 | } 216 | 217 | pub fn calc_ema_price_bid( 218 | price_step: f64, 219 | order_book_bid: f64, 220 | ema_bands_lower: f64, 221 | ema_dist: f64, 222 | ) -> f64 { 223 | f64::min( 224 | order_book_bid, 225 | round_dn(ema_bands_lower * (1.0 - ema_dist), price_step), 226 | ) 227 | } 228 | 229 | pub fn calc_ema_price_ask( 230 | price_step: f64, 231 | order_book_ask: f64, 232 | ema_bands_upper: f64, 233 | ema_dist: f64, 234 | ) -> f64 { 235 | f64::max( 236 | order_book_ask, 237 | round_up(ema_bands_upper * (1.0 + ema_dist), price_step), 238 | ) 239 | } 240 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | matplotlib==3.5.1 2 | prospector==1.6.0 3 | colorama==0.4.4 4 | mkdocs==1.4.0 5 | mkdocs-material==8.1.8 6 | pymdown-extensions==9.1 7 | tqdm==4.62.3 8 | python-dateutil==2.8.2 9 | numba==0.59.1; python_version>='3.9' 10 | numba==0.58.1; python_version<'3.9' 11 | pandas==1.4.0 12 | PyYAML==6.0 13 | asyncio==3.4.3 14 | pyecharts==1.9.1 15 | deap==1.4.1 16 | websockets==10.1 17 | aiohttp==3.8.1 18 | numpy==1.22.4 19 | ccxt==4.4.39 20 | hjson==3.0.2 21 | prettytable==3.0.0 22 | maturin==1.5.1 23 | sortedcontainers==2.4.0 24 | dictdiffer==0.9.0 25 | openpyxl==3.1.5 26 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from setuptools_rust import RustExtension 3 | 4 | 5 | def parse_requirements(filename): 6 | with open(filename, "r") as file: 7 | return [line.strip() for line in file if line.strip() and not line.startswith("#")] 8 | 9 | 10 | setup( 11 | name="passivbot", 12 | version="0.1.0", 13 | packages=find_packages(where="src"), 14 | package_dir={"": "src"}, 15 | rust_extensions=[ 16 | RustExtension("passivbot_rust", path="passivbot-rust/Cargo.toml", binding="pyo3") 17 | ], 18 | install_requires=parse_requirements("requirements.txt"), 19 | setup_requires=["setuptools-rust>=1.9.0", "wheel"], 20 | include_package_data=True, 21 | zip_safe=False, 22 | ) 23 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/postnext/Bitget/e704209a092f0063f248b656679db752e441f279/src/__init__.py -------------------------------------------------------------------------------- /src/auto_profit_transfer.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | if "NOJIT" not in os.environ: 4 | os.environ["NOJIT"] = "true" 5 | 6 | import traceback 7 | import json 8 | import argparse 9 | import asyncio 10 | from procedures import ( 11 | create_binance_bot, 12 | create_bybit_bot, 13 | make_get_filepath, 14 | load_exchange_key_secret_passphrase, 15 | load_user_info, 16 | utc_ms, 17 | ) 18 | from passivbot import setup_bot 19 | from pure_funcs import get_template_live_config, flatten 20 | from njit_funcs import round_dynamic 21 | from time import sleep 22 | import pprint 23 | import logging 24 | import logging.config 25 | 26 | 27 | async def main(): 28 | logging.basicConfig( 29 | format="%(asctime)s %(levelname)-8s %(message)s", 30 | level=logging.INFO, 31 | datefmt="%Y-%m-%dT%H:%M:%S", 32 | ) 33 | parser = argparse.ArgumentParser( 34 | prog="auto profit transfer", 35 | description="automatically transfer percentage of profits from futures wallet to spot wallet", 36 | ) 37 | parser.add_argument("user", type=str, help="user/account_name defined in api-keys.json") 38 | parser.add_argument( 39 | "-p", 40 | "--percentage", 41 | type=float, 42 | required=False, 43 | default=0.5, 44 | dest="percentage", 45 | help="per uno, i.e. 0.02==2 per cent. default=0.5", 46 | ) 47 | parser.add_argument( 48 | "-q", 49 | "--quote", 50 | type=str, 51 | required=False, 52 | default="USDT", 53 | dest="quote", 54 | help="quote coin, default USDT", 55 | ) 56 | args = parser.parse_args() 57 | user_info = load_user_info(args.user) 58 | exchange = user_info["exchange"] 59 | pnls_fname = os.path.join("caches", exchange, args.user + "_pnls.json") 60 | transfer_log_fpath = make_get_filepath( 61 | os.path.join("logs", f"automatic_profit_transfer_log_{exchange}_{args.user}.json") 62 | ) 63 | try: 64 | already_transferred_ids = set(json.load(open(transfer_log_fpath))) 65 | logging.info(f"loaded already transferred IDs: {transfer_log_fpath}") 66 | except: 67 | already_transferred_ids = set() 68 | logging.info(f"no previous transfers to load") 69 | if exchange == "bybit": 70 | config = get_template_live_config("v7") 71 | config["user"] = args.user 72 | bot = setup_bot(config) 73 | await bot.determine_utc_offset() 74 | else: 75 | raise Exception(f"unsupported exchange {exchange}") 76 | day_ms = 1000 * 60 * 60 * 24 77 | sleep_time_error = 10 78 | while True: 79 | try: 80 | if os.path.exists(pnls_fname): 81 | pnls = json.load(open(f"caches/{user_info['exchange']}/{args.user}_pnls.json")) 82 | else: 83 | logging.info(f"pnls file does not exist {pnls_fname}") 84 | pnls = [] 85 | now = bot.get_exchange_time() 86 | pnls_last_24_h = [x for x in pnls if x["timestamp"] >= now - day_ms] 87 | pnls_last_24_h = [x for x in pnls_last_24_h if x["id"] not in already_transferred_ids] 88 | profit = sum([e["pnl"] for e in pnls_last_24_h]) 89 | to_transfer = round_dynamic(profit * args.percentage, 4) 90 | if args.quote in ["USDT", "BUSD", "USDC"]: 91 | to_transfer = round(to_transfer, 4) 92 | if to_transfer > 0: 93 | try: 94 | transferred = await bot.cca.transfer(args.quote, to_transfer, "CONTRACT", "SPOT") 95 | logging.info(f"pnl: {profit} transferred {to_transfer} {args.quote}") 96 | logging.info(f"{transferred}") 97 | already_transferred_ids.update([e["id"] for e in pnls_last_24_h]) 98 | json.dump(list(already_transferred_ids), open(transfer_log_fpath, "w")) 99 | except Exception as e: 100 | logging.error(f"failed transferring {e}") 101 | traceback.print_exc() 102 | else: 103 | logging.info("nothing to transfer") 104 | sleep(60 * 60) 105 | except Exception as e: 106 | logging.info(f"error with profit transfer {e}") 107 | logging.info(f"trying again in {sleep_time_error} minutes") 108 | sleep(60 * sleep_time_error) 109 | 110 | 111 | if __name__ == "__main__": 112 | asyncio.run(main()) 113 | -------------------------------------------------------------------------------- /src/balance_overview.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "%load_ext autoreload\n", 10 | "%autoreload 2" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "from procedures import (\n", 20 | " create_binance_bot,\n", 21 | " create_binance_bot_spot,\n", 22 | " create_bybit_bot,\n", 23 | " load_exchange_key_secret_passphrase,\n", 24 | ")\n", 25 | "from pure_funcs import get_template_live_config, ts_to_date, get_daily_from_income\n", 26 | "from njit_funcs import round_dynamic\n", 27 | "from time import time\n", 28 | "import pandas as pd\n", 29 | "import matplotlib.pyplot as plt" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": null, 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "plt.rcParams[\"figure.figsize\"] = [21, 13]\n", 39 | "pd.set_option(\"display.precision\", 10)" 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": null, 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "config = get_template_live_config()\n", 49 | "config[\"market_type\"] = \"futures\"\n", 50 | "config[\"user\"] = \"binance_01\"\n", 51 | "config[\"exchange\"] = load_exchange_key_secret_passphrase(config[\"user\"])[0]\n", 52 | "config[\"symbol\"] = \"BTCUSDT\" # dummy symbol\n", 53 | "\n", 54 | "spot_quote = \"BTC\"\n", 55 | "\n", 56 | "n_days = 30\n", 57 | "start_time = (time() - 60 * 60 * 24 * n_days) * 1000\n", 58 | "end_time = time() * 1000\n", 59 | "\n", 60 | "if config[\"exchange\"] == \"binance\":\n", 61 | " if config[\"market_type\"] == \"spot\":\n", 62 | " raise Exception(\"implementation work in progress\")\n", 63 | " bot = await create_binance_bot_spot(config)\n", 64 | " balances = await bot.private_get(bot.endpoints[\"balance\"])\n", 65 | " ball = []\n", 66 | " for x in balances[\"balances\"]:\n", 67 | " onhand = float(x[\"free\"]) + float(x[\"locked\"])\n", 68 | " if onhand != 0.0:\n", 69 | " ball.append({\"coin\": x[\"asset\"], \"onhand\": onhand})\n", 70 | " symbols = [x[\"coin\"] + spot_quote for x in ball]\n", 71 | " print(symbols)\n", 72 | "\n", 73 | " else:\n", 74 | " bot = await create_binance_bot(config)\n", 75 | " poss = await bot.private_get(bot.endpoints[\"position\"])\n", 76 | " tickers = await bot.public_get(bot.endpoints[\"ticker\"])\n", 77 | " tickersd = {x[\"symbol\"]: float(x[\"bidPrice\"]) for x in tickers}\n", 78 | " balance = bot.position[\"wallet_balance\"]\n", 79 | " possl = []\n", 80 | " for x in poss:\n", 81 | " if float(x[\"positionAmt\"]) != 0.0:\n", 82 | " possl.append(\n", 83 | " {\n", 84 | " \"symbol\": x[\"symbol\"],\n", 85 | " \"psize\": float(x[\"positionAmt\"]),\n", 86 | " \"pprice\": float(x[\"entryPrice\"]),\n", 87 | " \"side\": x[\"positionSide\"].lower(),\n", 88 | " }\n", 89 | " )\n", 90 | " possl[-1][\"last_price\"] = tickersd[x[\"symbol\"]]\n", 91 | " if possl[-1][\"psize\"] > 0:\n", 92 | " possl[-1][\"PA_dist\"] = (possl[-1][\"pprice\"] - tickersd[x[\"symbol\"]]) / tickersd[\n", 93 | " x[\"symbol\"]\n", 94 | " ]\n", 95 | " else:\n", 96 | " possl[-1][\"PA_dist\"] = (tickersd[x[\"symbol\"]] - possl[-1][\"pprice\"]) / possl[-1][\n", 97 | " \"pprice\"\n", 98 | " ]\n", 99 | "\n", 100 | "elif config[\"exchange\"] == \"bybit\":\n", 101 | " bot = await create_bybit_bot(config)\n", 102 | " poss = await bot.private_get(bot.endpoints[\"position\"])\n", 103 | " tickers = await bot.public_get(bot.endpoints[\"ticker\"])\n", 104 | " tickersd = {x[\"symbol\"]: float(x[\"bid_price\"]) for x in tickers[\"result\"]}\n", 105 | " balance = bot.position[\"wallet_balance\"]\n", 106 | " possl = []\n", 107 | " for x in poss[\"result\"]:\n", 108 | " x = x[\"data\"]\n", 109 | " if float(x[\"size\"]) != 0.0:\n", 110 | " possl.append(\n", 111 | " {\n", 112 | " \"symbol\": x[\"symbol\"],\n", 113 | " \"psize\": float(x[\"size\"]),\n", 114 | " \"pprice\": float(x[\"entry_price\"]),\n", 115 | " \"side\": \"long\" if x[\"side\"] == \"Buy\" else \"short\",\n", 116 | " }\n", 117 | " )\n", 118 | " possl[-1][\"last_price\"] = tickersd[x[\"symbol\"]]\n", 119 | " if possl[-1][\"psize\"] > 0:\n", 120 | " possl[-1][\"PA_dist\"] = (possl[-1][\"pprice\"] - tickersd[x[\"symbol\"]]) / tickersd[\n", 121 | " x[\"symbol\"]\n", 122 | " ]\n", 123 | " else:\n", 124 | " possl[-1][\"PA_dist\"] = (tickersd[x[\"symbol\"]] - possl[-1][\"pprice\"]) / possl[-1][\n", 125 | " \"pprice\"\n", 126 | " ]\n", 127 | "\n", 128 | "\n", 129 | "pdf = pd.DataFrame(possl)\n", 130 | "pdf.loc[:, \"pcost\"] = abs(pdf.psize * pdf.pprice)\n", 131 | "pdf.loc[:, \"wallet_exposure\"] = pdf.pcost / balance\n", 132 | "symbols = sorted(pdf.symbol.unique())\n", 133 | "\n", 134 | "incomes = []\n", 135 | "if config[\"exchange\"] == \"bybit\":\n", 136 | " for symbol in symbols:\n", 137 | " print(f\"fetching income for {symbol}\")\n", 138 | " income = await bot.get_all_income(start_time=start_time, symbol=symbol)\n", 139 | " incomes += income\n", 140 | "elif config[\"exchange\"] == \"binance\":\n", 141 | " incomes = await bot.get_all_income(start_time=start_time)\n", 142 | "idf, bdf = get_daily_from_income(\n", 143 | " sorted(incomes, key=lambda x: x[\"timestamp\"]),\n", 144 | " bot.position[\"wallet_balance\"],\n", 145 | " start_time=start_time,\n", 146 | " end_time=end_time,\n", 147 | ")" 148 | ] 149 | }, 150 | { 151 | "cell_type": "code", 152 | "execution_count": null, 153 | "metadata": {}, 154 | "outputs": [], 155 | "source": [ 156 | "print(f\"current balance {round_dynamic(balance, 5)}\")\n", 157 | "n_days = (time() * 1000 - idf.timestamp.iloc[0]) / 1000 / 60 / 60 / 24\n", 158 | "print(\"n days\", n_days)\n", 159 | "print(f'abs sum {round_dynamic(idf.income.sum(), 4)} {idf.iloc[-1][\"token\"]}')\n", 160 | "print(f'abs adg {round_dynamic(idf.income.sum() / n_days, 4)} {idf.iloc[-1][\"token\"]}')\n", 161 | "print(f\"pct sum {((balance + idf.income.sum()) / balance) - 1:.5f}\")\n", 162 | "print(f\"pct adg {((balance + idf.income.sum()) / balance) ** (1 / n_days) - 1:.5f}\")\n", 163 | "\n", 164 | "idf.income.cumsum().plot()" 165 | ] 166 | }, 167 | { 168 | "cell_type": "code", 169 | "execution_count": null, 170 | "metadata": {}, 171 | "outputs": [], 172 | "source": [ 173 | "idf.tail(20)" 174 | ] 175 | }, 176 | { 177 | "cell_type": "code", 178 | "execution_count": null, 179 | "metadata": {}, 180 | "outputs": [], 181 | "source": [ 182 | "bdf" 183 | ] 184 | }, 185 | { 186 | "cell_type": "code", 187 | "execution_count": null, 188 | "metadata": {}, 189 | "outputs": [], 190 | "source": [ 191 | "bdf.gain.plot()" 192 | ] 193 | }, 194 | { 195 | "cell_type": "code", 196 | "execution_count": null, 197 | "metadata": {}, 198 | "outputs": [], 199 | "source": [ 200 | "# check income last x days\n", 201 | "x = 3\n", 202 | "since = (time() - 60 * 60 * 24 * x) * 1000\n", 203 | "idf[idf.timestamp > since].groupby(\"symbol\").income.sum().sort_values()" 204 | ] 205 | }, 206 | { 207 | "cell_type": "code", 208 | "execution_count": null, 209 | "metadata": {}, 210 | "outputs": [], 211 | "source": [ 212 | "pdf.sort_values(\"PA_dist\")" 213 | ] 214 | }, 215 | { 216 | "cell_type": "code", 217 | "execution_count": null, 218 | "metadata": {}, 219 | "outputs": [], 220 | "source": [] 221 | } 222 | ], 223 | "metadata": { 224 | "kernelspec": { 225 | "display_name": "Python 3 (ipykernel)", 226 | "language": "python", 227 | "name": "python3" 228 | }, 229 | "language_info": { 230 | "codemirror_mode": { 231 | "name": "ipython", 232 | "version": 3 233 | }, 234 | "file_extension": ".py", 235 | "mimetype": "text/x-python", 236 | "name": "python", 237 | "nbconvert_exporter": "python", 238 | "pygments_lexer": "ipython3", 239 | "version": "3.9.8" 240 | } 241 | }, 242 | "nbformat": 4, 243 | "nbformat_minor": 4 244 | } 245 | -------------------------------------------------------------------------------- /src/correlation.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "%load_ext autoreload\n", 10 | "%autoreload 2" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "from procedures import create_binance_bot, create_binance_bot_spot, make_get_filepath\n", 20 | "from pure_funcs import get_template_live_config, ts_to_date, get_daily_from_income\n", 21 | "from njit_funcs import round_dynamic\n", 22 | "from time import time, sleep\n", 23 | "import os\n", 24 | "import pandas as pd\n", 25 | "import asyncio\n", 26 | "import numpy as np\n", 27 | "import matplotlib.pyplot as plt" 28 | ] 29 | }, 30 | { 31 | "cell_type": "code", 32 | "execution_count": null, 33 | "metadata": {}, 34 | "outputs": [], 35 | "source": [ 36 | "plt.rcParams[\"figure.figsize\"] = [21, 13]\n", 37 | "pd.set_option(\"display.precision\", 10)" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": null, 43 | "metadata": {}, 44 | "outputs": [], 45 | "source": [ 46 | "config = get_template_live_config()\n", 47 | "config[\"symbol\"] = \"XMRUSDT\" # dummy symbol\n", 48 | "config[\"exchange\"] = \"binance\"\n", 49 | "config[\"market_type\"] = \"futures\"\n", 50 | "config[\"user\"] = \"binance_01\"\n", 51 | "try:\n", 52 | " await bot.session.close()\n", 53 | "except:\n", 54 | " pass\n", 55 | "if config[\"market_type\"] == \"spot\":\n", 56 | " bot = await create_binance_bot_spot(config)\n", 57 | "else:\n", 58 | " bot = await create_binance_bot(config)" 59 | ] 60 | }, 61 | { 62 | "cell_type": "code", 63 | "execution_count": null, 64 | "metadata": {}, 65 | "outputs": [], 66 | "source": [ 67 | "info = await bot.public_get(bot.endpoints[\"exchange_info\"])\n", 68 | "csvs = {}\n", 69 | "syms = [x[\"symbol\"] for x in info[\"symbols\"] if x[\"symbol\"].endswith(\"USDT\")]\n", 70 | "n = 7\n", 71 | "interval = \"5m\"\n", 72 | "while True:\n", 73 | " if not syms:\n", 74 | " break\n", 75 | " subset = syms[:n]\n", 76 | " syms = syms[n:]\n", 77 | " res = await asyncio.gather(*[bot.fetch_ohlcvs(symbol=s, interval=interval) for s in subset])\n", 78 | " for s, r in zip(subset, res):\n", 79 | " csvs[s] = pd.DataFrame(r)\n", 80 | " print(subset, len(syms))" 81 | ] 82 | }, 83 | { 84 | "cell_type": "code", 85 | "execution_count": null, 86 | "metadata": {}, 87 | "outputs": [], 88 | "source": [ 89 | "def correlation(x, y):\n", 90 | " x_mean = x.mean()\n", 91 | " y_mean = y.mean()\n", 92 | " a = x - x_mean\n", 93 | " b = y - y_mean\n", 94 | " return ((a * b).sum()) / (((a**2).sum() * (b**2).sum()) ** 0.5)" 95 | ] 96 | }, 97 | { 98 | "cell_type": "code", 99 | "execution_count": null, 100 | "metadata": {}, 101 | "outputs": [], 102 | "source": [ 103 | "n_symbols = 30\n", 104 | "vols = [(csvs[s].volume * csvs[s].close).sum() for s in csvs]\n", 105 | "minvol = sorted(vols)[-n_symbols]\n", 106 | "ss = [s for s in csvs if (csvs[s].volume * csvs[s].close).sum() >= minvol]\n", 107 | "corrs = []\n", 108 | "corrs_dict = {}\n", 109 | "done = set()\n", 110 | "for s0 in sorted(ss):\n", 111 | " for s1 in sorted(ss):\n", 112 | " if s0 == s1:\n", 113 | " continue\n", 114 | " key = tuple(sorted([s0, s1]))\n", 115 | " if key in done:\n", 116 | " continue\n", 117 | " done.add(key)\n", 118 | " x = csvs[s0].close\n", 119 | " y = csvs[s1].close\n", 120 | " corr = correlation(x, y)\n", 121 | " print(f\"\\r{s0} {s1} {corr:.4f} \", end=\" \")\n", 122 | " corrs.append((corr,) + key)\n", 123 | "\n", 124 | "corrs_dict = {}\n", 125 | "corrs_dict_abs = {}\n", 126 | "for e in corrs:\n", 127 | " if e[1] not in corrs_dict:\n", 128 | " corrs_dict[e[1]] = {e[2]: e[0]}\n", 129 | " corrs_dict_abs[e[1]] = {e[2]: abs(e[0])}\n", 130 | "\n", 131 | " else:\n", 132 | " corrs_dict[e[1]][e[2]] = e[0]\n", 133 | " corrs_dict_abs[e[1]][e[2]] = abs(e[0])\n", 134 | "\n", 135 | " if e[2] not in corrs_dict:\n", 136 | " corrs_dict[e[2]] = {e[1]: e[0]}\n", 137 | " corrs_dict_abs[e[2]] = {e[1]: abs(e[0])}\n", 138 | "\n", 139 | " else:\n", 140 | " corrs_dict[e[2]][e[1]] = e[0]\n", 141 | " corrs_dict_abs[e[2]][e[1]] = abs(e[0])" 142 | ] 143 | }, 144 | { 145 | "cell_type": "code", 146 | "execution_count": null, 147 | "metadata": {}, 148 | "outputs": [], 149 | "source": [ 150 | "# sum correlation of each symbol to every other symbol\n", 151 | "corr_sorted = sorted([(sum(corrs_dict[s].values()), s) for s in corrs_dict])\n", 152 | "corr_sorted" 153 | ] 154 | }, 155 | { 156 | "cell_type": "code", 157 | "execution_count": null, 158 | "metadata": {}, 159 | "outputs": [], 160 | "source": [ 161 | "# mean of all symbol\n", 162 | "mean_prices = pd.concat([e.close / e.close.iloc[0] for e in csvs.values()], axis=1).mean(axis=1)\n", 163 | "\n", 164 | "# compare individual symbol to mean\n", 165 | "symbol = \"SXPUSDT\"\n", 166 | "# symbol = corr_sorted[0][1] # least correlated symbol\n", 167 | "mean_prices.rename(\"mean_prices\").plot()\n", 168 | "(csvs[symbol].close / csvs[symbol].close.iloc[0]).rename(symbol).plot()\n", 169 | "plt.legend()" 170 | ] 171 | }, 172 | { 173 | "cell_type": "code", 174 | "execution_count": null, 175 | "metadata": {}, 176 | "outputs": [], 177 | "source": [ 178 | "# compare two symbols\n", 179 | "symbol0 = \"BNBUSDT\"\n", 180 | "symbol1 = \"LINKUSDT\"\n", 181 | "(csvs[symbol0].close / csvs[symbol0].close.iloc[0]).rename(symbol0).plot()\n", 182 | "(csvs[symbol1].close / csvs[symbol1].close.iloc[0]).rename(symbol1).plot()\n", 183 | "plt.legend()" 184 | ] 185 | }, 186 | { 187 | "cell_type": "code", 188 | "execution_count": null, 189 | "metadata": {}, 190 | "outputs": [], 191 | "source": [] 192 | } 193 | ], 194 | "metadata": { 195 | "kernelspec": { 196 | "display_name": "Python 3 (ipykernel)", 197 | "language": "python", 198 | "name": "python3" 199 | }, 200 | "language_info": { 201 | "codemirror_mode": { 202 | "name": "ipython", 203 | "version": 3 204 | }, 205 | "file_extension": ".py", 206 | "mimetype": "text/x-python", 207 | "name": "python", 208 | "nbconvert_exporter": "python", 209 | "pygments_lexer": "ipython3", 210 | "version": "3.10.4" 211 | } 212 | }, 213 | "nbformat": 4, 214 | "nbformat_minor": 4 215 | } 216 | -------------------------------------------------------------------------------- /src/exchanges/gateio.py: -------------------------------------------------------------------------------- 1 | from passivbot import Passivbot, logging 2 | from uuid import uuid4 3 | import ccxt.pro as ccxt_pro 4 | import ccxt.async_support as ccxt_async 5 | import pprint 6 | import asyncio 7 | import traceback 8 | import json 9 | import numpy as np 10 | from pure_funcs import ( 11 | multi_replace, 12 | floatify, 13 | ts_to_date_utc, 14 | calc_hash, 15 | shorten_custom_id, 16 | coin2symbol, 17 | symbol_to_coin, 18 | ) 19 | from njit_funcs import ( 20 | calc_diff, 21 | round_, 22 | round_up, 23 | round_dn, 24 | round_dynamic, 25 | round_dynamic_up, 26 | round_dynamic_dn, 27 | ) 28 | from procedures import print_async_exception, utc_ms, assert_correct_ccxt_version 29 | from sortedcontainers import SortedDict 30 | 31 | assert_correct_ccxt_version(ccxt=ccxt_async) 32 | 33 | 34 | class GateIOBot(Passivbot): 35 | def __init__(self, config: dict): 36 | super().__init__(config) 37 | self.ohlcvs_1m_init_duration_seconds = ( 38 | 120 # gateio has stricter rate limiting on fetching ohlcvs 39 | ) 40 | self.hedge_mode = False 41 | self.max_n_creations_per_batch = 10 42 | self.max_n_cancellations_per_batch = 20 43 | 44 | def create_ccxt_sessions(self): 45 | self.ccp = getattr(ccxt_pro, self.exchange)( 46 | { 47 | "apiKey": self.user_info["key"], 48 | "secret": self.user_info["secret"], 49 | "headers": {"X-Gate-Channel-Id": self.broker_code} if self.broker_code else {}, 50 | } 51 | ) 52 | self.ccp.options["defaultType"] = "swap" 53 | self.cca = getattr(ccxt_async, self.exchange)( 54 | { 55 | "apiKey": self.user_info["key"], 56 | "secret": self.user_info["secret"], 57 | "headers": {"X-Gate-Channel-Id": self.broker_code} if self.broker_code else {}, 58 | } 59 | ) 60 | self.cca.options["defaultType"] = "swap" 61 | 62 | def set_market_specific_settings(self): 63 | super().set_market_specific_settings() 64 | for symbol in self.markets_dict: 65 | elm = self.markets_dict[symbol] 66 | self.symbol_ids[symbol] = elm["id"] 67 | self.min_costs[symbol] = ( 68 | 0.1 if elm["limits"]["cost"]["min"] is None else elm["limits"]["cost"]["min"] 69 | ) 70 | self.min_qtys[symbol] = ( 71 | elm["precision"]["amount"] 72 | if elm["limits"]["amount"]["min"] is None 73 | else elm["limits"]["amount"]["min"] 74 | ) 75 | self.qty_steps[symbol] = elm["precision"]["amount"] 76 | self.price_steps[symbol] = elm["precision"]["price"] 77 | self.c_mults[symbol] = elm["contractSize"] 78 | self.max_leverage[symbol] = elm["limits"]["leverage"]["max"] 79 | 80 | async def determine_utc_offset(self, verbose=True): 81 | # returns millis to add to utc to get exchange timestamp 82 | # call some endpoint which includes timestamp for exchange's server 83 | # if timestamp is not included in self.cca.fetch_balance(), 84 | # implement method in exchange child class 85 | result = await self.cca.fetch_ohlcv("BTC/USDT:USDT", timeframe="1m") 86 | self.utc_offset = round((result[-1][0] - utc_ms()) / (1000 * 60 * 60)) * (1000 * 60 * 60) 87 | if verbose: 88 | logging.info(f"Exchange time offset is {self.utc_offset}ms compared to UTC") 89 | 90 | async def watch_balance(self): 91 | # hyperliquid ccxt watch balance not supported. 92 | # relying instead on periodic REST updates 93 | res = None 94 | while True: 95 | try: 96 | if self.stop_websocket: 97 | break 98 | res = await self.cca.fetch_balance() 99 | res[self.quote]["total"] = float(res["info"]["marginSummary"]["accountValue"]) - sum( 100 | [float(x["position"]["unrealizedPnl"]) for x in res["info"]["assetPositions"]] 101 | ) 102 | self.handle_balance_update(res) 103 | await asyncio.sleep(10) 104 | except Exception as e: 105 | logging.error(f"exception watch_balance {res} {e}") 106 | traceback.print_exc() 107 | await asyncio.sleep(1) 108 | 109 | async def watch_orders(self): 110 | res = None 111 | while not self.stop_signal_received: 112 | if not self.ccp.uid: 113 | await asyncio.sleep(1) 114 | continue 115 | try: 116 | if self.stop_websocket: 117 | break 118 | res = await self.ccp.watch_orders() 119 | for i in range(len(res)): 120 | res[i]["position_side"] = self.determine_pos_side(res[i]) 121 | res[i]["qty"] = res[i]["amount"] 122 | self.handle_order_update(res) 123 | except Exception as e: 124 | logging.error(f"exception watch_orders {res} {e}") 125 | traceback.print_exc() 126 | await asyncio.sleep(1) 127 | 128 | def determine_pos_side(self, order): 129 | if order["side"] == "buy": 130 | return "short" if order["reduceOnly"] else "long" 131 | if order["side"] == "sell": 132 | return "long" if order["reduceOnly"] else "short" 133 | raise Exception(f"unsupported order side {order['side']}") 134 | 135 | async def fetch_open_orders(self, symbol: str = None): 136 | fetched = None 137 | open_orders = [] 138 | try: 139 | fetched = await self.cca.fetch_open_orders() 140 | for i in range(len(fetched)): 141 | fetched[i]["position_side"] = self.determine_pos_side(fetched[i]) 142 | fetched[i]["qty"] = fetched[i]["amount"] 143 | return sorted(fetched, key=lambda x: x["timestamp"]) 144 | except Exception as e: 145 | logging.error(f"error fetching open orders {e}") 146 | print_async_exception(fetched) 147 | traceback.print_exc() 148 | return False 149 | 150 | async def fetch_positions(self) -> ([dict], float): 151 | positions, balance = None, None 152 | try: 153 | positions_fetched, balance = await asyncio.gather( 154 | self.cca.fetch_positions(), self.cca.fetch_balance() 155 | ) 156 | if not hasattr(self, "uid") or not self.uid: 157 | self.uid = balance["info"][0]["user"] 158 | self.cca.uid = self.uid 159 | self.ccp.uid = self.uid 160 | balance = balance[self.quote]["total"] 161 | positions = [] 162 | for x in positions_fetched: 163 | if x["contracts"] != 0.0: 164 | x["size"] = x["contracts"] 165 | x["price"] = x["entryPrice"] 166 | x["position_side"] = x["side"] 167 | positions.append(x) 168 | return positions, balance 169 | except Exception as e: 170 | logging.error(f"error fetching positions and balance {e}") 171 | print_async_exception(positions) 172 | print_async_exception(balance) 173 | traceback.print_exc() 174 | return False 175 | 176 | async def fetch_tickers(self): 177 | fetched = None 178 | try: 179 | fetched = await self.cca.fetch( 180 | "https://api.hyperliquid.xyz/info", 181 | method="POST", 182 | headers={"Content-Type": "application/json"}, 183 | body=json.dumps({"type": "allMids"}), 184 | ) 185 | return { 186 | coin2symbol(coin, self.quote): { 187 | "bid": float(fetched[coin]), 188 | "ask": float(fetched[coin]), 189 | "last": float(fetched[coin]), 190 | } 191 | for coin in fetched 192 | } 193 | except Exception as e: 194 | logging.error(f"error fetching tickers {e}") 195 | print_async_exception(fetched) 196 | traceback.print_exc() 197 | return False 198 | 199 | async def fetch_ohlcv(self, symbol: str, timeframe="1m"): 200 | # intervals: 1,3,5,15,30,60,120,240,360,720,D,M,W 201 | # fetches latest ohlcvs 202 | fetched = None 203 | str2int = {"1m": 1, "5m": 5, "15m": 15, "1h": 60, "4h": 60 * 4} 204 | n_candles = 480 205 | try: 206 | since = int(utc_ms() - 1000 * 60 * str2int[timeframe] * n_candles) 207 | fetched = await self.cca.fetch_ohlcv(symbol, timeframe=timeframe, since=since) 208 | return fetched 209 | except Exception as e: 210 | logging.error(f"error fetching ohlcv for {symbol} {e}") 211 | print_async_exception(fetched) 212 | traceback.print_exc() 213 | return False 214 | 215 | async def fetch_ohlcvs_1m(self, symbol: str, limit=None): 216 | n_candles_limit = 1440 if limit is None else limit 217 | result = await self.cca.fetch_ohlcv( 218 | symbol, 219 | timeframe="1m", 220 | limit=n_candles_limit, 221 | ) 222 | return result 223 | 224 | async def fetch_pnls( 225 | self, 226 | start_time: int = None, 227 | end_time: int = None, 228 | limit=None, 229 | ): 230 | if start_time is None: 231 | return await self.fetch_pnl(limit=limit) 232 | all_fetched = {} 233 | if limit is None: 234 | limit = 1000 235 | offset = 0 236 | while True: 237 | fetched = await self.fetch_pnl(offset=offset, limit=limit) 238 | if not fetched: 239 | break 240 | for elm in fetched: 241 | all_fetched[elm["id"]] = elm 242 | if len(fetched) < limit: 243 | break 244 | if fetched[0]["timestamp"] <= start_time: 245 | break 246 | logging.info(f"debug fetching pnls {ts_to_date_utc(fetched[-1]['timestamp'])}") 247 | offset += limit 248 | return sorted(all_fetched.values(), key=lambda x: x["timestamp"]) 249 | 250 | async def fetch_pnl( 251 | self, 252 | offset=0, 253 | limit=None, 254 | ): 255 | fetched = None 256 | n_pnls_limit = 1000 if limit is None else limit 257 | try: 258 | fetched = await self.cca.fetch_closed_orders( 259 | limit=n_pnls_limit, params={"offset": offset} 260 | ) 261 | for i in range(len(fetched)): 262 | fetched[i]["pnl"] = float(fetched[i]["info"]["pnl"]) 263 | fetched[i]["position_side"] = self.determine_pos_side(fetched[i]) 264 | return sorted(fetched, key=lambda x: x["timestamp"]) 265 | except Exception as e: 266 | logging.error(f"error fetching pnl {e}") 267 | print_async_exception(fetched) 268 | traceback.print_exc() 269 | return False 270 | 271 | async def execute_cancellation(self, order: dict) -> dict: 272 | return await self.execute_cancellations([order]) 273 | 274 | async def execute_cancellations(self, orders: [dict]) -> [dict]: 275 | if not orders: 276 | return [] 277 | res = None 278 | max_n_cancellations_per_batch = min( 279 | self.max_n_cancellations_per_batch, self.config["live"]["max_n_cancellations_per_batch"] 280 | ) 281 | try: 282 | if len(orders) > max_n_cancellations_per_batch: 283 | # prioritize cancelling reduce-only orders 284 | try: 285 | reduce_only_orders = [x for x in orders if x["reduce_only"]] 286 | rest = [x for x in orders if not x["reduce_only"]] 287 | orders = (reduce_only_orders + rest)[:max_n_cancellations_per_batch] 288 | except Exception as e: 289 | logging.error(f"debug filter cancellations {e}") 290 | res = await self.cca.cancel_orders([x["id"] for x in orders]) 291 | cancellations = [] 292 | for order, elm in zip(orders, res): 293 | if elm["status"] != "rejected": 294 | joined = order.copy() 295 | for k, v in elm.items(): 296 | if k not in joined or not joined[k]: 297 | joined[k] = v 298 | cancellations.append(joined) 299 | return cancellations 300 | except Exception as e: 301 | logging.error(f"error executing cancellations {e} {orders}") 302 | print_async_exception(res) 303 | traceback.print_exc() 304 | 305 | async def execute_order(self, order: dict) -> dict: 306 | return await self.execute_orders([order]) 307 | 308 | async def execute_orders(self, orders: [dict]) -> [dict]: 309 | if len(orders) == 0: 310 | return [] 311 | to_execute = [] 312 | for order in orders[: self.max_n_creations_per_batch]: 313 | order_type = order["type"] if "type" in order else "limit" 314 | params = { 315 | "reduce_only": order["reduce_only"], 316 | } 317 | if order_type == "limit": 318 | params["timeInForce"] = ( 319 | "poc" if self.config["live"]["time_in_force"] == "post_only" else "gtc" 320 | ) 321 | to_execute.append( 322 | { 323 | "symbol": order["symbol"], 324 | "type": order_type, 325 | "side": order["side"], 326 | "amount": order["qty"], 327 | "price": order["price"], 328 | "params": params, 329 | } 330 | ) 331 | res = await self.cca.create_orders(to_execute) 332 | executed = [] 333 | for ex, order in zip(res, orders): 334 | if "info" in ex and ex["status"] in ["closed", "open"]: 335 | executed.append({**ex, **order}) 336 | return executed 337 | 338 | async def update_exchange_config_by_symbols(self, symbols): 339 | return 340 | coros_to_call_margin_mode = {} 341 | for symbol in symbols: 342 | try: 343 | params = { 344 | "leverage": int( 345 | min( 346 | self.max_leverage[symbol], 347 | self.live_configs[symbol]["leverage"], 348 | ) 349 | ) 350 | } 351 | if self.user_info["is_vault"]: 352 | params["vaultAddress"] = self.user_info["wallet_address"] 353 | coros_to_call_margin_mode[symbol] = asyncio.create_task( 354 | self.cca.set_margin_mode("cross", symbol=symbol, params=params) 355 | ) 356 | except Exception as e: 357 | logging.error(f"{symbol}: error setting cross mode and leverage {e}") 358 | for symbol in symbols: 359 | res = None 360 | to_print = "" 361 | try: 362 | res = await coros_to_call_margin_mode[symbol] 363 | to_print += f"set cross mode {res}" 364 | except Exception as e: 365 | if '"code":"59107"' in e.args[0]: 366 | to_print += f" cross mode and leverage: {res} {e}" 367 | else: 368 | logging.error(f"{symbol} error setting cross mode {res} {e}") 369 | if to_print: 370 | logging.info(f"{symbol}: {to_print}") 371 | 372 | async def update_exchange_config(self): 373 | pass 374 | 375 | def calc_ideal_orders(self): 376 | # hyperliquid needs custom price rounding 377 | ideal_orders = super().calc_ideal_orders() 378 | return ideal_orders 379 | -------------------------------------------------------------------------------- /src/inspect_opt_results.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | if "NOJIT" not in os.environ: 4 | os.environ["NOJIT"] = "true" 5 | 6 | import json 7 | import re 8 | import pprint 9 | import numpy as np 10 | from prettytable import PrettyTable 11 | import argparse 12 | import hjson 13 | from procedures import load_live_config, dump_live_config, make_get_filepath 14 | from pure_funcs import ( 15 | config_pretty_str, 16 | candidate_to_live_config, 17 | calc_scores, 18 | determine_passivbot_mode, 19 | make_compatible, 20 | ) 21 | from njit_funcs import round_dynamic 22 | 23 | 24 | def shorten(key): 25 | key_ = key 26 | for src, dst in [ 27 | ("weighted", "w"), 28 | ("exposure", "exp"), 29 | ("distance", "dist"), 30 | ("ratio", "rt"), 31 | ("mean_of_10_worst", "10_worst_mean"), 32 | ]: 33 | key_ = key_.replace(src, dst) 34 | return key_ 35 | 36 | 37 | def main(): 38 | parser = argparse.ArgumentParser(prog="view conf", description="inspect conf") 39 | parser.add_argument("results_fpath", type=str, help="path to results file") 40 | parser.add_argument( 41 | "-i", 42 | "--index", 43 | dest="index", 44 | type=int, 45 | required=False, 46 | default=None, 47 | help="inspect particular config of given index", 48 | ) 49 | parser.add_argument( 50 | "-oc", 51 | "--optimize_config", 52 | type=str, 53 | required=False, 54 | dest="optimize_config_path", 55 | default="configs/optimize/default.hjson", 56 | help="optimize config hjson file", 57 | ) 58 | parser.add_argument( 59 | "-d", 60 | "--dump_live_config", 61 | action="store_true", 62 | help="dump config", 63 | ) 64 | 65 | args = parser.parse_args() 66 | 67 | opt_config = hjson.load(open(args.optimize_config_path)) 68 | minsmaxs = {} 69 | for k in opt_config: 70 | if "maximum_" in k or "minimum_" in k: 71 | minsmaxs[k] = opt_config[k] 72 | klen = max([len(k) for k in minsmaxs]) 73 | for k, v in minsmaxs.items(): 74 | print(f"{k: <{klen}} {v}") 75 | 76 | if os.path.isdir(args.results_fpath): 77 | args.results_fpath = os.path.join(args.results_fpath, "all_results.txt") 78 | with open(args.results_fpath) as f: 79 | results = [json.loads(x) for x in f.readlines()] 80 | print(f"{'n results': <{klen}} {len(results)}") 81 | passivbot_mode = determine_passivbot_mode(make_compatible(results[-1]["config"])) 82 | all_scores = [] 83 | symbols = [s for s in results[0]["results"] if s != "config_no"] 84 | starting_balance = results[-1]["results"][symbols[0]]["starting_balance"] 85 | print(f"{'starting_balance': <{klen}} {starting_balance}") 86 | sides = ["long", "short"] 87 | for r in results: 88 | cfg = r["config"].copy() 89 | cfg.update(opt_config) 90 | ress = r["results"] 91 | all_scores.append({}) 92 | scores_res = calc_scores(cfg, {s: r["results"][s] for s in symbols}) 93 | scores, individual_scores, keys = ( 94 | scores_res["scores"], 95 | scores_res["individual_scores"], 96 | scores_res["keys"], 97 | ) 98 | keys = keys[:1] + [("adg_per_exposure", True)] + keys[1:] 99 | for side in sides: 100 | all_scores[-1][side] = { 101 | "config": cfg[side], 102 | "score": scores[side], 103 | "individual_scores": individual_scores[side], 104 | "symbols_to_include": scores_res["symbols_to_include"][side], 105 | "stats": {sym: {k: v for k, v in ress[sym].items() if side in k} for sym in symbols}, 106 | "config_no": ress["config_no"], 107 | "n_days": {sym: ress[sym]["n_days"] for sym in symbols}, 108 | } 109 | best_candidate = {} 110 | for side in sides: 111 | scoress = sorted([sc[side] for sc in all_scores], key=lambda x: x["score"]) 112 | best_candidate[side] = scoress[0] 113 | if args.index is not None: 114 | best_candidate[side] = [elm for elm in scoress if elm["config_no"] == args.index][0] 115 | best_config = {side: best_candidate[side]["config"] for side in sides} 116 | best_config = { 117 | "long": best_candidate["long"]["config"], 118 | "short": best_candidate["short"]["config"], 119 | } 120 | table_filepath = f"{args.results_fpath.replace('all_results.txt', '')}table_best_config.txt" 121 | if os.path.exists(table_filepath): 122 | os.remove(table_filepath) 123 | for side in sides: 124 | row_headers = ["symbol"] + [shorten(k[0]) for k in keys] + ["n_days", "score"] 125 | table = PrettyTable(row_headers) 126 | for rh in row_headers: 127 | table.align[rh] = "l" 128 | table.title = ( 129 | f"{side} (config no. {best_candidate[side]['config_no']}," 130 | + f" score {round_dynamic(best_candidate[side]['score'], 15)})" 131 | ) 132 | for sym in sorted( 133 | symbols, 134 | key=lambda x: best_candidate[side]["individual_scores"][x], 135 | reverse=True, 136 | ): 137 | xs = [best_candidate[side]["stats"][sym][f"{k[0]}_{side}"] for k in keys] 138 | table.add_row( 139 | [("-> " if sym in best_candidate[side]["symbols_to_include"] else "") + sym] 140 | + [round_dynamic(x, 4) if np.isfinite(x) else x for x in xs] 141 | + [round(best_candidate[side]["n_days"][sym], 2)] 142 | + [round_dynamic(best_candidate[side]["individual_scores"][sym], 12)] 143 | ) 144 | means = [ 145 | np.mean( 146 | [ 147 | best_candidate[side]["stats"][s_][f"{k[0]}_{side}"] 148 | for s_ in best_candidate[side]["symbols_to_include"] 149 | ] 150 | ) 151 | for k in keys 152 | ] 153 | ind_scores_mean = np.mean( 154 | [ 155 | best_candidate[side]["individual_scores"][sym] 156 | for sym in best_candidate[side]["symbols_to_include"] 157 | ] 158 | ) 159 | table.add_row( 160 | ["mean"] 161 | + [round_dynamic(m, 4) if np.isfinite(m) else m for m in means] 162 | + [round(np.mean(list(best_candidate[side]["n_days"].values())), 2)] 163 | + [round_dynamic(ind_scores_mean, 12)] 164 | ) 165 | with open(make_get_filepath(table_filepath), "a") as f: 166 | output = table.get_string(border=True, padding_width=1) 167 | print(output) 168 | f.write(re.sub("\033\\[([0-9]+)(;[0-9]+)*m", "", output) + "\n\n") 169 | live_config = candidate_to_live_config(best_config) 170 | if args.dump_live_config: 171 | lc_fpath = make_get_filepath(f"{args.results_fpath.replace('.txt', '_best_config.json')}") 172 | print(f"dump_live_config {lc_fpath}") 173 | dump_live_config(live_config, lc_fpath) 174 | print(config_pretty_str(live_config)) 175 | 176 | 177 | if __name__ == "__main__": 178 | main() 179 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import time 4 | import subprocess 5 | import sys 6 | import select 7 | import platform 8 | 9 | RUST_SOURCE_DIR = "passivbot-rust/" 10 | COMPILED_EXTENSION_NAME = "libpassivbot_rust" 11 | LOCK_FILE = os.path.join(RUST_SOURCE_DIR, ".compile.lock") 12 | LOCK_TIMEOUT = 300 # 5 minutes in seconds 13 | LOCK_CHECK_INTERVAL = 2 # Check every 2 seconds 14 | 15 | 16 | def get_compiled_extension_paths(): 17 | possible_extensions = ["so", "dylib", "dll", "pyd", "", "bundle", "sl"] 18 | return [ 19 | os.path.join(RUST_SOURCE_DIR, "target", "release", f"{COMPILED_EXTENSION_NAME}.{ext}") 20 | for ext in possible_extensions 21 | ] 22 | 23 | 24 | COMPILED_EXTENSION_PATHS = get_compiled_extension_paths() 25 | 26 | 27 | def acquire_lock(): 28 | start_time = time.time() 29 | while True: 30 | try: 31 | if os.path.exists(LOCK_FILE): 32 | # Check if lock is stale 33 | if time.time() - os.path.getmtime(LOCK_FILE) > LOCK_TIMEOUT: 34 | print("Found stale lock file. Removing and proceeding with compilation.") 35 | try: 36 | os.remove(LOCK_FILE) 37 | except OSError: 38 | pass 39 | else: 40 | if time.time() - start_time > LOCK_TIMEOUT: 41 | print("Lock timeout reached. Assuming crashed compilation and proceeding.") 42 | try: 43 | os.remove(LOCK_FILE) 44 | except OSError: 45 | pass 46 | return True 47 | print( 48 | f"Another compilation in progress. Waiting... ({int(time.time() - start_time)}s)" 49 | ) 50 | time.sleep(LOCK_CHECK_INTERVAL) 51 | continue 52 | 53 | # Create lock file 54 | with open(LOCK_FILE, "w") as f: 55 | f.write(str(os.getpid())) 56 | return True 57 | 58 | except OSError as e: 59 | print(f"Error managing lock file: {e}") 60 | return False 61 | 62 | 63 | def release_lock(): 64 | try: 65 | if os.path.exists(LOCK_FILE): 66 | os.remove(LOCK_FILE) 67 | except OSError as e: 68 | print(f"Error removing lock file: {e}") 69 | 70 | 71 | def check_compilation_needed(): 72 | try: 73 | # Find the most recently modified compiled extension 74 | compiled_files = [path for path in COMPILED_EXTENSION_PATHS if os.path.exists(path)] 75 | if not compiled_files: 76 | print(f"No Rust extension found. Compiling...") 77 | return True # No extension found, compilation needed 78 | 79 | compiled_time = max(os.path.getmtime(path) for path in compiled_files) 80 | 81 | # Check all .rs files in the Rust source directory 82 | for root, _, files in os.walk(RUST_SOURCE_DIR): 83 | for file in files: 84 | if file.endswith(".rs"): 85 | file_path = os.path.join(root, file) 86 | if os.path.getmtime(file_path) > compiled_time: 87 | print(f"Rust extension found, but out of date. Recompiling...") 88 | return True # A source file is newer, compilation needed 89 | return False # No compilation needed 90 | except Exception as e: 91 | print(f"Error checking compilation status: {e}") 92 | return True # If in doubt, suggest recompilation 93 | 94 | 95 | def prompt_user_for_recompilation(): 96 | print("Rust code needs recompilation. Recompile now? [Y/n]") 97 | start_time = time.time() 98 | while time.time() - start_time < 10: 99 | rlist, _, _ = select.select([sys.stdin], [], [], 0.1) 100 | if rlist: 101 | user_input = sys.stdin.readline().strip().lower() 102 | if user_input == "n": 103 | return False 104 | else: 105 | return True 106 | print("No input received within 10 seconds. Proceeding with recompilation.") 107 | return True 108 | 109 | 110 | def recompile_rust(): 111 | try: 112 | current_dir = os.getcwd() 113 | os.chdir(RUST_SOURCE_DIR) 114 | result = subprocess.run( 115 | ["maturin", "develop", "--release"], check=True, capture_output=True, text=True 116 | ) 117 | os.chdir(current_dir) 118 | print("Compilation successful.") 119 | print(result.stdout) 120 | return True 121 | except subprocess.CalledProcessError as e: 122 | print(f"Compilation failed with error:") 123 | print(e.stderr) 124 | return False 125 | except Exception as e: 126 | print(f"An error occurred during compilation: {e}") 127 | return False 128 | 129 | 130 | def manage_rust_compilation(): 131 | if check_compilation_needed(): 132 | if acquire_lock(): 133 | try: 134 | if recompile_rust(): 135 | print("Rust extension successfully recompiled.") 136 | else: 137 | print("Failed to recompile Rust extension. Please compile manually.") 138 | sys.exit(1) 139 | finally: 140 | release_lock() 141 | else: 142 | print("Failed to acquire lock for compilation. Please try again later.") 143 | sys.exit(1) 144 | else: 145 | print("Rust extension is up to date.") 146 | 147 | 148 | if __name__ == "__main__": 149 | manage_rust_compilation() 150 | from passivbot import main 151 | 152 | asyncio.run(main()) 153 | -------------------------------------------------------------------------------- /src/min_costs.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "2c755c1d-2774-4ecd-80fd-b9200b147bf1", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import ccxt.async_support as ccxt\n", 11 | "import json\n", 12 | "import pprint\n", 13 | "import numpy as np\n", 14 | "import pandas as pd\n", 15 | "from forager import get_min_costs_and_contract_multipliers" 16 | ] 17 | }, 18 | { 19 | "cell_type": "code", 20 | "execution_count": null, 21 | "id": "92399db7-bc05-4a75-ac16-0d659632fcef", 22 | "metadata": {}, 23 | "outputs": [], 24 | "source": [ 25 | "# choices: okx, binanceusdm, bitget, bybit, kucoinfutures, bingx\n", 26 | "exchange = \"bingx\"\n", 27 | "try:\n", 28 | " await cc.close()\n", 29 | "except:\n", 30 | " pass\n", 31 | "cc = getattr(ccxt, exchange)()" 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": null, 37 | "id": "b03e4b7e-128b-4594-a3d9-36081d023f71", 38 | "metadata": {}, 39 | "outputs": [], 40 | "source": [ 41 | "res = await get_min_costs_and_contract_multipliers(cc)\n", 42 | "min_costs, c_mults = res" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": null, 48 | "id": "6beb0b0c-1c8a-4f47-a704-1606237367cc", 49 | "metadata": {}, 50 | "outputs": [], 51 | "source": [ 52 | "sorted(min_costs.items(), key=lambda x: x[1], reverse=True)" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": null, 58 | "id": "043c5902-b270-41b9-9727-1671804534eb", 59 | "metadata": {}, 60 | "outputs": [], 61 | "source": [] 62 | } 63 | ], 64 | "metadata": { 65 | "kernelspec": { 66 | "display_name": "Python 3 (ipykernel)", 67 | "language": "python", 68 | "name": "python3" 69 | }, 70 | "language_info": { 71 | "codemirror_mode": { 72 | "name": "ipython", 73 | "version": 3 74 | }, 75 | "file_extension": ".py", 76 | "mimetype": "text/x-python", 77 | "name": "python", 78 | "nbconvert_exporter": "python", 79 | "pygments_lexer": "ipython3", 80 | "version": "3.9.16" 81 | } 82 | }, 83 | "nbformat": 4, 84 | "nbformat_minor": 5 85 | } 86 | -------------------------------------------------------------------------------- /src/notes_inspect_multiopt.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "1c365acb-7182-4e0f-a0e0-16121c2b1fb5", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "%load_ext autoreload\n", 11 | "%autoreload 2" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "id": "b8dd91b2-f134-440b-a80b-47449a1bdaa4", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "import os\n", 22 | "import json\n", 23 | "import pandas as pd\n", 24 | "import numpy as np\n", 25 | "import pprint\n", 26 | "from procedures import dump_live_config, utc_ms, make_get_filepath\n", 27 | "from pure_funcs import (\n", 28 | " numpyize,\n", 29 | " denumpyize,\n", 30 | " live_config_dict_to_list_recursive_grid,\n", 31 | " sort_dict_keys,\n", 32 | " config_pretty_str,\n", 33 | " candidate_to_live_config,\n", 34 | " flatten_dict,\n", 35 | " ts_to_date_utc,\n", 36 | ")\n", 37 | "import matplotlib.pyplot as plt\n", 38 | "from collections import OrderedDict" 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": null, 44 | "id": "66cb2f77-b479-4b56-b771-a6804d3ff32f", 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "plt.rcParams[\"figure.figsize\"] = [24, 13.5]\n", 49 | "plt.rcParams[\"figure.facecolor\"] = \"w\"\n", 50 | "pd.set_option(\"display.precision\", 10)" 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": null, 56 | "id": "423111f8-847a-47db-9099-e2b5c56a9c8e", 57 | "metadata": {}, 58 | "outputs": [], 59 | "source": [ 60 | "def calc_dist(p0, p1):\n", 61 | " return ((p0[0] - p1[0]) ** 2 + (p0[1] - p1[1]) ** 2) ** 0.5\n", 62 | "\n", 63 | "\n", 64 | "def dominates_d(x, y, higher_is_better):\n", 65 | " \"\"\"Check if point x dominates point y.\"\"\"\n", 66 | " better_in_one = False\n", 67 | " for xi, yi, hib in zip(x, y, higher_is_better):\n", 68 | " if hib:\n", 69 | " if xi > yi:\n", 70 | " better_in_one = True\n", 71 | " elif xi < yi:\n", 72 | " return False\n", 73 | " else:\n", 74 | " if xi < yi:\n", 75 | " better_in_one = True\n", 76 | " elif xi > yi:\n", 77 | " return False\n", 78 | " return better_in_one\n", 79 | "\n", 80 | "\n", 81 | "def calc_pareto_front_d(objectives: dict, higher_is_better: [bool]):\n", 82 | " sorted_keys = sorted(\n", 83 | " objectives,\n", 84 | " key=lambda k: [\n", 85 | " -objectives[k][i] if higher_is_better[i] else objectives[k][i]\n", 86 | " for i in range(len(higher_is_better))\n", 87 | " ],\n", 88 | " )\n", 89 | " pareto_front = []\n", 90 | "\n", 91 | " for kcandidate in sorted_keys:\n", 92 | " # Assume candidate is not dominated initially\n", 93 | " is_dominated = False\n", 94 | " for kmember in pareto_front:\n", 95 | " if dominates_d(objectives[kmember], objectives[kcandidate], higher_is_better):\n", 96 | " is_dominated = True\n", 97 | " break\n", 98 | " if not is_dominated:\n", 99 | " # Remove any members of the pareto_front that are dominated by the candidate\n", 100 | " pareto_front = [\n", 101 | " kmember\n", 102 | " for kmember in pareto_front\n", 103 | " if not dominates_d(objectives[kcandidate], objectives[kmember], higher_is_better)\n", 104 | " ]\n", 105 | " pareto_front.append(kcandidate)\n", 106 | " return pareto_front" 107 | ] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "execution_count": null, 112 | "id": "6c4f8c9a-04d3-4a1e-9ad7-0d8c1303a679", 113 | "metadata": {}, 114 | "outputs": [], 115 | "source": [ 116 | "# location of 'all_results.txt' file from multisymbol opt\n", 117 | "with open(\"results_multi/2024-03-14T20_32_13_all_results.txt\") as f:\n", 118 | " lines = [x.strip() for x in f.readlines()]\n", 119 | "print(f\"n backtests: {len(lines)}\")" 120 | ] 121 | }, 122 | { 123 | "cell_type": "code", 124 | "execution_count": null, 125 | "id": "002b5631-5586-4571-9546-41956a7e03a8", 126 | "metadata": {}, 127 | "outputs": [], 128 | "source": [ 129 | "xs = [json.loads(x) for x in lines if x]\n", 130 | "res = pd.DataFrame([flatten_dict(x) for x in xs])\n", 131 | "\n", 132 | "worst_drawdown_lower_bound = res.iloc[0].args_worst_drawdown_lower_bound\n", 133 | "print(\"worst_drawdown_lower_bound\", worst_drawdown_lower_bound)\n", 134 | "\n", 135 | "# choose two metrics for pareto front.\n", 136 | "# choices:\n", 137 | "# [w_adg_weighted, w_drawdowns_daily_mean, w_loss_profit_ratio, w_price_action_distance_mean, w_sharpe_ratio]\n", 138 | "# for all \"w_\" metrics, lower is better.\n", 139 | "# w_metric = max(worst_drawdown_lower_bound, worst_drawdown) - worst_drawdown_lower_bound) * 10 {+/-} metric\n", 140 | "keys, higher_is_better = [\"w_adg_weighted\", \"w_sharpe_ratio\"], [False, False]\n", 141 | "keys = [\"analysis_\" + key for key in keys]\n", 142 | "candidates = res[res.analysis_worst_drawdown <= worst_drawdown_lower_bound][keys]\n", 143 | "print(\"n candidates\", len(candidates))\n", 144 | "pareto = candidates.loc[\n", 145 | " calc_pareto_front_d({i: x for i, x in zip(candidates.index, candidates.values)}, higher_is_better)\n", 146 | "]\n", 147 | "\n", 148 | "# normalize and find distances to ideal point\n", 149 | "cands_norm = (candidates - candidates.min()) / (candidates.max() - candidates.min())\n", 150 | "pareto_norm = (pareto - candidates.min()) / (candidates.max() - candidates.min())\n", 151 | "dists = [calc_dist(p, [float(x) for x in higher_is_better]) for p in pareto_norm.values]\n", 152 | "pareto_w_dists = pareto_norm.join(pd.Series(dists, name=\"dists\", index=pareto_norm.index))\n", 153 | "closest_to_ideal = pareto_w_dists.sort_values(\"dists\")\n", 154 | "best = closest_to_ideal.dists.idxmin()\n", 155 | "print(\"best\")\n", 156 | "print(candidates.loc[best])\n", 157 | "print(\"pareto front:\")\n", 158 | "pareto.loc[closest_to_ideal.index]" 159 | ] 160 | }, 161 | { 162 | "cell_type": "code", 163 | "execution_count": null, 164 | "id": "2f4d4df1-329a-4468-ae74-0fecc13a3e58", 165 | "metadata": {}, 166 | "outputs": [], 167 | "source": [ 168 | "# scatterplot all candidates in blue, pareto front in orange, ideal target in green, closest to ideal in red\n", 169 | "plt.scatter(*candidates.values.T)\n", 170 | "plt.scatter(*pareto.values.T)\n", 171 | "plt.scatter(*pareto.min().values.T)\n", 172 | "plt.scatter(*pareto.loc[best].T)" 173 | ] 174 | }, 175 | { 176 | "cell_type": "code", 177 | "execution_count": null, 178 | "id": "53c10855-37e5-4c47-ae6b-1ea7ae00e698", 179 | "metadata": {}, 180 | "outputs": [], 181 | "source": [ 182 | "ts_to_date_utc(utc_ms())[:19].replace(\":\", \"_\")" 183 | ] 184 | }, 185 | { 186 | "cell_type": "code", 187 | "execution_count": null, 188 | "id": "fd7b9b8f-30e7-4698-962b-d345cdd74fdc", 189 | "metadata": {}, 190 | "outputs": [], 191 | "source": [ 192 | "# config from best result\n", 193 | "best_d = xs[best]\n", 194 | "\n", 195 | "# cfg = best_d['position']\n", 196 | "cfg = best_d[\"live_config\"]\n", 197 | "cfg[\"long\"][\"wallet_exposure_limit\"] = cfg[\"global\"][\"TWE_long\"] / len(best_d[\"args\"][\"symbols\"])\n", 198 | "cfg[\"short\"][\"wallet_exposure_limit\"] = cfg[\"global\"][\"TWE_short\"] / len(best_d[\"args\"][\"symbols\"])\n", 199 | "cfg[\"long\"][\"enabled\"] = best_d[\"args\"][\"long_enabled\"]\n", 200 | "cfg[\"short\"][\"enabled\"] = best_d[\"args\"][\"short_enabled\"]\n", 201 | "fjson = json.dumps(best_d, indent=4, sort_keys=True)\n", 202 | "print(fjson)\n", 203 | "# best_d['args']['worst_drawdown_lower_bound'] = 0.25\n", 204 | "coins = \"\".join([s.replace(\"USDT\", \"\") for s in best_d[\"args\"][\"symbols\"]])\n", 205 | "coins = [s.replace(\"USDT\", \"\") for s in best_d[\"args\"][\"symbols\"]]\n", 206 | "fname = ts_to_date_utc(utc_ms())[:19].replace(\":\", \"_\")\n", 207 | "fname += \"_\" + (\"_\".join(coins) if len(coins) <= 5 else f\"{len(coins)}_coins\") + \".json\"\n", 208 | "full_path = make_get_filepath(os.path.join(\"results_multi_analysis\", fname))\n", 209 | "json.dump(best_d, open(full_path, \"w\"), indent=4, sort_keys=True)" 210 | ] 211 | }, 212 | { 213 | "cell_type": "code", 214 | "execution_count": null, 215 | "id": "1d1342e0-33f9-4cea-b6dc-f05d7c9c68c1", 216 | "metadata": {}, 217 | "outputs": [], 218 | "source": [] 219 | }, 220 | { 221 | "cell_type": "code", 222 | "execution_count": null, 223 | "id": "701195d9-d76e-4085-86ad-099bf1371f2a", 224 | "metadata": {}, 225 | "outputs": [], 226 | "source": [] 227 | }, 228 | { 229 | "cell_type": "code", 230 | "execution_count": null, 231 | "id": "83700827-4e3a-4509-b164-dcc3e06ef29d", 232 | "metadata": {}, 233 | "outputs": [], 234 | "source": [] 235 | } 236 | ], 237 | "metadata": { 238 | "kernelspec": { 239 | "display_name": "Python 3 (ipykernel)", 240 | "language": "python", 241 | "name": "python3" 242 | }, 243 | "language_info": { 244 | "codemirror_mode": { 245 | "name": "ipython", 246 | "version": 3 247 | }, 248 | "file_extension": ".py", 249 | "mimetype": "text/x-python", 250 | "name": "python", 251 | "nbconvert_exporter": "python", 252 | "pygments_lexer": "ipython3", 253 | "version": "3.9.16" 254 | } 255 | }, 256 | "nbformat": 4, 257 | "nbformat_minor": 5 258 | } 259 | -------------------------------------------------------------------------------- /src/tools/extract_best_config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import hjson 4 | import pandas as pd 5 | import argparse 6 | import sys 7 | import pprint 8 | import dictdiffer 9 | from tqdm import tqdm 10 | import traceback 11 | 12 | sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) 13 | from pure_funcs import config_pretty_str 14 | from copy import deepcopy 15 | 16 | sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) 17 | from procedures import utc_ms, make_get_filepath, dump_config, format_config 18 | from pure_funcs import ( 19 | flatten_dict, 20 | ts_to_date_utc, 21 | backtested_multiconfig2live_multiconfig, 22 | sort_dict_keys, 23 | config_pretty_str, 24 | ) 25 | 26 | 27 | def data_generator(all_results_filename, verbose=False): 28 | """ 29 | Generator function that iterates over an all_results.txt file written with dictdiffer. 30 | It yields the full data at each step by reconstructing it using diffs. 31 | 32 | Args: 33 | all_results_filename (str): Path to the all_results.txt file. 34 | verbose (bool): If True, enable all printing and progress tracking. 35 | 36 | Yields: 37 | dict: The full data dictionary at each step. 38 | """ 39 | prev_data = None 40 | # Get the total file size in bytes 41 | file_size = os.path.getsize(all_results_filename) 42 | # Disable progress bar and printing if verbose is False 43 | with open(all_results_filename, "r") as f: 44 | with tqdm( 45 | total=file_size, 46 | desc="Loading content", 47 | unit="B", 48 | unit_scale=True, 49 | unit_divisor=1024, 50 | disable=not verbose, 51 | ) as pbar: 52 | for line in f: 53 | if verbose: 54 | pbar.update(len(line.encode("utf-8"))) 55 | try: 56 | data = json.loads(line) 57 | if "diff" not in data: 58 | # This is the first entry; full data is provided 59 | prev_data = data 60 | yield deepcopy(prev_data) 61 | else: 62 | # Apply the diff to the previous data to get the current data 63 | diff = data["diff"] 64 | for i in range(len(diff)): 65 | if len(diff[i]) == 2: 66 | diff[i] = ("change", diff[i][0], (0.0, diff[i][1])) 67 | prev_data = dictdiffer.patch(diff, prev_data) 68 | yield deepcopy(prev_data) 69 | except Exception as e: 70 | if verbose: 71 | print( 72 | f"Error in data_generator: {e} Filename: {all_results_filename} line: {line}" 73 | ) 74 | yield {} 75 | if not verbose: 76 | pbar.close() 77 | 78 | 79 | # Function definitions remain unchanged 80 | def calc_dist(p0, p1): 81 | return ((p0[0] - p1[0]) ** 2 + (p0[1] - p1[1]) ** 2) ** 0.5 82 | 83 | 84 | def dominates_d(x, y, higher_is_better): 85 | """Check if point x dominates point y.""" 86 | better_in_one = False 87 | for xi, yi, hib in zip(x, y, higher_is_better): 88 | if hib: 89 | if xi > yi: 90 | better_in_one = True 91 | elif xi < yi: 92 | return False 93 | else: 94 | if xi < yi: 95 | better_in_one = True 96 | elif xi > yi: 97 | return False 98 | return better_in_one 99 | 100 | 101 | def calc_pareto_front_d(objectives: dict, higher_is_better: [bool]): 102 | sorted_keys = sorted( 103 | objectives, 104 | key=lambda k: [ 105 | -objectives[k][i] if higher_is_better[i] else objectives[k][i] 106 | for i in range(len(higher_is_better)) 107 | ], 108 | ) 109 | pareto_front = [] 110 | for kcandidate in sorted_keys: 111 | is_dominated = False 112 | for kmember in pareto_front: 113 | if dominates_d(objectives[kmember], objectives[kcandidate], higher_is_better): 114 | is_dominated = True 115 | break 116 | if not is_dominated: 117 | pareto_front = [ 118 | kmember 119 | for kmember in pareto_front 120 | if not dominates_d(objectives[kcandidate], objectives[kmember], higher_is_better) 121 | ] 122 | pareto_front.append(kcandidate) 123 | return pareto_front 124 | 125 | 126 | def gprint(verbose): 127 | if verbose: 128 | return print 129 | else: 130 | return lambda *args, **kwargs: None 131 | 132 | 133 | def process_single(file_location, verbose=False): 134 | print_ = gprint(verbose) 135 | try: 136 | result = json.load(open(file_location)) 137 | print_(config_pretty_str(sort_dict_keys(result))) 138 | return result 139 | except: 140 | pass 141 | xs = [] 142 | for x in data_generator(file_location, verbose=verbose): 143 | if x: 144 | xs.append(x) 145 | if not xs: 146 | print_(f"No valid data found in {file_location}") 147 | return None 148 | print_("Processing...") 149 | res = pd.DataFrame([flatten_dict(x) for x in xs]) 150 | 151 | # Determine the prefix based on the data 152 | if "analyses_combined" in xs[0]: 153 | analysis_prefix = "analyses_combined_" 154 | analysis_key = "analyses_combined" 155 | elif "analysis" in xs[0]: 156 | analysis_prefix = "analysis_" 157 | analysis_key = "analysis" 158 | else: 159 | raise Exception("Neither 'analyses_combined' nor 'analysis' found in data") 160 | 161 | keys, higher_is_better = ["w_0", "w_1"], [False, False] 162 | keys = [analysis_prefix + key for key in keys] 163 | print_("n backtests", len(res)) 164 | 165 | # Adjust the filtering condition based on the prefix 166 | res_keys_w_0 = res[analysis_prefix + "w_0"] 167 | res_keys_w_1 = res[analysis_prefix + "w_1"] 168 | candidates = res[(res_keys_w_0 <= 0.0) & (res_keys_w_1 <= 0.0)][keys] 169 | if len(candidates) == 0: 170 | candidates = res[keys] 171 | print_("n candidates", len(candidates)) 172 | if len(candidates) == 1: 173 | best = candidates.iloc[0].name 174 | pareto = candidates 175 | else: 176 | pareto = candidates.loc[ 177 | calc_pareto_front_d( 178 | {i: x for i, x in zip(candidates.index, candidates.values)}, higher_is_better 179 | ) 180 | ] 181 | cands_norm = (candidates - candidates.min()) / (candidates.max() - candidates.min()) 182 | pareto_norm = (pareto - candidates.min()) / (candidates.max() - candidates.min()) 183 | dists = [calc_dist(p, [float(x) for x in higher_is_better]) for p in pareto_norm.values] 184 | pareto_w_dists = pareto_norm.join( 185 | pd.Series(dists, name="dist_to_ideal", index=pareto_norm.index) 186 | ) 187 | closest_to_ideal = pareto_w_dists.sort_values("dist_to_ideal") 188 | best = closest_to_ideal.dist_to_ideal.idxmin() 189 | print_("best") 190 | print_(candidates.loc[best]) 191 | print_("pareto front:") 192 | res_to_print = res[[x for x in res.columns if analysis_prefix[:-1] in x]].loc[ 193 | closest_to_ideal.index 194 | ] 195 | res_to_print.columns = [x.replace(analysis_prefix, "") for x in res_to_print.columns] 196 | print_(res_to_print) 197 | 198 | # Processing the best result for configuration 199 | best_d = xs[best] 200 | # Adjust for 'analysis' or 'analyses_combined' 201 | best_d[analysis_key]["n_iters"] = len(xs) 202 | if "config" in best_d: 203 | best_d.update(deepcopy(best_d["config"])) 204 | del best_d["config"] 205 | fjson = config_pretty_str(best_d) 206 | print_(fjson) 207 | print_(file_location) 208 | full_path = file_location.replace("_all_results.txt", "") + ".json" 209 | base_path = os.path.split(full_path)[0] 210 | full_path = make_get_filepath(full_path.replace(base_path, base_path + "_analysis/")) 211 | pareto_to_dump = [x for i, x in enumerate(xs) if i in pareto.index] 212 | for i in range(len(pareto_to_dump)): 213 | if "config" in pareto_to_dump[i]: 214 | pareto_to_dump[i].update(deepcopy(pareto_to_dump[i]["config"])) 215 | del pareto_to_dump[i]["config"] 216 | with open(full_path.replace(".json", "_pareto.txt"), "w") as f: 217 | for x in pareto_to_dump: 218 | f.write(json.dumps(x) + "\n") 219 | dump_config(format_config(best_d), full_path) 220 | return best_d 221 | 222 | 223 | def main(args): 224 | if os.path.isdir(args.file_location): 225 | for fname in sorted(os.listdir(args.file_location), reverse=True): 226 | fpath = os.path.join(args.file_location, fname) 227 | try: 228 | process_single(fpath) 229 | print(f"successfully processed {fpath}") 230 | except Exception as e: 231 | print(f"error with {fpath} {e}") 232 | traceback.print_exc() 233 | else: 234 | try: 235 | result = process_single(args.file_location, args.verbose) 236 | print(f"successfully processed {args.file_location}") 237 | except Exception as e: 238 | print(f"error with {args.file_location} {e}") 239 | traceback.print_exc() 240 | 241 | 242 | if __name__ == "__main__": 243 | parser = argparse.ArgumentParser(description="Process results.") 244 | parser.add_argument("file_location", type=str, help="Location of the results file or directory") 245 | parser.add_argument( 246 | "-v", 247 | "--verbose", 248 | action="store_true", 249 | help="Disable printing and progress tracking", 250 | ) 251 | args = parser.parse_args() 252 | 253 | main(args) 254 | -------------------------------------------------------------------------------- /src/tools/generate_mcap_list.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import argparse 4 | import sys 5 | import os 6 | 7 | sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) 8 | from pure_funcs import calc_hash, symbol_to_coin, ts_to_date_utc 9 | from procedures import utc_ms 10 | 11 | 12 | def is_stablecoin(elm): 13 | if elm["symbol"] in ["tether", "usdb", "usdy", "tusd", "usd0", "usde"]: 14 | return True 15 | if ( 16 | all([abs(elm[k] - 1.0) < 0.01 for k in ["high_24h", "low_24h", "current_price"]]) 17 | and abs(elm["price_change_24h"]) < 0.01 18 | ): 19 | return True 20 | return False 21 | 22 | 23 | def get_top_market_caps(n_coins, minimum_market_cap_millions, exchange=None): 24 | # Fetch the top N coins by market cap 25 | markets_url = "https://api.coingecko.com/api/v3/coins/markets" 26 | per_page = 150 27 | page = 1 28 | params = { 29 | "vs_currency": "usd", 30 | "order": "market_cap_desc", 31 | "per_page": per_page, 32 | "page": 1, 33 | "sparkline": "false", 34 | } 35 | minimum_market_cap = minimum_market_cap_millions * 1e6 36 | approved_coins = {} 37 | prev_hash = None 38 | exchange_approved_coins = None 39 | if exchange is not None: 40 | exchanges = exchange.split(",") 41 | import ccxt 42 | 43 | exchange_map = { 44 | "bybit": ("bybit", "USDT"), 45 | "binance": ("binanceusdm", "USDT"), 46 | "bitget": ("bitget", "USDT"), 47 | "hyperliquid": ("hyperliquid", "USDC"), 48 | "gateio": ("gateio", "USDT"), 49 | "okx": ("okx", "USDT"), 50 | } 51 | exchange_approved_coins = set() 52 | for exchange in exchanges: 53 | try: 54 | cc = getattr(ccxt, exchange_map[exchange][0])() 55 | cc.options["defaultType"] = "swap" 56 | markets = cc.fetch_markets() 57 | for elm in markets: 58 | if ( 59 | elm["swap"] 60 | and elm["active"] 61 | and elm["symbol"].endswith(f":{exchange_map[exchange][1]}") 62 | ): 63 | exchange_approved_coins.add(symbol_to_coin(elm["symbol"])) 64 | print(f"Added coin filter for {exchange}") 65 | except Exception as e: 66 | print(f"error loading ccxt for {exchange} {e}") 67 | while len(approved_coins) < n_coins: 68 | response = requests.get(markets_url, params=params) 69 | if response.status_code != 200: 70 | print(f"Error fetching market data: {response.status_code} - {response.text}") 71 | break 72 | market_data = response.json() 73 | new_hash = calc_hash(market_data) 74 | if new_hash == prev_hash: 75 | break 76 | prev_hash = new_hash 77 | added = [] 78 | disapproved = {} 79 | for elm in market_data: 80 | coin = elm["symbol"].upper() 81 | if len(approved_coins) >= n_coins: 82 | print(f"N coins == {n_coins}") 83 | if added: 84 | print(f"Added approved coins {','.join(added)}") 85 | return approved_coins 86 | if elm["market_cap"] < minimum_market_cap: 87 | print("Lowest market cap", coin) 88 | if added: 89 | print(f"Added approved coins {','.join(added)}") 90 | return approved_coins 91 | if is_stablecoin(elm): 92 | disapproved[coin] = "stablecoin" 93 | continue 94 | if exchange_approved_coins is not None and coin not in exchange_approved_coins: 95 | disapproved[coin] = "not_active" 96 | continue 97 | if coin not in approved_coins: 98 | approved_coins[coin] = elm 99 | added.append(coin) 100 | print(f"added approved coins {','.join(added)}") 101 | if disapproved: 102 | for key in set(disapproved.values()): 103 | to_print = [c for c in disapproved if disapproved[c] == key] 104 | print(f"disapproved {key} {','.join(to_print)}") 105 | disapproved = {} 106 | if len(approved_coins) >= n_coins: 107 | break 108 | params["page"] += 1 109 | return approved_coins 110 | 111 | 112 | if __name__ == "__main__": 113 | parser = argparse.ArgumentParser(prog="mcap generator", description="generate_mcap_list") 114 | parser.add_argument( 115 | f"--n_coins", 116 | f"-n", 117 | type=int, 118 | dest="n_coins", 119 | required=False, 120 | default=100, 121 | help=f"Maxiumum number of top market cap coins. Default=100", 122 | ) 123 | parser.add_argument( 124 | f"--minimum_market_cap_dollars", 125 | f"-m", 126 | type=float, 127 | dest="minimum_market_cap_millions", 128 | required=False, 129 | default=300.0, 130 | help=f"Minimum market cap in millions of USD. Default=300.0", 131 | ) 132 | parser.add_argument( 133 | f"--exchange", 134 | f"-e", 135 | type=str, 136 | dest="exchange", 137 | required=False, 138 | default=None, 139 | help=f"Optional: filter by coins available on exchange. Comma separated values. Default=None", 140 | ) 141 | parser.add_argument( 142 | f"--output", 143 | f"-o", 144 | type=str, 145 | dest="output", 146 | required=False, 147 | default=None, 148 | help="Optional: Output path. Default=configs/approved_coins_{n_coins}_{min_mcap}.json", 149 | ) 150 | args = parser.parse_args() 151 | 152 | market_caps = get_top_market_caps(args.n_coins, args.minimum_market_cap_millions, args.exchange) 153 | if args.output is None: 154 | fname = f"configs/approved_coins_{ts_to_date_utc(utc_ms())[:10]}" 155 | fname += f"_{args.n_coins}_coins_{int(args.minimum_market_cap_millions)}_min_mcap" 156 | if args.exchange is not None: 157 | fname += "_" + "_".join(args.exchange.split(",")) 158 | fname += ".json" 159 | else: 160 | fname = args.output 161 | print(f"Dumping output to {fname}") 162 | json.dump(list(market_caps), open(fname, "w")) 163 | --------------------------------------------------------------------------------