├── .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 | 
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 | [](https://discord.gg/QAF2H2UmzZ)
140 |
141 | [](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 |
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 |
32 |
--------------------------------------------------------------------------------
/docs/images/pbot_logo_square.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
--------------------------------------------------------------------------------