├── .github └── workflows │ └── python-publish.yml ├── .gitignore ├── LICENSE.txt ├── Pipfile ├── Pipfile.lock ├── README.md ├── setup.py ├── src ├── __init__.py └── three_commas │ ├── __init__.py │ ├── api │ ├── __init__.py │ ├── v2 │ │ ├── __init__.py │ │ └── smart_trades.py │ └── ver1 │ │ ├── __init__.py │ │ ├── accounts.py │ │ ├── bots.py │ │ ├── deals.py │ │ ├── grid_bots.py │ │ ├── loose_accounts.py │ │ ├── marketplace.py │ │ ├── ping.py │ │ ├── smart_trades.py │ │ ├── time.py │ │ └── users.py │ ├── cached_api.py │ ├── configuration.py │ ├── error.py │ ├── model │ ├── __init__.py │ ├── generated_enums.py │ ├── generated_models.py │ ├── models.py │ └── other_enums.py │ ├── site.py │ ├── streams │ ├── __init__.py │ └── streams.py │ ├── sys_utils.py │ └── utils │ ├── __init__.py │ ├── bot_utils.py │ ├── other_utils.py │ └── pairs_utils.py ├── test ├── __init__.py ├── sample_data │ ├── accounts │ │ ├── binance_account.json │ │ ├── ftx_account.json │ │ ├── market_list.json │ │ ├── paper_account.json │ │ └── pie_chart_data.json │ ├── bots │ │ ├── btc │ │ │ ├── bot_show_btc.json │ │ │ └── bot_show_with_events_btc.json │ │ └── usdt │ │ │ └── bot_show_with_events_usdt.json │ ├── deals │ │ └── usdt │ │ │ ├── deal_show_usdt.json │ │ │ ├── deals_market_orders_usdt.json │ │ │ └── deals_usdt.json │ └── errors │ │ ├── api_key_has_no_permission_error.json │ │ ├── api_key_invalid_or_expired_error.json │ │ ├── bo_too_small_no_pair.json │ │ ├── bo_too_small_with_pair.json │ │ ├── multiple_bo_so_errors.json │ │ └── signature_invalid.json ├── test_3c_parameter_parser.py ├── test_bot_models.py ├── test_enums.py ├── test_generated_models.py ├── test_models_field_types.py └── test_three_commas_errors.py └── type_generators ├── 3commas_swaggerdoc.json ├── 3commas_swaggerdoc_2022_01_24.json ├── api_generator.py ├── auto_api_from_swaggerdoc.py ├── auto_api_from_swaggerdoc_2.py ├── enum_generator.py ├── model_generator.py └── parsing_and_return_mapping.py /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Set up Python 26 | uses: actions/setup-python@v3 27 | with: 28 | python-version: '3.x' 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install build 33 | - name: Build package 34 | run: python -m build 35 | - name: Publish package 36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 37 | with: 38 | user: __token__ 39 | password: ${{ secrets.PYPI_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/python 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 4 | 5 | ### Python ### 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | cover/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | db.sqlite3-journal 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | .pybuilder/ 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # IPython 87 | profile_default/ 88 | ipython_config.py 89 | 90 | # pyenv 91 | # For a library or package, you might want to ignore these files since the code is 92 | # intended to run in multiple environments; otherwise, check them in: 93 | # .python-version 94 | 95 | # pipenv 96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 99 | # install all needed dependencies. 100 | #Pipfile.lock 101 | 102 | # poetry 103 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 104 | # This is especially recommended for binary packages to ensure reproducibility, and is more 105 | # commonly ignored for libraries. 106 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 107 | #poetry.lock 108 | 109 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 110 | __pypackages__/ 111 | 112 | # Celery stuff 113 | celerybeat-schedule 114 | celerybeat.pid 115 | 116 | # SageMath parsed files 117 | *.sage.py 118 | 119 | # Environments 120 | .env 121 | .venv 122 | env/ 123 | venv/ 124 | ENV/ 125 | env.bak/ 126 | venv.bak/ 127 | 128 | # Spyder project settings 129 | .spyderproject 130 | .spyproject 131 | 132 | # Rope project settings 133 | .ropeproject 134 | 135 | # mkdocs documentation 136 | /site 137 | 138 | # mypy 139 | .mypy_cache/ 140 | .dmypy.json 141 | dmypy.json 142 | 143 | # Pyre type checker 144 | .pyre/ 145 | 146 | # pytype static type analyzer 147 | .pytype/ 148 | 149 | # Cython debug symbols 150 | cython_debug/ 151 | 152 | .idea/ 153 | # End of https://www.toptal.com/developers/gitignore/api/python 154 | bkp/ 155 | playground.py 156 | type_generators/generated_api/ 157 | type_generators/api/ -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | pytest = "*" 8 | cachetools = "*" 9 | py3cw = "*" 10 | python-dotenv = "*" 11 | aenum = "*" 12 | websockets = "*" 13 | prodict = "*" 14 | 15 | [dev-packages] 16 | 17 | [requires] 18 | python_version = "3.8" 19 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "ccfe5bd9e151c60b64fa6265003c7a644984ea8b7e35c55f3e6db28557a0b606" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.8" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "aenum": { 20 | "hashes": [ 21 | "sha256:12ae89967f2e25c0ce28c293955d643f891603488bc3d9946158ba2b35203638", 22 | "sha256:525b4870a27d0b471c265bda692bc657f1e0dd7597ad4186d072c59f9db666f6", 23 | "sha256:aed2c273547ae72a0d5ee869719c02a643da16bf507c80958faadc7e038e3f73" 24 | ], 25 | "index": "pypi", 26 | "version": "==3.1.11" 27 | }, 28 | "attrs": { 29 | "hashes": [ 30 | "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", 31 | "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd" 32 | ], 33 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 34 | "version": "==21.4.0" 35 | }, 36 | "cachetools": { 37 | "hashes": [ 38 | "sha256:6a94c6402995a99c3970cc7e4884bb60b4a8639938157eeed436098bf9831757", 39 | "sha256:f9f17d2aec496a9aa6b76f53e3b614c965223c061982d434d160f930c698a9db" 40 | ], 41 | "index": "pypi", 42 | "version": "==5.2.0" 43 | }, 44 | "certifi": { 45 | "hashes": [ 46 | "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d", 47 | "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412" 48 | ], 49 | "markers": "python_version >= '3.6'", 50 | "version": "==2022.6.15" 51 | }, 52 | "charset-normalizer": { 53 | "hashes": [ 54 | "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5", 55 | "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413" 56 | ], 57 | "markers": "python_version >= '3.6'", 58 | "version": "==2.1.0" 59 | }, 60 | "idna": { 61 | "hashes": [ 62 | "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", 63 | "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" 64 | ], 65 | "markers": "python_version >= '3.5'", 66 | "version": "==3.3" 67 | }, 68 | "iniconfig": { 69 | "hashes": [ 70 | "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", 71 | "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" 72 | ], 73 | "version": "==1.1.1" 74 | }, 75 | "packaging": { 76 | "hashes": [ 77 | "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", 78 | "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" 79 | ], 80 | "markers": "python_version >= '3.6'", 81 | "version": "==21.3" 82 | }, 83 | "pluggy": { 84 | "hashes": [ 85 | "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", 86 | "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" 87 | ], 88 | "markers": "python_version >= '3.6'", 89 | "version": "==1.0.0" 90 | }, 91 | "prodict": { 92 | "hashes": [ 93 | "sha256:7a4dfa0f9d361b34fc0f7367ec67923f97595f79917730f19fda7ec078dd1d77" 94 | ], 95 | "index": "pypi", 96 | "version": "==0.8.18" 97 | }, 98 | "py": { 99 | "hashes": [ 100 | "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", 101 | "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" 102 | ], 103 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 104 | "version": "==1.11.0" 105 | }, 106 | "py3cw": { 107 | "hashes": [ 108 | "sha256:bd96f81a47392ee8b3be473f1636992621551b1a52419a4db90f7a34b13957cf", 109 | "sha256:bf4df7bd8939c028a6464741053be00eebe659e6b7130e7545a86183131f12f0" 110 | ], 111 | "index": "pypi", 112 | "version": "==0.0.39" 113 | }, 114 | "pyparsing": { 115 | "hashes": [ 116 | "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", 117 | "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" 118 | ], 119 | "markers": "python_full_version >= '3.6.8'", 120 | "version": "==3.0.9" 121 | }, 122 | "pytest": { 123 | "hashes": [ 124 | "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c", 125 | "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45" 126 | ], 127 | "index": "pypi", 128 | "version": "==7.1.2" 129 | }, 130 | "python-dotenv": { 131 | "hashes": [ 132 | "sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f", 133 | "sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938" 134 | ], 135 | "index": "pypi", 136 | "version": "==0.20.0" 137 | }, 138 | "requests": { 139 | "hashes": [ 140 | "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", 141 | "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" 142 | ], 143 | "markers": "python_version >= '3.7' and python_version < '4'", 144 | "version": "==2.28.1" 145 | }, 146 | "tomli": { 147 | "hashes": [ 148 | "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", 149 | "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" 150 | ], 151 | "markers": "python_version >= '3.7'", 152 | "version": "==2.0.1" 153 | }, 154 | "urllib3": { 155 | "hashes": [ 156 | "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14", 157 | "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e" 158 | ], 159 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", 160 | "version": "==1.26.9" 161 | }, 162 | "websockets": { 163 | "hashes": [ 164 | "sha256:07cdc0a5b2549bcfbadb585ad8471ebdc7bdf91e32e34ae3889001c1c106a6af", 165 | "sha256:210aad7fdd381c52e58777560860c7e6110b6174488ef1d4b681c08b68bf7f8c", 166 | "sha256:28dd20b938a57c3124028680dc1600c197294da5db4292c76a0b48efb3ed7f76", 167 | "sha256:2f94fa3ae454a63ea3a19f73b95deeebc9f02ba2d5617ca16f0bbdae375cda47", 168 | "sha256:31564a67c3e4005f27815634343df688b25705cccb22bc1db621c781ddc64c69", 169 | "sha256:347974105bbd4ea068106ec65e8e8ebd86f28c19e529d115d89bd8cc5cda3079", 170 | "sha256:379e03422178436af4f3abe0aa8f401aa77ae2487843738542a75faf44a31f0c", 171 | "sha256:3eda1cb7e9da1b22588cefff09f0951771d6ee9fa8dbe66f5ae04cc5f26b2b55", 172 | "sha256:51695d3b199cd03098ae5b42833006a0f43dc5418d3102972addc593a783bc02", 173 | "sha256:54c000abeaff6d8771a4e2cef40900919908ea7b6b6a30eae72752607c6db559", 174 | "sha256:5b936bf552e4f6357f5727579072ff1e1324717902127ffe60c92d29b67b7be3", 175 | "sha256:6075fd24df23133c1b078e08a9b04a3bc40b31a8def4ee0b9f2c8865acce913e", 176 | "sha256:661f641b44ed315556a2fa630239adfd77bd1b11cb0b9d96ed8ad90b0b1e4978", 177 | "sha256:6ea6b300a6bdd782e49922d690e11c3669828fe36fc2471408c58b93b5535a98", 178 | "sha256:6ed1d6f791eabfd9808afea1e068f5e59418e55721db8b7f3bfc39dc831c42ae", 179 | "sha256:7934e055fd5cd9dee60f11d16c8d79c4567315824bacb1246d0208a47eca9755", 180 | "sha256:7ab36e17af592eec5747c68ef2722a74c1a4a70f3772bc661079baf4ae30e40d", 181 | "sha256:7f6d96fdb0975044fdd7953b35d003b03f9e2bcf85f2d2cf86285ece53e9f991", 182 | "sha256:83e5ca0d5b743cde3d29fda74ccab37bdd0911f25bd4cdf09ff8b51b7b4f2fa1", 183 | "sha256:85506b3328a9e083cc0a0fb3ba27e33c8db78341b3eb12eb72e8afd166c36680", 184 | "sha256:8af75085b4bc0b5c40c4a3c0e113fa95e84c60f4ed6786cbb675aeb1ee128247", 185 | "sha256:8b1359aba0ff810d5830d5ab8e2c4a02bebf98a60aa0124fb29aa78cfdb8031f", 186 | "sha256:8fbd7d77f8aba46d43245e86dd91a8970eac4fb74c473f8e30e9c07581f852b2", 187 | "sha256:907e8247480f287aa9bbc9391bd6de23c906d48af54c8c421df84655eef66af7", 188 | "sha256:93d5ea0b5da8d66d868b32c614d2b52d14304444e39e13a59566d4acb8d6e2e4", 189 | "sha256:97bc9d41e69a7521a358f9b8e44871f6cdeb42af31815c17aed36372d4eec667", 190 | "sha256:994cdb1942a7a4c2e10098d9162948c9e7b235df755de91ca33f6e0481366fdb", 191 | "sha256:a141de3d5a92188234afa61653ed0bbd2dde46ad47b15c3042ffb89548e77094", 192 | "sha256:a1e15b230c3613e8ea82c9fc6941b2093e8eb939dd794c02754d33980ba81e36", 193 | "sha256:aad5e300ab32036eb3fdc350ad30877210e2f51bceaca83fb7fef4d2b6c72b79", 194 | "sha256:b529fdfa881b69fe563dbd98acce84f3e5a67df13de415e143ef053ff006d500", 195 | "sha256:b9c77f0d1436ea4b4dc089ed8335fa141e6a251a92f75f675056dac4ab47a71e", 196 | "sha256:bb621ec2dbbbe8df78a27dbd9dd7919f9b7d32a73fafcb4d9252fc4637343582", 197 | "sha256:c7250848ce69559756ad0086a37b82c986cd33c2d344ab87fea596c5ac6d9442", 198 | "sha256:c8d1d14aa0f600b5be363077b621b1b4d1eb3fbf90af83f9281cda668e6ff7fd", 199 | "sha256:d1655a6fc7aecd333b079d00fb3c8132d18988e47f19740c69303bf02e9883c6", 200 | "sha256:d6353ba89cfc657a3f5beabb3b69be226adbb5c6c7a66398e17809b0ce3c4731", 201 | "sha256:da4377904a3379f0c1b75a965fff23b28315bcd516d27f99a803720dfebd94d4", 202 | "sha256:e49ea4c1a9543d2bd8a747ff24411509c29e4bdcde05b5b0895e2120cb1a761d", 203 | "sha256:e4e08305bfd76ba8edab08dcc6496f40674f44eb9d5e23153efa0a35750337e8", 204 | "sha256:e6fa05a680e35d0fcc1470cb070b10e6fe247af54768f488ed93542e71339d6f", 205 | "sha256:e7e6f2d6fd48422071cc8a6f8542016f350b79cc782752de531577d35e9bd677", 206 | "sha256:e904c0381c014b914136c492c8fa711ca4cced4e9b3d110e5e7d436d0fc289e8", 207 | "sha256:ec2b0ab7edc8cd4b0eb428b38ed89079bdc20c6bdb5f889d353011038caac2f9", 208 | "sha256:ef5ce841e102278c1c2e98f043db99d6755b1c58bde475516aef3a008ed7f28e", 209 | "sha256:f351c7d7d92f67c0609329ab2735eee0426a03022771b00102816a72715bb00b", 210 | "sha256:fab7c640815812ed5f10fbee7abbf58788d602046b7bb3af9b1ac753a6d5e916", 211 | "sha256:fc06cc8073c8e87072138ba1e431300e2d408f054b27047d047b549455066ff4" 212 | ], 213 | "index": "pypi", 214 | "version": "==10.3" 215 | } 216 | }, 217 | "develop": {} 218 | } 219 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Title 2 | 3 | This library provides tools that help develop code that access the 4 | [3commas api](https://github.com/3commas-io/3commas-official-api-docs) very fast and easy. 5 | The library is built on top of the py3cw library. 6 | Main features are prebuilt functions for the api, models for easier access of the returned data, 7 | automatic attribute parsing of the returned data, built in error parsing. 8 | 9 | WIP: this library is still in construction. Some endpoints implementations are missing. Feel free to create a PR in github 10 | if you desire a particular endpoint implementation https://github.com/badass-blockchain/python-three-commas 11 | 12 | ## Installation 13 | 14 | pip install three-commas 15 | 16 | ## Usage 17 | 18 | The package is built mirroring the names of the api paths. For example: 19 | 20 | from three_commas import api 21 | 22 | api.ver1.bots # bots endpoint 23 | api.ver1.accounts # account endpoint 24 | api.ver1.deals # deals endpoint 25 | api.ver1.users # users endpoint 26 | api.v2.smart_trades # v2 smart_trades endpoint 27 | 28 | The endpoints are mirrored from the paths too. 29 | 30 | # GET /ver1/bots/pairs_black_list 31 | error, black_list_pairs = api.ver1.bots.get_pairs_black_list() 32 | 33 | # GET /v2/smart_trades/{id} 34 | error, smart_trades_list = api.v2.smart_trades.get_by_id(id=) 35 | 36 | 37 | You can get all bots with: 38 | 39 | error, bots_list = api.ver1.bots.get() 40 | 41 | Or a single bot with: 42 | 43 | error, bot = api.ver1.bots.get_show_by_id(bot_id=) 44 | 45 | Or a smart trade 46 | 47 | error, smart_trade = api.v2.smart_trades.get_by_id(id=9993000) 48 | 49 | The endpoints return a dict object with added functionality. You can use the object like a normal dictionary 50 | (exactly how you receive from py3cw), or use the added functions. 51 | For example if you want to get the bot max_active_deals you can do both: 52 | 53 | error, bot = api.ver1.bots.get_show_by_id(bot_id=9999999) 54 | if not error: 55 | max_active_deals = bot['max_active_deals'] 56 | max_active_deals = bot.max_active_deals 57 | 58 | ### Websocket Streams 59 | 60 | You can easily connect to the websockets 61 | You can use annotations. 62 | 63 | import three_commas 64 | from three_commas.model import DealEntity, SmartTradeV2Entity 65 | 66 | @three_commas.streams.smart_trades 67 | def handle_smart_trades(smart_trade: SmartTradeV2Entity): 68 | # Do here something with the smart trade 69 | # Every new smart trade is passed to this function 70 | print(smart_trade) 71 | 72 | @three_commas.streams.deals 73 | def handle_deals(deal: DealEntity): 74 | # do your awesome stuff with the deal 75 | print(deal) # {'id': 1311811868, 'type': 'Deal', 'bot_id': 6313165, 'max_safety_orders': 6, 'deal_has_error': False .... 76 | print(deal.account_id) # 99648312 77 | print(deal.created_at) # string object '2022-02-18T05:26:06.803Z' 78 | print(deal.parsed(True).created_at) # datetime.datetime object 79 | 80 | 81 | In order to use the websocket streams you need to set the api key and secret in your environment. 82 | [Later in the document you can find how to set up the environment variables](#Set the api key and secret) 83 | 84 | Or you can pass the keys to the decorator: 85 | 86 | @three_commas.streams.deals(api_key='', secret='') 87 | def handle_deals(deal): 88 | ... 89 | 90 | 91 | For debugging you can turn on debug level 92 | 93 | import logging 94 | 95 | logging.getLogger('three_commas.streams').setLevel(level=logging.DEBUG) 96 | 97 | You will see a lot of websocket messages including pings: 98 | 99 | > DEBUG:three_commas.streams.streams: {"type": "welcome"} 100 | > DEBUG:three_commas.streams.streams: {"type": "ping", "message": 1645286932} 101 | > DEBUG:three_commas.streams.streams: {"type": "ping", "message": 1645286935} 102 | > DEBUG:three_commas.streams.streams: {"type": "ping", "message": 1645286938} 103 | 104 | You can also set up the level to info for less verbose loging 105 | logging.getLogger('three_commas.streams').setLevel(level=logging.INFO) 106 | 107 | 108 | ### Parsing 109 | One of the features of this library is the automatic parsing of the returned data. 110 | Some numeric data fetched from the api is returned as string. For example in the bot object: 111 | 112 | ... 113 | "base_order_volume": "0.003", 114 | "safety_order_volume": "0.003", 115 | "safety_order_step_percentage": "1.0", 116 | ... 117 | 118 | Now you do not need to bother checking the type of the field and parsing it into the desired type. 119 | This library auto parses these fields: 120 | 121 | error, bot = api.ver1.bots.get_show(9999999) 122 | # base_order_volume is a float 123 | base_order_volume = bot.get_base_order_volume() 124 | 125 | 126 | Parsing (except datetime fields) is done by default. 127 | If you do not want the field to be parsed, and you want the original string to be returned use parsed=False 128 | 129 | error, bot = api.ver1.bots.get_show(9999999) 130 | # base_order_volume is a str 131 | base_order_volume = bot.parsed(False).base_order_volume 132 | 133 | 134 | Some fields like "created_at" are timestamps. You can parse these fields to a python datetime object. 135 | Timestamp fields are NOT parsed by default, only on demand: 136 | 137 | error, account = api.ver1.accounts.get_by_id(8888888) 138 | 139 | # the original string returned by the api 140 | created_at_str = account.created_at 141 | 142 | # parsed into a datetime.datetime object 143 | created_at_datetime = account.parsed(True).created_at 144 | 145 | 146 | ### Api keys 147 | 148 | In order to use the api you need to set the api key and secret. This could be done globally or per request. 149 | To set it globally you need to set the environment variables THREE_COMMAS_API_KEY and THREE_COMMAS_API_SECRET. 150 | To do it per request just pass them into the function: 151 | 152 | error, account = api.ver1.accounts.get_by_id(8888888, api_key='my_key', api_secret='my_secret') 153 | 154 | Request keys have priority. If both global and request keys are set, then the request keys will be used. 155 | 156 | ### Forced mode 157 | 158 | You can set the forced mode globally or also per request. 159 | To set it globally set the environment variable THREE_COMMAS_FORCED_MODE to either paper or real. 160 | To use the forced mode per request pass it as an argument: 161 | 162 | error, paper_deals = api.ver1.deals.get(forced_mode='paper') 163 | error, real_deals = api.ver1.deals.get(forced_mode='real') 164 | 165 | 166 | ### Enums 167 | 168 | Some enum fields have functionality. 169 | 170 | error, accounts_list = api_v1.accounts.get_accounts() 171 | if not error: 172 | for account in accounts_list: 173 | if account.get_market_code().is_binance(): 174 | # do stuff with the binance account. 175 | else: 176 | # deal with error here 177 | 178 | You can check what enums are available in the three_commas.model.generated_enums package 179 | 180 | ### Set the api key and secret 181 | 182 | Always make sure your api keys are stored securely and are not pushed into any public repositories 183 | 184 | #### dotenv file and dotenv loader (preferred, more secure) 185 | 186 | You can set key and secret as an environment variables. The preferred more secure way is to use a .env file: 187 | 188 | create a file called ".env" with the following content: 189 | 190 | THREE_COMMAS_API_KEY= 191 | THREE_COMMAS_API_SECRET= 192 | 193 | Use a .env loader as python-dotenv. Install it on your machine with pip: "pip install python-dotenv". 194 | If your project is a git project make sure you gitignore the .env file. 195 | Then in your code 196 | 197 | from dotenv import load_dotenv 198 | load_dotenv() 199 | 200 | Now your variables are loaded. Enjoy using the library. 201 | 202 | #### setting the environment variable with the os module (less secure) 203 | 204 | You can also set the api key and secret directly in the code. This method is less secure and not recommended. 205 | 206 | import os 207 | 208 | os.environ["THREE_COMMAS_API_KEY"] = 209 | os.environ["THREE_COMMAS_API_SECRET"] = 210 | 211 | 212 | 213 | ### Examples 214 | #### Posting a new smart trade 215 | smart_trade = { 216 | 'account_id': 99999999, 217 | 'pair': 'USDT_FUN', 218 | 'position': { 219 | 'type': 'buy', 220 | 'order_type': 'market', 221 | 'units': { 222 | 'value': "30526.0", 223 | }, 224 | "total": { 225 | "value": "600.61907506" 226 | } 227 | }, 228 | 'take_profit': { 229 | 'enabled': False, 230 | }, 231 | 'stop_loss': { 232 | 'enabled': False, 233 | } 234 | 235 | } 236 | 237 | error, smart_trade_response = api.v2.smart_trades.post(smart_trade) 238 | 239 | #### Retrieving a smart trade 240 | 241 | error, smart_trade = api.v2.smart_trades.get_by_id(13819196) 242 | if not error: 243 | # Do your awesome stuff with the smart trade 244 | print(smart_trade.profit) 245 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | with open('README.md', 'r') as fh: 4 | long_description = fh.read() 5 | 6 | setup( 7 | name='three-commas', 8 | packages=['three_commas', 9 | 'three_commas.api', 10 | 'three_commas.api.ver1', 11 | 'three_commas.api.v2', 12 | 'three_commas.model', 13 | 'three_commas.utils', 14 | 'three_commas.streams', 15 | ], 16 | version='0.2.9', 17 | description='Python api wrapper for 3commas with extended functionality in the api, models, error handling', 18 | url='https://github.com/badass-blockchain/python-three-commas', 19 | author='Sergey Gerodes', 20 | author_email='sgerodes@gmail.com', 21 | long_description=long_description, 22 | long_description_content_type='text/markdown', 23 | py_modules=['some_module'], 24 | package_dir={'': 'src'}, 25 | keywords=['python', '3commas', 'api', 'crypto', 'cryptocurrency', 26 | 'three commas', 'bitcoin', 'trading', 'btc', 'eth'], 27 | python_requires='>=3.8', 28 | classifiers=[ 29 | "Programming Language :: Python :: 3", 30 | "Programming Language :: Python :: 3.8", 31 | "Operating System :: Unix", 32 | "Operating System :: MacOS :: MacOS X", 33 | "Operating System :: Microsoft :: Windows", 34 | ], 35 | install_requires=[ 36 | 'py3cw', 37 | 'cachetools', 38 | 'aenum', 39 | 'websockets' 40 | ] 41 | ) 42 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgerodes/python-three-commas/3d73d098284d3be02fb67686593eb826a2c8520c/src/__init__.py -------------------------------------------------------------------------------- /src/three_commas/__init__.py: -------------------------------------------------------------------------------- 1 | from . import api 2 | from . import site 3 | from . import cached_api 4 | from . import error 5 | from . import model 6 | from . import utils 7 | from . import streams 8 | -------------------------------------------------------------------------------- /src/three_commas/api/__init__.py: -------------------------------------------------------------------------------- 1 | from . import ver1, v2 2 | -------------------------------------------------------------------------------- /src/three_commas/api/v2/__init__.py: -------------------------------------------------------------------------------- 1 | from . import smart_trades 2 | -------------------------------------------------------------------------------- /src/three_commas/api/v2/smart_trades.py: -------------------------------------------------------------------------------- 1 | from py3cw.request import Py3CW 2 | from ...model import * 3 | from ...error import ThreeCommasApiError 4 | from typing import Tuple, List 5 | import logging 6 | from ...sys_utils import logged, with_py3cw, Py3cwClosure 7 | 8 | 9 | logger = logging.getLogger(__name__) 10 | wrapper: Py3cwClosure = None 11 | 12 | 13 | @logged 14 | @with_py3cw 15 | def get(payload: dict = None) -> Tuple[ThreeCommasApiError, List[SmartTradeV2Entity]]: 16 | """ 17 | GET /v2/smart_trades 18 | Get smart trade history (Permission: SMART_TRADE_READ, Security: SIGNED) 19 | 20 | """ 21 | error, data = wrapper.request( 22 | entity='smart_trades_v2', 23 | action='', 24 | payload=payload, 25 | ) 26 | return ThreeCommasApiError(error), SmartTradeV2Entity.of_list(data) 27 | 28 | 29 | @logged 30 | @with_py3cw 31 | def post(payload: dict = None) -> Tuple[ThreeCommasApiError, SmartTradeV2Entity]: 32 | """ 33 | POST /v2/smart_trades 34 | Create smart trade v2 (Permission: SMART_TRADE_WRITE, Security: SIGNED) 35 | 36 | """ 37 | error, data = wrapper.request( 38 | entity='smart_trades_v2', 39 | action='new', 40 | payload=payload, 41 | ) 42 | return ThreeCommasApiError(error), SmartTradeV2Entity(data) 43 | 44 | 45 | @logged 46 | @with_py3cw 47 | def get_by_id(id, payload: dict = None) -> Tuple[ThreeCommasApiError, SmartTradeV2Entity]: 48 | """ 49 | GET /v2/smart_trades/{id} 50 | Get smart trade v2 by id (Permission: SMART_TRADE_READ, Security: SIGNED) 51 | 52 | """ 53 | error, data = wrapper.request( 54 | entity='smart_trades_v2', 55 | action='get_by_id', 56 | action_id=str(id), 57 | payload=payload, 58 | ) 59 | return ThreeCommasApiError(error), SmartTradeV2Entity(data) 60 | 61 | 62 | @logged 63 | @with_py3cw 64 | def delete_by_id(id, payload: dict = None): 65 | """ 66 | DELETE /v2/smart_trades/{id} 67 | Cancel smart trade v2 (Permission: SMART_TRADE_WRITE, Security: SIGNED) 68 | 69 | """ 70 | error, data = wrapper.request( 71 | entity='smart_trades_v2', 72 | action='cancel', 73 | action_id=str(id), 74 | payload=payload, 75 | ) 76 | return ThreeCommasApiError(error), data 77 | 78 | 79 | @logged 80 | @with_py3cw 81 | def patch_by_id(id, payload: dict = None): 82 | """ 83 | PATCH /v2/smart_trades/{id} 84 | Update smart trade v2 (Permission: SMART_TRADE_WRITE, Security: SIGNED) 85 | 86 | """ 87 | error, data = wrapper.request( 88 | entity='smart_trades_v2', 89 | action='update', 90 | action_id=str(id), 91 | payload=payload, 92 | ) 93 | return ThreeCommasApiError(error), data 94 | 95 | 96 | @logged 97 | @with_py3cw 98 | def post_reduce_funds_by_id(id, payload: dict = None): 99 | """ 100 | POST /v2/smart_trades/{id}/reduce_funds 101 | Reduce funds for smart trade v2 (Permission: SMART_TRADE_WRITE, Security: SIGNED) 102 | 103 | """ 104 | error, data = wrapper.request( 105 | entity='smart_trades_v2', 106 | action='reduce_funds', 107 | action_id=str(id), 108 | payload=payload, 109 | ) 110 | return ThreeCommasApiError(error), data 111 | 112 | 113 | @logged 114 | @with_py3cw 115 | def post_add_funds_by_id(id, payload: dict = None): 116 | """ 117 | POST /v2/smart_trades/{id}/add_funds 118 | Average for smart trade v2 (Permission: SMART_TRADE_WRITE, Security: SIGNED) 119 | 120 | """ 121 | error, data = wrapper.request( 122 | entity='smart_trades_v2', 123 | action='add_funds', 124 | action_id=str(id), 125 | payload=payload, 126 | ) 127 | return ThreeCommasApiError(error), data 128 | 129 | 130 | @logged 131 | @with_py3cw 132 | def post_close_by_market_by_id(id, payload: dict = None): 133 | """ 134 | POST /v2/smart_trades/{id}/close_by_market 135 | Close by market smart trade v2 (Permission: SMART_TRADE_WRITE, Security: SIGNED) 136 | 137 | """ 138 | error, data = wrapper.request( 139 | entity='smart_trades_v2', 140 | action='close_by_market', 141 | action_id=str(id), 142 | payload=payload, 143 | ) 144 | return ThreeCommasApiError(error), data 145 | 146 | 147 | @logged 148 | @with_py3cw 149 | def post_force_start_by_id(id, payload: dict = None): 150 | """ 151 | POST /v2/smart_trades/{id}/force_start 152 | Force start smart trade v2 (Permission: SMART_TRADE_WRITE, Security: SIGNED) 153 | 154 | """ 155 | error, data = wrapper.request( 156 | entity='smart_trades_v2', 157 | action='force_start', 158 | action_id=str(id), 159 | payload=payload, 160 | ) 161 | return ThreeCommasApiError(error), data 162 | 163 | 164 | @logged 165 | @with_py3cw 166 | def post_force_process_by_id(id, payload: dict = None): 167 | """ 168 | POST /v2/smart_trades/{id}/force_process 169 | Process smart trade v2 (Permission: SMART_TRADE_WRITE, Security: SIGNED) 170 | 171 | """ 172 | error, data = wrapper.request( 173 | entity='smart_trades_v2', 174 | action='force_process', 175 | action_id=str(id), 176 | payload=payload, 177 | ) 178 | return ThreeCommasApiError(error), data 179 | 180 | 181 | @logged 182 | @with_py3cw 183 | def post_set_note_by_id(id, payload: dict = None): 184 | """ 185 | POST /v2/smart_trades/{id}/set_note 186 | Set note to smart trade v2 (Permission: SMART_TRADE_WRITE, Security: SIGNED) 187 | 188 | """ 189 | error, data = wrapper.request( 190 | entity='smart_trades_v2', 191 | action='set_note', 192 | action_id=str(id), 193 | payload=payload, 194 | ) 195 | return ThreeCommasApiError(error), data 196 | 197 | 198 | @logged 199 | @with_py3cw 200 | def get_trades_by_id(id, payload: dict = None): 201 | """ 202 | GET /v2/smart_trades/{smart_trade_id}/trades 203 | Get smart trade v2 trades (Permission: SMART_TRADE_READ, Security: SIGNED) 204 | 205 | """ 206 | error, data = wrapper.request( 207 | entity='smart_trades_v2', 208 | action='get_trades', 209 | action_id=str(id), 210 | payload=payload, 211 | ) 212 | return ThreeCommasApiError(error), data 213 | 214 | 215 | @logged 216 | @with_py3cw 217 | def post_trades_close_by_market_by_id(id, sub_id, payload: dict = None): 218 | """ 219 | POST /v2/smart_trades/{smart_trade_id}/trades/{id}/close_by_market 220 | Panic close trade by market (Permission: SMART_TRADE_WRITE, Security: SIGNED) 221 | 222 | """ 223 | error, data = wrapper.request( 224 | entity='smart_trades_v2', 225 | action='panic_close_by_market', 226 | action_id=str(id), 227 | action_sub_id=str(sub_id), 228 | payload=payload, 229 | ) 230 | return ThreeCommasApiError(error), data 231 | 232 | 233 | @logged 234 | @with_py3cw 235 | def delete_trades_by_id(id, sub_id, payload: dict = None): 236 | """ 237 | DELETE /v2/smart_trades/{smart_trade_id}/trades/{id} 238 | Cancel trade (Permission: SMART_TRADE_WRITE, Security: SIGNED) 239 | 240 | """ 241 | error, data = wrapper.request( 242 | entity='smart_trades_v2', 243 | action='cancel_trade', 244 | action_id=str(id), 245 | action_sub_id=str(sub_id), 246 | payload=payload, 247 | ) 248 | return ThreeCommasApiError(error), data 249 | 250 | 251 | -------------------------------------------------------------------------------- /src/three_commas/api/ver1/__init__.py: -------------------------------------------------------------------------------- 1 | from . import accounts, bots, deals, grid_bots, marketplace, users 2 | -------------------------------------------------------------------------------- /src/three_commas/api/ver1/accounts.py: -------------------------------------------------------------------------------- 1 | from py3cw.request import Py3CW 2 | from ...model import * 3 | from ...error import ThreeCommasApiError 4 | from typing import Tuple, List 5 | import logging 6 | from ...sys_utils import logged, with_py3cw, Py3cwClosure 7 | 8 | 9 | logger = logging.getLogger(__name__) 10 | wrapper: Py3cwClosure = None 11 | 12 | 13 | @logged 14 | @with_py3cw 15 | def post_transfer(payload: dict = None): 16 | """ 17 | POST /ver1/accounts/transfer 18 | Transfer coins between accounts (Permission: ACCOUNTS_WRITE, Security: SIGNED) 19 | 20 | """ 21 | error, data = wrapper.request( 22 | entity='accounts', 23 | action='transfer', 24 | payload=payload, 25 | ) 26 | return ThreeCommasApiError(error), data 27 | 28 | 29 | @logged 30 | @with_py3cw 31 | def get_transfer_history(payload: dict = None): 32 | """ 33 | GET /ver1/accounts/transfer_history 34 | Transfers history (Permission: ACCOUNTS_READ, Security: SIGNED) 35 | 36 | """ 37 | error, data = wrapper.request( 38 | entity='accounts', 39 | action='transfer_history', 40 | payload=payload, 41 | ) 42 | return ThreeCommasApiError(error), data 43 | 44 | 45 | @logged 46 | @with_py3cw 47 | def get_transfer_data(payload: dict = None): 48 | """ 49 | GET /ver1/accounts/transfer_data 50 | Data for transfer between accounts (Permission: ACCOUNTS_READ, Security: SIGNED) 51 | 52 | """ 53 | error, data = wrapper.request( 54 | entity='accounts', 55 | action='transfer_data', 56 | payload=payload, 57 | ) 58 | return ThreeCommasApiError(error), data 59 | 60 | 61 | @logged 62 | @with_py3cw 63 | def post_new(payload: dict = None) -> Tuple[ThreeCommasApiError, AccountEntity]: 64 | """ 65 | POST /ver1/accounts/new 66 | Add exchange account (Permission: ACCOUNTS_WRITE, Security: SIGNED) 67 | 68 | """ 69 | error, data = wrapper.request( 70 | entity='accounts', 71 | action='new', 72 | payload=payload, 73 | ) 74 | return ThreeCommasApiError(error), AccountEntity(data) 75 | 76 | 77 | @logged 78 | @with_py3cw 79 | def post_update(payload: dict = None): 80 | """ 81 | POST /ver1/accounts/update 82 | Edit exchange account 83 | 84 | """ 85 | error, data = wrapper.request( 86 | entity='accounts', 87 | action='update', 88 | payload=payload, 89 | ) 90 | return ThreeCommasApiError(error), data 91 | 92 | 93 | @logged 94 | @with_py3cw 95 | def get(payload: dict = None): 96 | """ 97 | GET /ver1/accounts 98 | User connected exchanges(and EthereumWallet) list (Permission: ACCOUNTS_READ, Security: SIGNED) 99 | 100 | """ 101 | error, data = wrapper.request( 102 | entity='accounts', 103 | action='', 104 | payload=payload, 105 | ) 106 | return ThreeCommasApiError(error), data 107 | 108 | 109 | @logged 110 | @with_py3cw 111 | def get_market_list(payload: dict = None): 112 | """ 113 | GET /ver1/accounts/market_list 114 | Supported markets list (Permission: NONE, Security: NONE) 115 | 116 | """ 117 | error, data = wrapper.request( 118 | entity='accounts', 119 | action='market_list', 120 | payload=payload, 121 | ) 122 | return ThreeCommasApiError(error), data 123 | 124 | 125 | @logged 126 | @with_py3cw 127 | def get_market_pairs(payload: dict = None) -> Tuple[ThreeCommasApiError, List[str]]: 128 | """ 129 | GET /ver1/accounts/market_pairs 130 | All market pairs (Permission: NONE, Security: NONE) 131 | 132 | """ 133 | error, data = wrapper.request( 134 | entity='accounts', 135 | action='market_pairs', 136 | payload=payload, 137 | ) 138 | return ThreeCommasApiError(error), data 139 | 140 | 141 | ''' This endpoint was not present in the py3cw module 142 | @logged 143 | @with_py3cw 144 | def get_currency_rates_with_leverage_data(payload: dict = None): 145 | """ 146 | GET /ver1/accounts/currency_rates_with_leverage_data 147 | Currency rates and limits with leverage data (Permission: NONE, Security: NONE) 148 | 149 | """ 150 | error, data = wrapper.request( 151 | entity='accounts', 152 | action='', 153 | payload=payload, 154 | ) 155 | return ThreeCommasApiError(error), data 156 | ''' 157 | 158 | 159 | @logged 160 | @with_py3cw 161 | def get_currency_rates(payload: dict = None): 162 | """ 163 | GET /ver1/accounts/currency_rates 164 | Currency rates and limits (Permission: NONE, Security: NONE) 165 | 166 | """ 167 | error, data = wrapper.request( 168 | entity='accounts', 169 | action='currency_rates', 170 | payload=payload, 171 | ) 172 | return ThreeCommasApiError(error), data 173 | 174 | 175 | ''' This endpoint was not present in the py3cw module 176 | @logged 177 | @with_py3cw 178 | def get_deposit_data_by_id(id, payload: dict = None): 179 | """ 180 | GET /ver1/accounts/{account_id}/deposit_data 181 | User Deposit Data (Permission: ACCOUNTS_READ, Security: SIGNED) 182 | 183 | """ 184 | error, data = wrapper.request( 185 | entity='accounts', 186 | action='', 187 | action_id=str(id), 188 | payload=payload, 189 | ) 190 | return ThreeCommasApiError(error), data 191 | ''' 192 | 193 | 194 | @logged 195 | @with_py3cw 196 | def get_networks_info_by_id(id, payload: dict = None): 197 | """ 198 | GET /ver1/accounts/{account_id}/networks_info 199 | Deposit/withdraw networks info (Permission: ACCOUNTS_READ, Security: SIGNED) 200 | 201 | """ 202 | error, data = wrapper.request( 203 | entity='accounts', 204 | action='networks_info', 205 | action_id=str(id), 206 | payload=payload, 207 | ) 208 | return ThreeCommasApiError(error), data 209 | 210 | 211 | @logged 212 | @with_py3cw 213 | def post_convert_dust_to_bnb_by_id(id, payload: dict = None): 214 | """ 215 | POST /ver1/accounts/{account_id}/convert_dust_to_bnb 216 | Convert dust coins to BNB (Permission: ACCOUNTS_WRITE, Security: SIGNED) 217 | 218 | """ 219 | error, data = wrapper.request( 220 | entity='accounts', 221 | action='convert_dust_to_bnb', 222 | action_id=str(id), 223 | payload=payload, 224 | ) 225 | return ThreeCommasApiError(error), data 226 | 227 | 228 | @logged 229 | @with_py3cw 230 | def get_active_trading_entities_by_id(id, payload: dict = None): 231 | """ 232 | GET /ver1/accounts/{account_id}/active_trading_entities 233 | Active trade entities (Permission: ACCOUNTS_READ, Security: SIGNED) 234 | 235 | """ 236 | error, data = wrapper.request( 237 | entity='accounts', 238 | action='active_trading_entities', 239 | action_id=str(id), 240 | payload=payload, 241 | ) 242 | return ThreeCommasApiError(error), data 243 | 244 | 245 | @logged 246 | @with_py3cw 247 | def post_sell_all_to_usd_by_id(id, payload: dict = None): 248 | """ 249 | POST /ver1/accounts/{account_id}/sell_all_to_usd 250 | Sell all to USD (Permission: ACCOUNTS_WRITE, Security: SIGNED) 251 | 252 | """ 253 | error, data = wrapper.request( 254 | entity='accounts', 255 | action='sell_all_to_usd', 256 | action_id=str(id), 257 | payload=payload, 258 | ) 259 | return ThreeCommasApiError(error), data 260 | 261 | 262 | @logged 263 | @with_py3cw 264 | def post_sell_all_to_btc_by_id(id, payload: dict = None): 265 | """ 266 | POST /ver1/accounts/{account_id}/sell_all_to_btc 267 | Sell all to BTC (Permission: ACCOUNTS_WRITE, Security: SIGNED) 268 | 269 | """ 270 | error, data = wrapper.request( 271 | entity='accounts', 272 | action='sell_all_to_btc', 273 | action_id=str(id), 274 | payload=payload, 275 | ) 276 | return ThreeCommasApiError(error), data 277 | 278 | 279 | @logged 280 | @with_py3cw 281 | def get_balance_chart_data_by_id(id, payload: dict = None): 282 | """ 283 | GET /ver1/accounts/{account_id}/balance_chart_data 284 | balance history data (Permission: ACCOUNTS_READ, Security: SIGNED) 285 | 286 | """ 287 | error, data = wrapper.request( 288 | entity='accounts', 289 | action='balance_chart_data', 290 | action_id=str(id), 291 | payload=payload, 292 | ) 293 | return ThreeCommasApiError(error), data 294 | 295 | 296 | @logged 297 | @with_py3cw 298 | def post_load_balances_by_id(id, payload: dict = None): 299 | """ 300 | POST /ver1/accounts/{account_id}/load_balances 301 | Load balances for specified exchange (Permission: ACCOUNTS_READ, Security: SIGNED) 302 | 303 | """ 304 | error, data = wrapper.request( 305 | entity='accounts', 306 | action='load_balances', 307 | action_id=str(id), 308 | payload=payload, 309 | ) 310 | return ThreeCommasApiError(error), data 311 | 312 | 313 | @logged 314 | @with_py3cw 315 | def post_rename_by_id(id, payload: dict = None): 316 | """ 317 | POST /ver1/accounts/{account_id}/rename 318 | Rename exchange connection (Permission: ACCOUNTS_WRITE, Security: SIGNED) 319 | 320 | """ 321 | error, data = wrapper.request( 322 | entity='accounts', 323 | action='rename', 324 | action_id=str(id), 325 | payload=payload, 326 | ) 327 | return ThreeCommasApiError(error), data 328 | 329 | 330 | @logged 331 | @with_py3cw 332 | def post_pie_chart_data_by_id(id, payload: dict = None): 333 | """ 334 | POST /ver1/accounts/{account_id}/pie_chart_data 335 | Information about all user balances on specified exchange in pretty for pie chart format (Permission: ACCOUNTS_READ, Security: SIGNED) 336 | 337 | """ 338 | error, data = wrapper.request( 339 | entity='accounts', 340 | action='pie_chart_data', 341 | action_id=str(id), 342 | payload=payload, 343 | ) 344 | return ThreeCommasApiError(error), data 345 | 346 | 347 | @logged 348 | @with_py3cw 349 | def post_account_table_data_by_id(id, payload: dict = None): 350 | """ 351 | POST /ver1/accounts/{account_id}/account_table_data 352 | Information about all user balances on specified exchange (Permission: ACCOUNTS_READ, Security: SIGNED) 353 | 354 | """ 355 | error, data = wrapper.request( 356 | entity='accounts', 357 | action='account_table_data', 358 | action_id=str(id), 359 | payload=payload, 360 | ) 361 | return ThreeCommasApiError(error), data 362 | 363 | 364 | @logged 365 | @with_py3cw 366 | def post_remove_by_id(id, payload: dict = None): 367 | """ 368 | POST /ver1/accounts/{account_id}/remove 369 | Remove exchange connection (Permission: ACCOUNTS_WRITE, Security: SIGNED) 370 | 371 | """ 372 | error, data = wrapper.request( 373 | entity='accounts', 374 | action='remove', 375 | action_id=str(id), 376 | payload=payload, 377 | ) 378 | return ThreeCommasApiError(error), data 379 | 380 | 381 | ''' This endpoint was not present in the py3cw module 382 | @logged 383 | @with_py3cw 384 | def get_leverage_data_by_id(id, payload: dict = None): 385 | """ 386 | GET /ver1/accounts/{account_id}/leverage_data 387 | Information about account leverage (Permission: ACCOUNTS_READ, Security: SIGNED) 388 | 389 | """ 390 | error, data = wrapper.request( 391 | entity='accounts', 392 | action='', 393 | action_id=str(id), 394 | payload=payload, 395 | ) 396 | return ThreeCommasApiError(error), data 397 | ''' 398 | 399 | 400 | @logged 401 | @with_py3cw 402 | def get_by_id(id, payload: dict = None) -> Tuple[ThreeCommasApiError, AccountEntity]: 403 | """ 404 | GET /ver1/accounts/{account_id} 405 | Single Account Info (Permission: ACCOUNTS_READ, Security: SIGNED) 406 | You can send 'summary' instead of {account_id} to get summary account info 407 | 408 | """ 409 | error, data = wrapper.request( 410 | entity='accounts', 411 | action='account_info', 412 | action_id=str(id), 413 | payload=payload, 414 | ) 415 | return ThreeCommasApiError(error), AccountEntity(data) 416 | 417 | 418 | -------------------------------------------------------------------------------- /src/three_commas/api/ver1/bots.py: -------------------------------------------------------------------------------- 1 | from py3cw.request import Py3CW 2 | from ...model import * 3 | from ...error import ThreeCommasApiError 4 | from typing import Tuple, List 5 | import logging 6 | from ...sys_utils import logged, with_py3cw, Py3cwClosure 7 | 8 | 9 | logger = logging.getLogger(__name__) 10 | wrapper: Py3cwClosure = None 11 | 12 | 13 | @logged 14 | @with_py3cw 15 | def get_strategy_list(payload: dict = None): 16 | """ 17 | GET /ver1/bots/strategy_list 18 | Available strategy list for bot (Permission: BOTS_READ, Security: SIGNED) 19 | 20 | """ 21 | error, data = wrapper.request( 22 | entity='bots', 23 | action='strategy_list', 24 | payload=payload, 25 | ) 26 | return ThreeCommasApiError(error), data 27 | 28 | 29 | @logged 30 | @with_py3cw 31 | def get_pairs_black_list(payload: dict = None): 32 | """ 33 | GET /ver1/bots/pairs_black_list 34 | Black List for bot pairs (Permission: BOTS_READ, Security: SIGNED) 35 | 36 | """ 37 | error, data = wrapper.request( 38 | entity='bots', 39 | action='pairs_black_list', 40 | payload=payload, 41 | ) 42 | return ThreeCommasApiError(error), data 43 | 44 | 45 | @logged 46 | @with_py3cw 47 | def post_update_pairs_black_list(payload: dict = None): 48 | """ 49 | POST /ver1/bots/update_pairs_black_list 50 | Create or Update pairs BlackList for bots (Permission: BOTS_WRITE, Security: SIGNED) 51 | 52 | """ 53 | error, data = wrapper.request( 54 | entity='bots', 55 | action='update_pairs_black_list', 56 | payload=payload, 57 | ) 58 | return ThreeCommasApiError(error), data 59 | 60 | 61 | @logged 62 | @with_py3cw 63 | def post_create_bot(payload: dict = None): 64 | """ 65 | POST /ver1/bots/create_bot 66 | Create bot (Permission: BOTS_WRITE, Security: SIGNED) 67 | 68 | """ 69 | error, data = wrapper.request( 70 | entity='bots', 71 | action='create_bot', 72 | payload=payload, 73 | ) 74 | return ThreeCommasApiError(error), data 75 | 76 | 77 | @logged 78 | @with_py3cw 79 | def get(payload: dict = None) -> Tuple[ThreeCommasApiError, List[BotEntity]]: 80 | """ 81 | GET /ver1/bots 82 | User bots (Permission: BOTS_READ, Security: SIGNED) 83 | 84 | """ 85 | error, data = wrapper.request( 86 | entity='bots', 87 | action='', 88 | payload=payload, 89 | ) 90 | return ThreeCommasApiError(error), BotEntity.of_list(data) 91 | 92 | 93 | @logged 94 | @with_py3cw 95 | def get_stats(payload: dict = None): 96 | """ 97 | GET /ver1/bots/stats 98 | Get bot stats (Permission: BOTS_READ, Security: SIGNED) 99 | 100 | """ 101 | error, data = wrapper.request( 102 | entity='bots', 103 | action='stats', 104 | payload=payload, 105 | ) 106 | return ThreeCommasApiError(error), data 107 | 108 | 109 | @logged 110 | @with_py3cw 111 | def post_copy_and_create_by_id(id, payload: dict = None): 112 | """ 113 | POST /ver1/bots/{bot_id}/copy_and_create 114 | POST /bots/:id/copy_and_create. Permission: BOTS_WRITE, Security: SIGNED 115 | 116 | """ 117 | error, data = wrapper.request( 118 | entity='bots', 119 | action='copy_and_create', 120 | action_id=str(id), 121 | payload=payload, 122 | ) 123 | return ThreeCommasApiError(error), data 124 | 125 | 126 | @logged 127 | @with_py3cw 128 | def patch_update_by_id(id, payload: dict = None): 129 | """ 130 | PATCH /ver1/bots/{bot_id}/update 131 | Edit bot (Permission: BOTS_WRITE, Security: SIGNED) 132 | 133 | """ 134 | error, data = wrapper.request( 135 | entity='bots', 136 | action='update', 137 | action_id=str(id), 138 | payload=payload, 139 | ) 140 | return ThreeCommasApiError(error), data 141 | 142 | 143 | @logged 144 | @with_py3cw 145 | def post_disable_by_id(id, payload: dict = None): 146 | """ 147 | POST /ver1/bots/{bot_id}/disable 148 | Disable bot (Permission: BOTS_WRITE, Security: SIGNED) 149 | 150 | """ 151 | error, data = wrapper.request( 152 | entity='bots', 153 | action='disable', 154 | action_id=str(id), 155 | payload=payload, 156 | ) 157 | return ThreeCommasApiError(error), data 158 | 159 | 160 | @logged 161 | @with_py3cw 162 | def post_enable_by_id(id, payload: dict = None): 163 | """ 164 | POST /ver1/bots/{bot_id}/enable 165 | Enable bot (Permission: BOTS_WRITE, Security: SIGNED) 166 | 167 | """ 168 | error, data = wrapper.request( 169 | entity='bots', 170 | action='enable', 171 | action_id=str(id), 172 | payload=payload, 173 | ) 174 | return ThreeCommasApiError(error), data 175 | 176 | 177 | @logged 178 | @with_py3cw 179 | def post_start_new_deal_by_id(id, payload: dict = None): 180 | """ 181 | POST /ver1/bots/{bot_id}/start_new_deal 182 | Start new deal asap (Permission: BOTS_WRITE, Security: SIGNED) 183 | 184 | """ 185 | error, data = wrapper.request( 186 | entity='bots', 187 | action='start_new_deal', 188 | action_id=str(id), 189 | payload=payload, 190 | ) 191 | return ThreeCommasApiError(error), data 192 | 193 | 194 | @logged 195 | @with_py3cw 196 | def post_delete_by_id(id, payload: dict = None): 197 | """ 198 | POST /ver1/bots/{bot_id}/delete 199 | Delete bot (Permission: BOTS_WRITE, Security: SIGNED) 200 | 201 | """ 202 | error, data = wrapper.request( 203 | entity='bots', 204 | action='delete', 205 | action_id=str(id), 206 | payload=payload, 207 | ) 208 | return ThreeCommasApiError(error), data 209 | 210 | 211 | @logged 212 | @with_py3cw 213 | def post_panic_sell_all_deals_by_id(id, payload: dict = None): 214 | """ 215 | POST /ver1/bots/{bot_id}/panic_sell_all_deals 216 | Panic sell all bot deals (Permission: BOTS_WRITE, Security: SIGNED) 217 | 218 | """ 219 | error, data = wrapper.request( 220 | entity='bots', 221 | action='panic_sell_all_deals', 222 | action_id=str(id), 223 | payload=payload, 224 | ) 225 | return ThreeCommasApiError(error), data 226 | 227 | 228 | @logged 229 | @with_py3cw 230 | def post_cancel_all_deals_by_id(id, payload: dict = None): 231 | """ 232 | POST /ver1/bots/{bot_id}/cancel_all_deals 233 | Cancel all bot deals (Permission: BOTS_WRITE, Security: SIGNED) 234 | 235 | """ 236 | error, data = wrapper.request( 237 | entity='bots', 238 | action='cancel_all_deals', 239 | action_id=str(id), 240 | payload=payload, 241 | ) 242 | return ThreeCommasApiError(error), data 243 | 244 | 245 | @logged 246 | @with_py3cw 247 | def get_deals_stats_by_id(id, payload: dict = None): 248 | """ 249 | GET /ver1/bots/{bot_id}/deals_stats 250 | Bot deals stats (Permission: BOTS_READ, Security: SIGNED) 251 | 252 | """ 253 | error, data = wrapper.request( 254 | entity='bots', 255 | action='deals_stats', 256 | action_id=str(id), 257 | payload=payload, 258 | ) 259 | return ThreeCommasApiError(error), data 260 | 261 | 262 | @logged 263 | @with_py3cw 264 | def get_show_by_id(id, payload: dict = None) -> Tuple[ThreeCommasApiError, BotEntity]: 265 | """ 266 | GET /ver1/bots/{bot_id}/show 267 | Bot info (Permission: BOTS_READ, Security: SIGNED) 268 | 269 | """ 270 | error, data = wrapper.request( 271 | entity='bots', 272 | action='show', 273 | action_id=str(id), 274 | payload=payload, 275 | ) 276 | return ThreeCommasApiError(error), BotEntity(data) 277 | 278 | 279 | -------------------------------------------------------------------------------- /src/three_commas/api/ver1/deals.py: -------------------------------------------------------------------------------- 1 | from py3cw.request import Py3CW 2 | from ...model import * 3 | from ...error import ThreeCommasApiError 4 | from typing import Tuple, List 5 | import logging 6 | from ...sys_utils import logged, with_py3cw, Py3cwClosure 7 | 8 | 9 | logger = logging.getLogger(__name__) 10 | wrapper: Py3cwClosure = None 11 | 12 | 13 | @logged 14 | @with_py3cw 15 | def get(payload: dict = None): 16 | """ 17 | GET /ver1/deals 18 | User deals (Permission: BOTS_READ, Security: SIGNED) 19 | 20 | """ 21 | error, data = wrapper.request( 22 | entity='deals', 23 | action='', 24 | payload=payload, 25 | ) 26 | return ThreeCommasApiError(error), data 27 | 28 | 29 | @logged 30 | @with_py3cw 31 | def post_convert_to_smart_trade_by_id(id, payload: dict = None): 32 | """ 33 | POST /ver1/deals/{deal_id}/convert_to_smart_trade 34 | Convert to smart trade (Permission: SMART_TRADE_WRITE, Security: SIGNED) 35 | 36 | """ 37 | error, data = wrapper.request( 38 | entity='deals', 39 | action='convert_to_smart_trade', 40 | action_id=str(id), 41 | payload=payload, 42 | ) 43 | return ThreeCommasApiError(error), data 44 | 45 | 46 | @logged 47 | @with_py3cw 48 | def post_update_max_safety_orders_by_id(id, payload: dict = None): 49 | """ 50 | POST /ver1/deals/{deal_id}/update_max_safety_orders 51 | Update max safety orders (Permission: BOTS_WRITE, Security: SIGNED) 52 | 53 | """ 54 | error, data = wrapper.request( 55 | entity='deals', 56 | action='update_max_safety_orders', 57 | action_id=str(id), 58 | payload=payload, 59 | ) 60 | return ThreeCommasApiError(error), data 61 | 62 | 63 | @logged 64 | @with_py3cw 65 | def post_panic_sell_by_id(id, payload: dict = None): 66 | """ 67 | POST /ver1/deals/{deal_id}/panic_sell 68 | Panic sell deal (Permission: BOTS_WRITE, Security: SIGNED) 69 | 70 | """ 71 | error, data = wrapper.request( 72 | entity='deals', 73 | action='panic_sell', 74 | action_id=str(id), 75 | payload=payload, 76 | ) 77 | return ThreeCommasApiError(error), data 78 | 79 | 80 | @logged 81 | @with_py3cw 82 | def post_cancel_by_id(id, payload: dict = None): 83 | """ 84 | POST /ver1/deals/{deal_id}/cancel 85 | Cancel deal (Permission: BOTS_WRITE, Security: SIGNED) 86 | 87 | """ 88 | error, data = wrapper.request( 89 | entity='deals', 90 | action='cancel', 91 | action_id=str(id), 92 | payload=payload, 93 | ) 94 | return ThreeCommasApiError(error), data 95 | 96 | 97 | @logged 98 | @with_py3cw 99 | def patch_update_deal_by_id(id, payload: dict = None): 100 | """ 101 | PATCH /ver1/deals/{deal_id}/update_deal 102 | Update deal (Permission: BOTS_WRITE, Security: SIGNED) 103 | 104 | """ 105 | error, data = wrapper.request( 106 | entity='deals', 107 | action='update_deal', 108 | action_id=str(id), 109 | payload=payload, 110 | ) 111 | return ThreeCommasApiError(error), data 112 | 113 | 114 | @logged 115 | @with_py3cw 116 | def post_update_tp_by_id(id, payload: dict = None): 117 | """ 118 | POST /ver1/deals/{deal_id}/update_tp 119 | DEPRECATED, Update take profit condition. Deal status should be bought (Permission: BOTS_WRITE, Security: SIGNED) 120 | 121 | """ 122 | error, data = wrapper.request( 123 | entity='deals', 124 | action='update_tp', 125 | action_id=str(id), 126 | payload=payload, 127 | ) 128 | return ThreeCommasApiError(error), data 129 | 130 | 131 | @logged 132 | @with_py3cw 133 | def get_show_by_id(id, payload: dict = None): 134 | """ 135 | GET /ver1/deals/{deal_id}/show 136 | Info about specific deal (Permission: BOTS_READ, Security: SIGNED) 137 | 138 | """ 139 | error, data = wrapper.request( 140 | entity='deals', 141 | action='show', 142 | action_id=str(id), 143 | payload=payload, 144 | ) 145 | return ThreeCommasApiError(error), data 146 | 147 | 148 | @logged 149 | @with_py3cw 150 | def post_cancel_order_by_id(id, payload: dict = None): 151 | """ 152 | POST /ver1/deals/{deal_id}/cancel_order 153 | Cancel manual safety orders (Permission: BOTS_WRITE, Security: SIGNED) 154 | 155 | """ 156 | error, data = wrapper.request( 157 | entity='deals', 158 | action='cancel_order', 159 | action_id=str(id), 160 | payload=payload, 161 | ) 162 | return ThreeCommasApiError(error), data 163 | 164 | 165 | @logged 166 | @with_py3cw 167 | def get_market_orders_by_id(id, payload: dict = None): 168 | """ 169 | GET /ver1/deals/{deal_id}/market_orders 170 | Deal safety orders (Permission: BOTS_READ, Security: SIGNED) 171 | 172 | """ 173 | error, data = wrapper.request( 174 | entity='deals', 175 | action='market_orders', 176 | action_id=str(id), 177 | payload=payload, 178 | ) 179 | return ThreeCommasApiError(error), data 180 | 181 | 182 | @logged 183 | @with_py3cw 184 | def post_add_funds_by_id(id, payload: dict = None): 185 | """ 186 | POST /ver1/deals/{deal_id}/add_funds 187 | Adding manual safety order (Permission: BOTS_WRITE, Security: SIGNED) 188 | 189 | """ 190 | error, data = wrapper.request( 191 | entity='deals', 192 | action='add_funds', 193 | action_id=str(id), 194 | payload=payload, 195 | ) 196 | return ThreeCommasApiError(error), data 197 | 198 | 199 | @logged 200 | @with_py3cw 201 | def get_data_for_adding_funds_by_id(id, payload: dict = None): 202 | """ 203 | GET /ver1/deals/{deal_id}/data_for_adding_funds 204 | Info required to add funds correctly: available amounts, exchange limitations etc (Permission: BOTS_READ, Security: SIGNED) 205 | 206 | """ 207 | error, data = wrapper.request( 208 | entity='deals', 209 | action='data_for_adding_funds', 210 | action_id=str(id), 211 | payload=payload, 212 | ) 213 | return ThreeCommasApiError(error), data 214 | 215 | 216 | -------------------------------------------------------------------------------- /src/three_commas/api/ver1/grid_bots.py: -------------------------------------------------------------------------------- 1 | from py3cw.request import Py3CW 2 | from ...model import * 3 | from ...error import ThreeCommasApiError 4 | from typing import Tuple, List 5 | import logging 6 | from ...sys_utils import logged, with_py3cw, Py3cwClosure 7 | 8 | 9 | logger = logging.getLogger(__name__) 10 | wrapper: Py3cwClosure = None 11 | 12 | 13 | @logged 14 | @with_py3cw 15 | def post_ai(payload: dict = None): 16 | """ 17 | POST /ver1/grid_bots/ai 18 | Create AI Grid Bot (Permission: BOTS_WRITE, Security: SIGNED) 19 | 20 | """ 21 | error, data = wrapper.request( 22 | entity='grid_bots', 23 | action='ai', 24 | payload=payload, 25 | ) 26 | return ThreeCommasApiError(error), data 27 | 28 | 29 | @logged 30 | @with_py3cw 31 | def post_manual(payload: dict = None): 32 | """ 33 | POST /ver1/grid_bots/manual 34 | Create Grid Bot (Permission: BOTS_WRITE, Security: SIGNED) 35 | 36 | """ 37 | error, data = wrapper.request( 38 | entity='grid_bots', 39 | action='manual', 40 | payload=payload, 41 | ) 42 | return ThreeCommasApiError(error), data 43 | 44 | 45 | @logged 46 | @with_py3cw 47 | def get_ai_settings(payload: dict = None): 48 | """ 49 | GET /ver1/grid_bots/ai_settings 50 | Get AI settings (Permission: BOTS_READ, Security: SIGNED) 51 | 52 | """ 53 | error, data = wrapper.request( 54 | entity='grid_bots', 55 | action='ai_settings', 56 | payload=payload, 57 | ) 58 | return ThreeCommasApiError(error), data 59 | 60 | 61 | @logged 62 | @with_py3cw 63 | def get(payload: dict = None): 64 | """ 65 | GET /ver1/grid_bots 66 | Grid bots list (Permission: BOTS_READ, Security: SIGNED) 67 | 68 | """ 69 | error, data = wrapper.request( 70 | entity='grid_bots', 71 | action='', 72 | payload=payload, 73 | ) 74 | return ThreeCommasApiError(error), data 75 | 76 | 77 | @logged 78 | @with_py3cw 79 | def get_market_orders_by_id(id, payload: dict = None): 80 | """ 81 | GET /ver1/grid_bots/{id}/market_orders 82 | Grid Bot Market Orders (Permission: BOTS_READ, Security: SIGNED) 83 | 84 | """ 85 | error, data = wrapper.request( 86 | entity='grid_bots', 87 | action='market_orders', 88 | action_id=str(id), 89 | payload=payload, 90 | ) 91 | return ThreeCommasApiError(error), data 92 | 93 | 94 | @logged 95 | @with_py3cw 96 | def get_profits_by_id(id, payload: dict = None): 97 | """ 98 | GET /ver1/grid_bots/{id}/profits 99 | Grid Bot Profits (Permission: BOTS_READ, Security: SIGNED) 100 | 101 | """ 102 | error, data = wrapper.request( 103 | entity='grid_bots', 104 | action='profits', 105 | action_id=str(id), 106 | payload=payload, 107 | ) 108 | return ThreeCommasApiError(error), data 109 | 110 | 111 | @logged 112 | @with_py3cw 113 | def patch_ai_by_id(id, payload: dict = None): 114 | """ 115 | PATCH /ver1/grid_bots/{id}/ai 116 | Edit Grid Bot (AI) (Permission: BOTS_WRITE, Security: SIGNED) 117 | 118 | """ 119 | error, data = wrapper.request( 120 | entity='grid_bots', 121 | action='ai_update', 122 | action_id=str(id), 123 | payload=payload, 124 | ) 125 | return ThreeCommasApiError(error), data 126 | 127 | 128 | @logged 129 | @with_py3cw 130 | def patch_manual_by_id(id, payload: dict = None): 131 | """ 132 | PATCH /ver1/grid_bots/{id}/manual 133 | Edit Grid Bot (Manual) (Permission: BOTS_WRITE, Security: SIGNED) 134 | 135 | """ 136 | error, data = wrapper.request( 137 | entity='grid_bots', 138 | action='manual_update', 139 | action_id=str(id), 140 | payload=payload, 141 | ) 142 | return ThreeCommasApiError(error), data 143 | 144 | 145 | @logged 146 | @with_py3cw 147 | def get_by_id(id, payload: dict = None): 148 | """ 149 | GET /ver1/grid_bots/{id} 150 | Show Grid Bot (Permission: BOTS_READ, Security: SIGNED) 151 | 152 | """ 153 | error, data = wrapper.request( 154 | entity='grid_bots', 155 | action='get', 156 | action_id=str(id), 157 | payload=payload, 158 | ) 159 | return ThreeCommasApiError(error), data 160 | 161 | 162 | @logged 163 | @with_py3cw 164 | def delete_by_id(id, payload: dict = None): 165 | """ 166 | DELETE /ver1/grid_bots/{id} 167 | Delete Grid Bot (Permission: BOTS_WRITE, Security: SIGNED) 168 | 169 | """ 170 | error, data = wrapper.request( 171 | entity='grid_bots', 172 | action='delete', 173 | action_id=str(id), 174 | payload=payload, 175 | ) 176 | return ThreeCommasApiError(error), data 177 | 178 | 179 | @logged 180 | @with_py3cw 181 | def post_disable_by_id(id, payload: dict = None): 182 | """ 183 | POST /ver1/grid_bots/{id}/disable 184 | Disable Grid Bot (Permission: BOTS_WRITE, Security: SIGNED) 185 | 186 | """ 187 | error, data = wrapper.request( 188 | entity='grid_bots', 189 | action='disable', 190 | action_id=str(id), 191 | payload=payload, 192 | ) 193 | return ThreeCommasApiError(error), data 194 | 195 | 196 | @logged 197 | @with_py3cw 198 | def post_enable_by_id(id, payload: dict = None): 199 | """ 200 | POST /ver1/grid_bots/{id}/enable 201 | Enable Grid Bot (Permission: BOTS_WRITE, Security: SIGNED) 202 | 203 | """ 204 | error, data = wrapper.request( 205 | entity='grid_bots', 206 | action='enable', 207 | action_id=str(id), 208 | payload=payload, 209 | ) 210 | return ThreeCommasApiError(error), data 211 | 212 | 213 | @logged 214 | @with_py3cw 215 | def get_required_balances_by_id(id, payload: dict = None): 216 | """ 217 | GET /ver1/grid_bots/{id}/required_balances 218 | Get required balances to start bot(Permission: BOTS_READ, Security: SIGNED) 219 | 220 | """ 221 | error, data = wrapper.request( 222 | entity='grid_bots', 223 | action='required_balances', 224 | action_id=str(id), 225 | payload=payload, 226 | ) 227 | return ThreeCommasApiError(error), data 228 | 229 | 230 | -------------------------------------------------------------------------------- /src/three_commas/api/ver1/loose_accounts.py: -------------------------------------------------------------------------------- 1 | from py3cw.request import Py3CW 2 | from ...model import * 3 | from ...error import ThreeCommasApiError 4 | from typing import Tuple, List 5 | import logging 6 | from ...sys_utils import logged, with_py3cw, Py3cwClosure 7 | 8 | 9 | logger = logging.getLogger(__name__) 10 | wrapper: Py3cwClosure = None 11 | 12 | 13 | ''' This endpoint was not present in the py3cw module 14 | @logged 15 | @with_py3cw 16 | def post(payload: dict = None): 17 | """ 18 | POST /ver1/loose_accounts 19 | Create Loose Account (Permission: ACCOUNTS_WRITE, Security: SIGNED) 20 | 21 | """ 22 | error, data = wrapper.request( 23 | entity='', 24 | action='', 25 | payload=payload, 26 | ) 27 | return ThreeCommasApiError(error), data 28 | ''' 29 | 30 | 31 | ''' This endpoint was not present in the py3cw module 32 | @logged 33 | @with_py3cw 34 | def get_available_currencies(payload: dict = None): 35 | """ 36 | GET /ver1/loose_accounts/available_currencies 37 | Available currencies (Permission: ACCOUNTS_READ, Security: SIGNED) 38 | 39 | """ 40 | error, data = wrapper.request( 41 | entity='', 42 | action='', 43 | payload=payload, 44 | ) 45 | return ThreeCommasApiError(error), data 46 | ''' 47 | 48 | 49 | ''' This endpoint was not present in the py3cw module 50 | @logged 51 | @with_py3cw 52 | def put_by_id(id, payload: dict = None): 53 | """ 54 | PUT /ver1/loose_accounts/{account_id} 55 | Update Loose Account (Permission: ACCOUNTS_WRITE, Security: SIGNED) 56 | 57 | """ 58 | error, data = wrapper.request( 59 | entity='', 60 | action='', 61 | action_id=str(id), 62 | payload=payload, 63 | ) 64 | return ThreeCommasApiError(error), data 65 | ''' 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/three_commas/api/ver1/marketplace.py: -------------------------------------------------------------------------------- 1 | from py3cw.request import Py3CW 2 | from ...model import * 3 | from ...error import ThreeCommasApiError 4 | from typing import Tuple, List 5 | import logging 6 | from ...sys_utils import logged, with_py3cw, Py3cwClosure 7 | 8 | 9 | logger = logging.getLogger(__name__) 10 | wrapper: Py3cwClosure = None 11 | 12 | 13 | @logged 14 | @with_py3cw 15 | def get_presets(payload: dict = None): 16 | """ 17 | GET /ver1/marketplace/presets 18 | Marketplace presets (Permission: NONE, Security: SIGNED) 19 | 20 | """ 21 | error, data = wrapper.request( 22 | entity='marketplace', 23 | action='presets', 24 | payload=payload, 25 | ) 26 | return ThreeCommasApiError(error), data 27 | 28 | 29 | @logged 30 | @with_py3cw 31 | def get_items(payload: dict = None): 32 | """ 33 | GET /ver1/marketplace/items 34 | All marketplace items (Permission: NONE, Security: NONE) 35 | 36 | """ 37 | error, data = wrapper.request( 38 | entity='marketplace', 39 | action='items', 40 | payload=payload, 41 | ) 42 | return ThreeCommasApiError(error), data 43 | 44 | 45 | @logged 46 | @with_py3cw 47 | def get_signals_by_id(id, payload: dict = None): 48 | """ 49 | GET /ver1/marketplace/{item_id}/signals 50 | Marketplace Item Signals (Permission: NONE, Security: NONE) 51 | 52 | """ 53 | error, data = wrapper.request( 54 | entity='marketplace', 55 | action='signals', 56 | action_id=str(id), 57 | payload=payload, 58 | ) 59 | return ThreeCommasApiError(error), data 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/three_commas/api/ver1/ping.py: -------------------------------------------------------------------------------- 1 | from py3cw.request import Py3CW 2 | from ...model import * 3 | from ...error import ThreeCommasApiError 4 | from typing import Tuple, List 5 | import logging 6 | from ...sys_utils import logged, with_py3cw, Py3cwClosure 7 | 8 | 9 | logger = logging.getLogger(__name__) 10 | wrapper: Py3cwClosure = None 11 | 12 | 13 | ''' This endpoint was not present in the py3cw module 14 | @logged 15 | @with_py3cw 16 | def get(payload: dict = None): 17 | """ 18 | GET /ver1/ping 19 | Test connectivity to the Rest API (Permission: NONE, Security: NONE) 20 | 21 | """ 22 | error, data = wrapper.request( 23 | entity='', 24 | action='', 25 | payload=payload, 26 | ) 27 | return ThreeCommasApiError(error), data 28 | ''' 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/three_commas/api/ver1/smart_trades.py: -------------------------------------------------------------------------------- 1 | from py3cw.request import Py3CW 2 | from ...model import * 3 | from ...error import ThreeCommasApiError 4 | from typing import Tuple, List 5 | import logging 6 | from ...sys_utils import logged, with_py3cw, Py3cwClosure 7 | 8 | 9 | logger = logging.getLogger(__name__) 10 | wrapper: Py3cwClosure = None 11 | 12 | 13 | ''' This endpoint was not present in the py3cw module 14 | @logged 15 | @with_py3cw 16 | def post_create_simple_sell(payload: dict = None): 17 | """ 18 | POST /ver1/smart_trades/create_simple_sell 19 | Create SimpleSell (Permission: SMART_TRADE_WRITE, Security: SIGNED) 20 | 21 | """ 22 | error, data = wrapper.request( 23 | entity='', 24 | action='', 25 | payload=payload, 26 | ) 27 | return ThreeCommasApiError(error), data 28 | ''' 29 | 30 | 31 | ''' This endpoint was not present in the py3cw module 32 | @logged 33 | @with_py3cw 34 | def post_create_simple_buy(payload: dict = None): 35 | """ 36 | POST /ver1/smart_trades/create_simple_buy 37 | Create SimpleBuy (Permission: SMART_TRADE_WRITE, Security: SIGNED) 38 | 39 | """ 40 | error, data = wrapper.request( 41 | entity='', 42 | action='', 43 | payload=payload, 44 | ) 45 | return ThreeCommasApiError(error), data 46 | ''' 47 | 48 | 49 | ''' This endpoint was not present in the py3cw module 50 | @logged 51 | @with_py3cw 52 | def post_create_smart_sell(payload: dict = None): 53 | """ 54 | POST /ver1/smart_trades/create_smart_sell 55 | Create SmartSale (Permission: SMART_TRADE_WRITE, Security: SIGNED) 56 | 57 | """ 58 | error, data = wrapper.request( 59 | entity='', 60 | action='', 61 | payload=payload, 62 | ) 63 | return ThreeCommasApiError(error), data 64 | ''' 65 | 66 | 67 | ''' This endpoint was not present in the py3cw module 68 | @logged 69 | @with_py3cw 70 | def post_create_smart_cover(payload: dict = None): 71 | """ 72 | POST /ver1/smart_trades/create_smart_cover 73 | Create SmartCover (Permission: SMART_TRADE_WRITE, Security: SIGNED) 74 | 75 | """ 76 | error, data = wrapper.request( 77 | entity='', 78 | action='', 79 | payload=payload, 80 | ) 81 | return ThreeCommasApiError(error), data 82 | ''' 83 | 84 | 85 | ''' This endpoint was not present in the py3cw module 86 | @logged 87 | @with_py3cw 88 | def post_create_smart_trade(payload: dict = None): 89 | """ 90 | POST /ver1/smart_trades/create_smart_trade 91 | Create SmartTrade (Permission: SMART_TRADE_WRITE, Security: SIGNED) 92 | 93 | """ 94 | error, data = wrapper.request( 95 | entity='', 96 | action='', 97 | payload=payload, 98 | ) 99 | return ThreeCommasApiError(error), data 100 | ''' 101 | 102 | 103 | ''' This endpoint was not present in the py3cw module 104 | @logged 105 | @with_py3cw 106 | def get(payload: dict = None): 107 | """ 108 | GET /ver1/smart_trades 109 | Get SmartTrade history (Permission: SMART_TRADE_READ, Security: SIGNED) 110 | 111 | """ 112 | error, data = wrapper.request( 113 | entity='', 114 | action='', 115 | payload=payload, 116 | ) 117 | return ThreeCommasApiError(error), data 118 | ''' 119 | 120 | 121 | ''' This endpoint was not present in the py3cw module 122 | @logged 123 | @with_py3cw 124 | def post_cancel_order_by_id(id, payload: dict = None): 125 | """ 126 | POST /ver1/smart_trades/{smart_trade_id}/cancel_order 127 | Manual cancel order (Permission: SMART_TRADE_WRITE, Security: SIGNED) 128 | 129 | """ 130 | error, data = wrapper.request( 131 | entity='', 132 | action='', 133 | action_id=str(id), 134 | payload=payload, 135 | ) 136 | return ThreeCommasApiError(error), data 137 | ''' 138 | 139 | 140 | ''' This endpoint was not present in the py3cw module 141 | @logged 142 | @with_py3cw 143 | def post_add_funds_by_id(id, payload: dict = None): 144 | """ 145 | POST /ver1/smart_trades/{smart_trade_id}/add_funds 146 | Smart Trade add funds (Permission: SMART_TRADE_WRITE, Security: SIGNED) 147 | 148 | """ 149 | error, data = wrapper.request( 150 | entity='', 151 | action='', 152 | action_id=str(id), 153 | payload=payload, 154 | ) 155 | return ThreeCommasApiError(error), data 156 | ''' 157 | 158 | 159 | ''' This endpoint was not present in the py3cw module 160 | @logged 161 | @with_py3cw 162 | def post_step_panic_sell_by_id(id, payload: dict = None): 163 | """ 164 | POST /ver1/smart_trades/{smart_trade_id}/step_panic_sell 165 | Step panic sell (Permission: SMART_TRADE_WRITE, Security: SIGNED) 166 | 167 | """ 168 | error, data = wrapper.request( 169 | entity='', 170 | action='', 171 | action_id=str(id), 172 | payload=payload, 173 | ) 174 | return ThreeCommasApiError(error), data 175 | ''' 176 | 177 | 178 | ''' This endpoint was not present in the py3cw module 179 | @logged 180 | @with_py3cw 181 | def patch_update_by_id(id, payload: dict = None): 182 | """ 183 | PATCH /ver1/smart_trades/{smart_trade_id}/update 184 | Edit SmartTrade/SmartSale/SmartCover (Permission: SMART_TRADE_WRITE, Security: SIGNED) 185 | 186 | """ 187 | error, data = wrapper.request( 188 | entity='', 189 | action='', 190 | action_id=str(id), 191 | payload=payload, 192 | ) 193 | return ThreeCommasApiError(error), data 194 | ''' 195 | 196 | 197 | ''' This endpoint was not present in the py3cw module 198 | @logged 199 | @with_py3cw 200 | def post_cancel_by_id(id, payload: dict = None): 201 | """ 202 | POST /ver1/smart_trades/{smart_trade_id}/cancel 203 | Cancel SmartTrade (Permission: SMART_TRADE_WRITE, Security: SIGNED) 204 | 205 | """ 206 | error, data = wrapper.request( 207 | entity='', 208 | action='', 209 | action_id=str(id), 210 | payload=payload, 211 | ) 212 | return ThreeCommasApiError(error), data 213 | ''' 214 | 215 | 216 | ''' This endpoint was not present in the py3cw module 217 | @logged 218 | @with_py3cw 219 | def post_panic_sell_by_id(id, payload: dict = None): 220 | """ 221 | POST /ver1/smart_trades/{smart_trade_id}/panic_sell 222 | Sell currency immediately (Permission: SMART_TRADE_WRITE, Security: SIGNED) 223 | 224 | """ 225 | error, data = wrapper.request( 226 | entity='', 227 | action='', 228 | action_id=str(id), 229 | payload=payload, 230 | ) 231 | return ThreeCommasApiError(error), data 232 | ''' 233 | 234 | 235 | ''' This endpoint was not present in the py3cw module 236 | @logged 237 | @with_py3cw 238 | def post_force_start_by_id(id, payload: dict = None): 239 | """ 240 | POST /ver1/smart_trades/{smart_trade_id}/force_start 241 | Process BuyStep immediately (Permission: SMART_TRADE_WRITE, Security: SIGNED) 242 | 243 | """ 244 | error, data = wrapper.request( 245 | entity='', 246 | action='', 247 | action_id=str(id), 248 | payload=payload, 249 | ) 250 | return ThreeCommasApiError(error), data 251 | ''' 252 | 253 | 254 | ''' This endpoint was not present in the py3cw module 255 | @logged 256 | @with_py3cw 257 | def post_force_process_by_id(id, payload: dict = None): 258 | """ 259 | POST /ver1/smart_trades/{smart_trade_id}/force_process 260 | Refresh SmartTrade state (Permission: SMART_TRADE_WRITE, Security: SIGNED) 261 | 262 | """ 263 | error, data = wrapper.request( 264 | entity='', 265 | action='', 266 | action_id=str(id), 267 | payload=payload, 268 | ) 269 | return ThreeCommasApiError(error), data 270 | ''' 271 | 272 | 273 | ''' This endpoint was not present in the py3cw module 274 | @logged 275 | @with_py3cw 276 | def get_show_by_id(id, payload: dict = None): 277 | """ 278 | GET /ver1/smart_trades/{smart_trade_id}/show 279 | """ 280 | error, data = wrapper.request( 281 | entity='', 282 | action='', 283 | action_id=str(id), 284 | payload=payload, 285 | ) 286 | return ThreeCommasApiError(error), data 287 | ''' 288 | 289 | 290 | -------------------------------------------------------------------------------- /src/three_commas/api/ver1/time.py: -------------------------------------------------------------------------------- 1 | from py3cw.request import Py3CW 2 | from ...model import * 3 | from ...error import ThreeCommasApiError 4 | from typing import Tuple, List 5 | import logging 6 | from ...sys_utils import logged, with_py3cw, Py3cwClosure 7 | 8 | 9 | logger = logging.getLogger(__name__) 10 | wrapper: Py3cwClosure = None 11 | 12 | 13 | ''' This endpoint was not present in the py3cw module 14 | @logged 15 | @with_py3cw 16 | def get(payload: dict = None): 17 | """ 18 | GET /ver1/time 19 | Test connectivity to the Rest API and get the current server time (Permission: NONE, Security: NONE) 20 | 21 | """ 22 | error, data = wrapper.request( 23 | entity='', 24 | action='', 25 | payload=payload, 26 | ) 27 | return ThreeCommasApiError(error), data 28 | ''' 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/three_commas/api/ver1/users.py: -------------------------------------------------------------------------------- 1 | from py3cw.request import Py3CW 2 | from ...model import * 3 | from ...error import ThreeCommasApiError 4 | from typing import Tuple, List 5 | import logging 6 | from ...sys_utils import logged, with_py3cw, Py3cwClosure 7 | 8 | 9 | logger = logging.getLogger(__name__) 10 | wrapper: Py3cwClosure = None 11 | 12 | 13 | @logged 14 | @with_py3cw 15 | def get_current_mode(payload: dict = None): 16 | """ 17 | GET /ver1/users/current_mode 18 | Current User Mode (Paper or Real) (Permission: ACCOUNTS_READ, Security: SIGNED) 19 | 20 | """ 21 | error, data = wrapper.request( 22 | entity='users', 23 | action='current_mode', 24 | payload=payload, 25 | ) 26 | return ThreeCommasApiError(error), data 27 | 28 | 29 | @logged 30 | @with_py3cw 31 | def post_change_mode(payload: dict = None): 32 | """ 33 | POST /ver1/users/change_mode 34 | Change User Mode (Paper or Real) (Permission: ACCOUNTS_WRITE, Security: SIGNED) 35 | 36 | """ 37 | error, data = wrapper.request( 38 | entity='users', 39 | action='change_mode', 40 | payload=payload, 41 | ) 42 | return ThreeCommasApiError(error), data 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/three_commas/cached_api.py: -------------------------------------------------------------------------------- 1 | from . import api 2 | from . import site 3 | from cachetools import cached, TTLCache 4 | from .model import * 5 | from typing import List 6 | 7 | 8 | @cached(cache=TTLCache(maxsize=1024, ttl=60)) 9 | def get_deals(*args, **kwargs) -> List[DealEntity]: 10 | return api.get_deals(*args, **kwargs) 11 | 12 | 13 | @cached(cache=TTLCache(maxsize=1024, ttl=60)) 14 | def get_all_deals(*args, **kwargs) -> List[DealEntity]: 15 | return api.get_all_deals(*args, **kwargs) 16 | 17 | 18 | @cached(cache=TTLCache(maxsize=1024, ttl=60*3)) 19 | def get_market_pairs(*args, **kwargs) -> List[str]: 20 | return api.get_market_pairs(*args, **kwargs) 21 | 22 | 23 | @cached(cache=TTLCache(maxsize=1024, ttl=60*3)) 24 | def get_account(*args, **kwargs) -> AccountEntity: 25 | return api.get_account(*args, **kwargs) 26 | 27 | 28 | @cached(cache=TTLCache(maxsize=1024, ttl=60*15)) 29 | def get_url_secret(bot_id: int) -> str: 30 | bot_model: Bot = api.get_bot(bot_id=bot_id) 31 | return bot_model.get_url_secret() 32 | 33 | 34 | @cached(cache=TTLCache(maxsize=1024, ttl=60*60*24)) 35 | def get_bot_account_id(bot_id: int) -> int: 36 | bot_model: BotEntity = api.get_bot(bot_id=bot_id) 37 | return bot_model.get_account_id() 38 | 39 | 40 | @cached(cache=TTLCache(maxsize=1024, ttl=60*3)) 41 | def get_bot_profit_line_chart_data(*args, **kwargs): 42 | return site.get_bot_profit_line_chart_data(*args, **kwargs) 43 | 44 | 45 | @cached(cache=TTLCache(maxsize=1024, ttl=60)) 46 | def get_pie_chart_data(*args, **kwargs) -> List[dict]: 47 | return api.get_pie_chart_data(*args, **kwargs) 48 | -------------------------------------------------------------------------------- /src/three_commas/configuration.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | def check_bool_env(env_var_name: str, default_value: bool) -> bool: 9 | default_value_str = str(default_value).upper() 10 | var = os.getenv(env_var_name, default_value_str) 11 | if var.upper() in {'TRUE', 'FALSE'}: 12 | return var.upper() == 'TRUE' 13 | else: 14 | logger.warning(f"boolean variable {env_var_name} value is not set to a boolean. " 15 | f"Should be in {{'TRUE', 'FALSE'}} but is '{var}', Will default to {default_value_str}") 16 | return default_value_str.upper() == 'TRUE' 17 | 18 | 19 | THREE_COMMAS_AUTO_PARSE_DEFAULT = check_bool_env('THREE_COMMAS_AUTO_PARSE_DEFAULT', True) 20 | THREE_COMMAS_AUTO_PARSE_DATETIME_DEFAULT = check_bool_env('THREE_COMMAS_AUTO_PARSE_DATETIME_DEFAULT', False) 21 | THREE_COMMAS_LOG_API = check_bool_env('THREE_COMMAS_LOG_API_DEFAULT', True) # will log only on debug level 22 | REDUCED_LOGGING_LIMIT = 130 23 | -------------------------------------------------------------------------------- /src/three_commas/error.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import List 3 | from dataclasses import dataclass 4 | import logging 5 | from .model.models import ThreeCommasDict 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | @dataclass 11 | class BaseOrderToSmallErrorElement: 12 | amount: float = None 13 | pair: str = None 14 | 15 | 16 | def cast_bool(func): 17 | def wrapped(*args, **kwargs): 18 | return bool(func(*args, **kwargs)) 19 | return wrapped 20 | 21 | 22 | class ThreeCommasApiError(ThreeCommasDict): 23 | BO_TO_SMALL_ERROR_PATTERN = re.compile(r"Base order size is too small\. Min: ([0-9.]*),? ?([\w_]+)?", re.IGNORECASE) 24 | NO_MARKET_PAIR_ERROR_PATTERN = re.compile(r"No market data for this pair: ([^\']*)\'", re.IGNORECASE) 25 | EXTRACT_PY3CW_MESSAGE_PATTERN = re.compile(r"Other error occurred: record_invalid Invalid parameters (\{.*\})\.", re.IGNORECASE) 26 | BOT_WAS_DELETED_ERROR_PATTERN = re.compile(r"Other error occurred: Not found None None", re.IGNORECASE) 27 | BOT_DID_NOT_EXISTED_OR_BELONGS_TO_OTHER_ACCOUNT_ERROR_PATTERN = re.compile(r"Other error occurred: not_found Not Found None", re.IGNORECASE) 28 | API_KEY_NOT_ENOUGH_PERMISSION_PATTERN = re.compile(r"access_denied Api key doesn't have enough permissions", re.IGNORECASE) 29 | API_KEY_INVALID_OR_EXPIRED_PATTERN = re.compile(r'api_key_invalid_or_expired Unauthorized. Invalid or expired api key', re.IGNORECASE) 30 | API_KEY_NOT_AUTHORIZED_FOR_THIS_ACTION_PATTERN = re.compile(r'key not authorized for this action', re.IGNORECASE) 31 | ACCOUNT_ALREADY_CONNECTED_PATTERN = re.compile(r'This account is already connected!', re.IGNORECASE) 32 | ACCOUNT_NOT_DELETABLE_PATTERN = re.compile(r"account_not_deletable Can't remove account.", re.IGNORECASE) 33 | 34 | @cast_bool 35 | def is_account_not_deletable_error(self) -> bool: 36 | if self.get_status_code() and self.get_status_code() != 422: 37 | return False 38 | return self._has_error_message() and self.ACCOUNT_NOT_DELETABLE_PATTERN.findall(self.get_msg()) 39 | 40 | @cast_bool 41 | def is_account_already_connected_error(self) -> bool: 42 | return self._has_error_message() and self.ACCOUNT_ALREADY_CONNECTED_PATTERN.findall(self.get_msg()) 43 | 44 | @cast_bool 45 | def is_api_key_not_authorized_for_this_action_error(self) -> bool: 46 | return self._has_error_message() and self.API_KEY_NOT_AUTHORIZED_FOR_THIS_ACTION_PATTERN.findall(self.get_msg()) 47 | 48 | @cast_bool 49 | def is_api_key_has_no_permission_error(self) -> bool: 50 | return self._has_error_message() and self.API_KEY_NOT_ENOUGH_PERMISSION_PATTERN.findall(self.get_msg()) 51 | 52 | @cast_bool 53 | def is_api_key_invalid_or_expired(self) -> bool: 54 | return self._has_error_message() and self.API_KEY_INVALID_OR_EXPIRED_PATTERN.findall(self.get_msg()) 55 | 56 | @cast_bool 57 | def is_base_order_to_small_error(self) -> bool: 58 | return self._has_error_message() and self.BO_TO_SMALL_ERROR_PATTERN.findall(self.get_msg()) 59 | 60 | @cast_bool 61 | def is_not_found_error(self) -> bool: 62 | return self._has_error_message() and 'not_found' in self.get_msg() or 'Not found' in self.get_msg() 63 | 64 | @cast_bool 65 | def is_no_market_pair_error(self) -> List[str]: 66 | return self._has_error_message() and self.NO_MARKET_PAIR_ERROR_PATTERN.findall(self.get_msg()) 67 | 68 | def get_no_market_pair_error(self) -> List[str]: 69 | if self._has_error_message(): 70 | pairs_to_remove = ThreeCommasApiError.NO_MARKET_PAIR_ERROR_PATTERN.findall(self.get_msg()) 71 | if pairs_to_remove: 72 | return pairs_to_remove 73 | return list() 74 | 75 | def get_base_order_to_small_error(self) -> List[BaseOrderToSmallErrorElement]: 76 | ret = list() 77 | if self._has_error_message(): 78 | try: 79 | match = ThreeCommasApiError.EXTRACT_PY3CW_MESSAGE_PATTERN.findall(self.get_msg()) 80 | if match: 81 | error_parsed = eval(match[0]) 82 | else: 83 | return list() 84 | except: 85 | return list() 86 | if error_parsed.get('base_order_volume'): 87 | for sub_message in error_parsed.get('base_order_volume'): 88 | bo_min_match = ThreeCommasApiError.BO_TO_SMALL_ERROR_PATTERN.findall(sub_message) 89 | if bo_min_match: 90 | amount = float(bo_min_match[0][0]) 91 | pair = bo_min_match[0][1] or None 92 | ret.append(BaseOrderToSmallErrorElement(amount=amount, pair=pair)) 93 | return ret 94 | 95 | @cast_bool 96 | def _has_error_message(self) -> bool: 97 | return bool(self and self.get_msg()) 98 | 99 | def get_msg(self) -> str: 100 | return self.get('msg') 101 | 102 | def get_status_code(self) -> int: 103 | return self.get('status_code', None) 104 | 105 | 106 | class ThreeCommasException(RuntimeError): 107 | pass 108 | -------------------------------------------------------------------------------- /src/three_commas/model/__init__.py: -------------------------------------------------------------------------------- 1 | from .generated_models import * 2 | from . import generated_enums as enums 3 | -------------------------------------------------------------------------------- /src/three_commas/model/generated_enums.py: -------------------------------------------------------------------------------- 1 | from .other_enums import AbstractStringEnum 2 | 3 | 4 | class DealStatus(AbstractStringEnum): 5 | ACTIVE = 'active' 6 | FINISHED = 'finished' 7 | COMPLETED = 'completed' 8 | CANCELLED = 'cancelled' 9 | FAILED = 'failed' 10 | 11 | def is_active(self) -> bool: 12 | return self == DealStatus.ACTIVE 13 | 14 | def is_finished(self) -> bool: 15 | return self == DealStatus.FINISHED 16 | 17 | def is_completed(self) -> bool: 18 | return self == DealStatus.COMPLETED 19 | 20 | def is_cancelled(self) -> bool: 21 | return self == DealStatus.CANCELLED 22 | 23 | def is_failed(self) -> bool: 24 | return self == DealStatus.FAILED 25 | 26 | 27 | class BotScope(AbstractStringEnum): 28 | ENABLED = 'enabled' 29 | DISABLED = 'disabled' 30 | 31 | def is_enabled(self) -> bool: 32 | return self == BotScope.ENABLED 33 | 34 | def is_disabled(self) -> bool: 35 | return self == BotScope.DISABLED 36 | 37 | 38 | class Mode(AbstractStringEnum): 39 | PAPER = 'paper' 40 | REAL = 'real' 41 | 42 | def is_paper(self) -> bool: 43 | return self == Mode.PAPER 44 | 45 | def is_real(self) -> bool: 46 | return self == Mode.REAL 47 | 48 | 49 | class AccountMarketCode(AbstractStringEnum): 50 | PAPER_TRADING = 'paper_trading' 51 | BINANCE = 'binance' 52 | BITFINEX = 'bitfinex' 53 | BITSTAMP = 'bitstamp' 54 | BITTREX = 'bittrex' 55 | GDAX = 'gdax' 56 | GEMINI = 'gemini' 57 | HUOBI = 'huobi' 58 | KUCOIN = 'kucoin' 59 | OKEX = 'okex' 60 | POLONIEX = 'poloniex' 61 | BITMEX = 'bitmex' 62 | KRAKEN = 'kraken' 63 | GATE_IO = 'gate_io' 64 | BINANCE_MARGIN = 'binance_margin' 65 | BYBIT = 'bybit' 66 | BINANCE_US = 'binance_us' 67 | BINANCE_FUTURES = 'binance_futures' 68 | DERIBIT = 'deribit' 69 | FTX = 'ftx' 70 | FTX_US = 'ftx_us' 71 | BYBIT_USDT_PERPETUAL = 'bybit_usdt_perpetual' 72 | BINANCE_FUTURES_COIN = 'binance_futures_coin' 73 | BYBIT_SPOT = 'bybit_spot' 74 | GATE_IO_USDT_PERPETUAL = 'gate_io_usdt_perpetual' 75 | GATE_IO_BTC_PERPETUAL = 'gate_io_btc_perpetual' 76 | ETHEREUMWALLET = 'ethereumwallet' 77 | TRX = 'trx' 78 | 79 | def is_paper_trading(self) -> bool: 80 | return self == AccountMarketCode.PAPER_TRADING 81 | 82 | def is_binance(self) -> bool: 83 | return self == AccountMarketCode.BINANCE 84 | 85 | def is_bitfinex(self) -> bool: 86 | return self == AccountMarketCode.BITFINEX 87 | 88 | def is_bitstamp(self) -> bool: 89 | return self == AccountMarketCode.BITSTAMP 90 | 91 | def is_bittrex(self) -> bool: 92 | return self == AccountMarketCode.BITTREX 93 | 94 | def is_gdax(self) -> bool: 95 | return self == AccountMarketCode.GDAX 96 | 97 | def is_gemini(self) -> bool: 98 | return self == AccountMarketCode.GEMINI 99 | 100 | def is_huobi(self) -> bool: 101 | return self == AccountMarketCode.HUOBI 102 | 103 | def is_kucoin(self) -> bool: 104 | return self == AccountMarketCode.KUCOIN 105 | 106 | def is_okex(self) -> bool: 107 | return self == AccountMarketCode.OKEX 108 | 109 | def is_poloniex(self) -> bool: 110 | return self == AccountMarketCode.POLONIEX 111 | 112 | def is_bitmex(self) -> bool: 113 | return self == AccountMarketCode.BITMEX 114 | 115 | def is_kraken(self) -> bool: 116 | return self == AccountMarketCode.KRAKEN 117 | 118 | def is_gate_io(self) -> bool: 119 | return self == AccountMarketCode.GATE_IO 120 | 121 | def is_binance_margin(self) -> bool: 122 | return self == AccountMarketCode.BINANCE_MARGIN 123 | 124 | def is_bybit(self) -> bool: 125 | return self == AccountMarketCode.BYBIT 126 | 127 | def is_binance_us(self) -> bool: 128 | return self == AccountMarketCode.BINANCE_US 129 | 130 | def is_binance_futures(self) -> bool: 131 | return self == AccountMarketCode.BINANCE_FUTURES 132 | 133 | def is_deribit(self) -> bool: 134 | return self == AccountMarketCode.DERIBIT 135 | 136 | def is_ftx(self) -> bool: 137 | return self == AccountMarketCode.FTX 138 | 139 | def is_ftx_us(self) -> bool: 140 | return self == AccountMarketCode.FTX_US 141 | 142 | def is_bybit_usdt_perpetual(self) -> bool: 143 | return self == AccountMarketCode.BYBIT_USDT_PERPETUAL 144 | 145 | def is_binance_futures_coin(self) -> bool: 146 | return self == AccountMarketCode.BINANCE_FUTURES_COIN 147 | 148 | def is_bybit_spot(self) -> bool: 149 | return self == AccountMarketCode.BYBIT_SPOT 150 | 151 | def is_gate_io_usdt_perpetual(self) -> bool: 152 | return self == AccountMarketCode.GATE_IO_USDT_PERPETUAL 153 | 154 | def is_gate_io_btc_perpetual(self) -> bool: 155 | return self == AccountMarketCode.GATE_IO_BTC_PERPETUAL 156 | 157 | def is_ethereumwallet(self) -> bool: 158 | return self == AccountMarketCode.ETHEREUMWALLET 159 | 160 | def is_trx(self) -> bool: 161 | return self == AccountMarketCode.TRX 162 | -------------------------------------------------------------------------------- /src/three_commas/model/generated_models.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from .models import ThreeCommasModel, FloatParser, IntParser, DatetimeParser, ParsedProxy 3 | import datetime 4 | from typing import Union 5 | 6 | 7 | class IndexEntity(ThreeCommasModel): 8 | bots: list 9 | total: int 10 | page: int 11 | 12 | _parse_map = { 13 | } 14 | _name_proxy = { 15 | } 16 | 17 | 18 | class MarketplaceBotEntity(ThreeCommasModel): 19 | id: int 20 | type: str 21 | name: str 22 | strategy: str 23 | secret: str 24 | marketplace_item: None 25 | profit: None 26 | currencies: list 27 | copies: int 28 | is_favorite: bool 29 | 30 | _parse_map = { 31 | } 32 | _name_proxy = { 33 | } 34 | 35 | 36 | class MarketplaceItem(ThreeCommasModel): 37 | id: int 38 | name: str 39 | icon_url: str 40 | 41 | _parse_map = { 42 | } 43 | _name_proxy = { 44 | } 45 | 46 | 47 | class Profit(ThreeCommasModel): 48 | period: str 49 | amount: float 50 | chart_data: list 51 | 52 | _parse_map = { 53 | } 54 | _name_proxy = { 55 | } 56 | 57 | 58 | class PongEntity(ThreeCommasModel): 59 | pong: str 60 | 61 | _parse_map = { 62 | } 63 | _name_proxy = { 64 | } 65 | 66 | 67 | class TimeEntity(ThreeCommasModel): 68 | server_time: int 69 | 70 | _parse_map = { 71 | } 72 | _name_proxy = { 73 | } 74 | 75 | 76 | class BotEntity(ThreeCommasModel): 77 | id: int 78 | account_id: int 79 | is_enabled: bool 80 | max_safety_orders: int 81 | active_safety_orders_count: int 82 | pairs: str 83 | strategy_list: str 84 | max_active_deals: int 85 | active_deals_count: int 86 | deletable: bool 87 | created_at: Union[str, datetime.datetime] 88 | updated_at: Union[str, datetime.datetime] 89 | trailing_enabled: bool 90 | tsl_enabled: bool 91 | deal_start_delay_seconds: int 92 | stop_loss_timeout_enabled: bool 93 | stop_loss_timeout_in_seconds: int 94 | disable_after_deals_count: int 95 | deals_counter: int 96 | allowed_deals_on_same_pair: int 97 | easy_form_supported: bool 98 | close_deals_timeout: int 99 | url_secret: str 100 | name: str 101 | take_profit: Union[str, float] 102 | base_order_volume: Union[str, float] 103 | safety_order_volume: Union[str, float] 104 | safety_order_step_percentage: Union[str, float] 105 | take_profit_type: str 106 | type: str 107 | martingale_volume_coefficient: Union[str, float] 108 | martingale_step_coefficient: Union[str, float] 109 | stop_loss_percentage: Union[str, float] 110 | cooldown: str 111 | btc_price_limit: Union[str, float] 112 | strategy: str 113 | min_volume_btc_24h: Union[str, float] 114 | profit_currency: str 115 | min_price: str 116 | max_price: str 117 | stop_loss_type: str 118 | safety_order_volume_type: str 119 | base_order_volume_type: str 120 | account_name: str 121 | trailing_deviation: Union[str, float] 122 | finished_deals_profit_usd: Union[str, float] 123 | finished_deals_count: Union[str, int] 124 | leverage_type: str 125 | leverage_custom_value: str 126 | start_order_type: str 127 | active_deals_usd_profit: Union[str, float] 128 | 129 | _parse_map = { 130 | 'created_at': DatetimeParser, 131 | 'updated_at': DatetimeParser, 132 | 'take_profit': FloatParser, 133 | 'base_order_volume': FloatParser, 134 | 'safety_order_volume': FloatParser, 135 | 'safety_order_step_percentage': FloatParser, 136 | 'martingale_volume_coefficient': FloatParser, 137 | 'martingale_step_coefficient': FloatParser, 138 | 'stop_loss_percentage': FloatParser, 139 | 'btc_price_limit': FloatParser, 140 | 'min_volume_btc_24h': FloatParser, 141 | 'trailing_deviation': FloatParser, 142 | 'finished_deals_profit_usd': FloatParser, 143 | 'finished_deals_count': IntParser, 144 | 'active_deals_usd_profit': FloatParser, 145 | } 146 | _name_proxy = { 147 | 'deletable': 'deletable?', 148 | } 149 | 150 | 151 | class AccountEntity(ThreeCommasModel): 152 | id: int 153 | auto_balance_period: int 154 | auto_balance_portfolio_id: int 155 | auto_balance_currency_change_limit: int 156 | autobalance_enabled: bool 157 | hedge_mode_available: bool 158 | hedge_mode_enabled: bool 159 | is_locked: bool 160 | smart_trading_supported: bool 161 | smart_selling_supported: bool 162 | available_for_trading: bool 163 | stats_supported: bool 164 | trading_supported: bool 165 | market_buy_supported: bool 166 | market_sell_supported: bool 167 | conditional_buy_supported: bool 168 | bots_allowed: bool 169 | bots_ttp_allowed: bool 170 | bots_tsl_allowed: bool 171 | gordon_bots_available: bool 172 | multi_bots_allowed: bool 173 | created_at: Union[str, datetime.datetime] 174 | updated_at: Union[str, datetime.datetime] 175 | last_auto_balance: str 176 | fast_convert_available: bool 177 | grid_bots_allowed: bool 178 | api_key_invalid: bool 179 | deposit_enabled: bool 180 | supported_market_types: str 181 | api_key: str 182 | name: str 183 | auto_balance_method: int 184 | auto_balance_error: str 185 | customer_id: str 186 | subaccount_name: str 187 | lock_reason: str 188 | btc_amount: Union[str, float] 189 | usd_amount: Union[str, float] 190 | day_profit_btc: Union[str, float] 191 | day_profit_usd: Union[str, float] 192 | day_profit_btc_percentage: Union[str, float] 193 | day_profit_usd_percentage: Union[str, float] 194 | btc_profit: Union[str, float] 195 | usd_profit: Union[str, float] 196 | usd_profit_percentage: Union[str, float] 197 | btc_profit_percentage: Union[str, float] 198 | total_btc_profit: Union[str, float] 199 | total_usd_profit: Union[str, float] 200 | pretty_display_type: str 201 | exchange_name: str 202 | market_code: str 203 | address: str 204 | 205 | _parse_map = { 206 | 'created_at': DatetimeParser, 207 | 'updated_at': DatetimeParser, 208 | 'btc_amount': FloatParser, 209 | 'usd_amount': FloatParser, 210 | 'day_profit_btc': FloatParser, 211 | 'day_profit_usd': FloatParser, 212 | 'day_profit_btc_percentage': FloatParser, 213 | 'day_profit_usd_percentage': FloatParser, 214 | 'btc_profit': FloatParser, 215 | 'usd_profit': FloatParser, 216 | 'usd_profit_percentage': FloatParser, 217 | 'btc_profit_percentage': FloatParser, 218 | 'total_btc_profit': FloatParser, 219 | 'total_usd_profit': FloatParser, 220 | } 221 | _name_proxy = { 222 | } 223 | 224 | 225 | class GridBotEntity(ThreeCommasModel): 226 | id: int 227 | account_id: int 228 | account_name: str 229 | is_enabled: bool 230 | grids_quantity: str 231 | created_at: str 232 | updated_at: str 233 | strategy_type: str 234 | lower_price: str 235 | upper_price: str 236 | quantity_per_grid: str 237 | leverage_type: str 238 | leverage_custom_value: str 239 | name: str 240 | pair: str 241 | start_price: str 242 | grid_price_step: str 243 | current_profit: str 244 | current_profit_usd: str 245 | total_profits_count: str 246 | bought_volume: str 247 | sold_volume: str 248 | profit_percentage: str 249 | current_price: str 250 | investment_base_currency: str 251 | investment_quote_currency: str 252 | grid_lines: None 253 | 254 | _parse_map = { 255 | } 256 | _name_proxy = { 257 | } 258 | 259 | 260 | class GridLineEntity(ThreeCommasModel): 261 | price: str 262 | side: str 263 | order_placed: bool 264 | 265 | _parse_map = { 266 | } 267 | _name_proxy = { 268 | } 269 | 270 | 271 | class GridBotProfitsEntity(ThreeCommasModel): 272 | grid_line_id: int 273 | profit: str 274 | usd_profit: str 275 | created_at: str 276 | 277 | _parse_map = { 278 | } 279 | _name_proxy = { 280 | } 281 | 282 | 283 | class DealEntity(ThreeCommasModel): 284 | id: int 285 | type: str 286 | bot_id: int 287 | max_safety_orders: int 288 | deal_has_error: bool 289 | from_currency_id: int 290 | to_currency_id: int 291 | account_id: int 292 | active_safety_orders_count: int 293 | created_at: Union[str, datetime.datetime] 294 | updated_at: Union[str, datetime.datetime] 295 | closed_at: Union[str, datetime.datetime] 296 | finished: bool 297 | current_active_safety_orders_count: int 298 | current_active_safety_orders: int 299 | completed_safety_orders_count: int 300 | completed_manual_safety_orders_count: int 301 | cancellable: bool 302 | panic_sellable: bool 303 | trailing_enabled: bool 304 | tsl_enabled: bool 305 | stop_loss_timeout_enabled: bool 306 | stop_loss_timeout_in_seconds: int 307 | active_manual_safety_orders: int 308 | pair: str 309 | status: str 310 | localized_status: str 311 | take_profit: Union[str, float] 312 | base_order_volume: Union[str, float] 313 | safety_order_volume: Union[str, float] 314 | safety_order_step_percentage: Union[str, float] 315 | leverage_type: str 316 | leverage_custom_value: str 317 | bought_amount: Union[str, float] 318 | bought_volume: Union[str, float] 319 | bought_average_price: Union[str, float] 320 | base_order_average_price: Union[str, float] 321 | sold_amount: Union[str, float] 322 | sold_volume: Union[str, float] 323 | sold_average_price: Union[str, float] 324 | take_profit_type: str 325 | final_profit: Union[str, float] 326 | martingale_coefficient: Union[str, float] 327 | martingale_volume_coefficient: Union[str, float] 328 | martingale_step_coefficient: Union[str, float] 329 | stop_loss_percentage: Union[str, float] 330 | error_message: str 331 | profit_currency: str 332 | stop_loss_type: str 333 | safety_order_volume_type: str 334 | base_order_volume_type: str 335 | from_currency: str 336 | to_currency: str 337 | current_price: Union[str, float] 338 | take_profit_price: Union[str, float] 339 | stop_loss_price: str 340 | final_profit_percentage: Union[str, float] 341 | actual_profit_percentage: Union[str, float] 342 | bot_name: str 343 | account_name: str 344 | usd_final_profit: Union[str, float] 345 | actual_profit: Union[str, float] 346 | actual_usd_profit: Union[str, float] 347 | failed_message: str 348 | reserved_base_coin: Union[str, float] 349 | reserved_second_coin: Union[str, float] 350 | trailing_deviation: Union[str, float] 351 | trailing_max_price: Union[str, float] 352 | tsl_max_price: str 353 | strategy: str 354 | reserved_quote_funds: Union[float, float] 355 | reserved_base_funds: Union[float, float] 356 | 357 | _parse_map = { 358 | 'created_at': DatetimeParser, 359 | 'updated_at': DatetimeParser, 360 | 'closed_at': DatetimeParser, 361 | 'take_profit': FloatParser, 362 | 'base_order_volume': FloatParser, 363 | 'safety_order_volume': FloatParser, 364 | 'safety_order_step_percentage': FloatParser, 365 | 'bought_amount': FloatParser, 366 | 'bought_volume': FloatParser, 367 | 'bought_average_price': FloatParser, 368 | 'base_order_average_price': FloatParser, 369 | 'sold_amount': FloatParser, 370 | 'sold_volume': FloatParser, 371 | 'sold_average_price': FloatParser, 372 | 'final_profit': FloatParser, 373 | 'martingale_coefficient': FloatParser, 374 | 'martingale_volume_coefficient': FloatParser, 375 | 'martingale_step_coefficient': FloatParser, 376 | 'stop_loss_percentage': FloatParser, 377 | 'current_price': FloatParser, 378 | 'take_profit_price': FloatParser, 379 | 'final_profit_percentage': FloatParser, 380 | 'actual_profit_percentage': FloatParser, 381 | 'usd_final_profit': FloatParser, 382 | 'actual_profit': FloatParser, 383 | 'actual_usd_profit': FloatParser, 384 | 'reserved_base_coin': FloatParser, 385 | 'reserved_second_coin': FloatParser, 386 | 'trailing_deviation': FloatParser, 387 | 'trailing_max_price': FloatParser, 388 | 'reserved_quote_funds': FloatParser, 389 | 'reserved_base_funds': FloatParser, 390 | } 391 | _name_proxy = { 392 | 'finished': 'finished?', 393 | 'cancellable': 'cancellable?', 394 | 'panic_sellable': 'panic_sellable?', 395 | } 396 | 397 | 398 | class SmartTradeV2Entity(ThreeCommasModel): 399 | id: int 400 | version: int 401 | account: dict 402 | pair: str 403 | instant: bool 404 | status: dict 405 | leverage: dict 406 | position: dict 407 | take_profit: dict 408 | stop_loss: dict 409 | note: str 410 | skip_enter_step: bool 411 | data: dict 412 | profit: dict 413 | margin: dict 414 | is_position_not_filled: bool 415 | 416 | _parse_map = { 417 | } 418 | _name_proxy = { 419 | } 420 | 421 | 422 | class TakeProfitStep(ThreeCommasModel): 423 | id: int 424 | version: int 425 | account: dict 426 | pair: str 427 | instant: bool 428 | status: dict 429 | leverage: dict 430 | position: dict 431 | take_profit: dict 432 | stop_loss: dict 433 | note: str 434 | skip_enter_step: bool 435 | data: dict 436 | profit: dict 437 | margin: dict 438 | is_position_not_filled: bool 439 | 440 | _parse_map = { 441 | } 442 | _name_proxy = { 443 | } 444 | 445 | 446 | class BotDealsStatsEntity(ThreeCommasModel): 447 | completed: int 448 | panic_sold: int 449 | active: int 450 | completed_deals_usd_profit: str 451 | from_currency_is_dollars: bool 452 | completed_deals_btc_profit: str 453 | funds_locked_in_active_deals: str 454 | btc_funds_locked_in_active_deals: str 455 | active_deals_usd_profit: str 456 | active_deals_btc_profit: str 457 | 458 | _parse_map = { 459 | } 460 | _name_proxy = { 461 | } 462 | 463 | 464 | class LooseAccountEntity(ThreeCommasModel): 465 | id: int 466 | name: str 467 | created_at: str 468 | updated_at: str 469 | type: str 470 | is_deleted: bool 471 | is_locked: bool 472 | 473 | _parse_map = { 474 | } 475 | _name_proxy = { 476 | } 477 | 478 | -------------------------------------------------------------------------------- /src/three_commas/model/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import List, Union, Callable, TypeVar, Any, Generic, Optional 3 | import datetime 4 | import functools 5 | import logging 6 | from .. import configuration 7 | import copy 8 | 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | T = TypeVar('T') 13 | 14 | 15 | class ThreeCommasParser: 16 | DATETIME_PATTERN = '%Y-%m-%dT%H:%M:%S.%fZ' 17 | 18 | @staticmethod 19 | def parsed_timestamp(func: Callable[[Any], Any]) -> Callable[[Any], Union[None, str, datetime.datetime]]: 20 | @functools.wraps(func) 21 | def wrapper(*args, parsed: bool = None, **kwargs) -> Union[None, str, datetime.datetime]: 22 | timestamp = func(*args, **kwargs) 23 | if timestamp is None: 24 | return None 25 | if parsed is None: 26 | parsed = configuration.THREE_COMMAS_AUTO_PARSE_DATETIME_DEFAULT 27 | return datetime.datetime.strptime(timestamp, ThreeCommasParser.DATETIME_PATTERN) if parsed else timestamp 28 | return wrapper 29 | 30 | @staticmethod 31 | def parsed(t: T): 32 | def decorator(func: Callable[[Any], Any]) -> Callable[[Any], Union[T, None]]: 33 | @functools.wraps(func) 34 | def wrapper(*args, parsed: bool = None, **kwargs) -> Union[T, str, None]: 35 | result = func(*args, **kwargs) 36 | if result is None: 37 | return None 38 | if parsed is None: 39 | parsed = configuration.THREE_COMMAS_AUTO_PARSE_DEFAULT 40 | return t(result) if parsed else result 41 | return wrapper 42 | return decorator 43 | 44 | @staticmethod 45 | def get_setter_with_getter(getter: Callable) -> Callable: 46 | """ 47 | this assumes that the setter exists and has the same name convention. the 'g' is replaces with 's' 48 | """ 49 | getter_name: str = getter.__name__ 50 | setter_name = 's' + getter_name[1:] 51 | 52 | print() 53 | print(getter) 54 | print(getter.__globals__) 55 | print(dir(getter)) 56 | print(getter.__dict__) 57 | 58 | instance_of_getter: dict = getter.__self__ 59 | setter = dir(instance_of_getter)[setter_name] 60 | return setter 61 | # setting the result back 62 | # instance_of_method: dict = func.__self__ 63 | # parameter = None 64 | # if len(args) == 1: 65 | # parameter = args[0] 66 | # elif len(kwargs) == 1: 67 | # parameter = kwargs.popitem()[1] 68 | # 69 | # if not isinstance(instance_of_method, dict): 70 | # logger.warning(f'Enclosing instance is not a dict, cant set the lazy parsed result back') 71 | # elif not parameter: 72 | # logger.warning(f'Could not determine the parameter from {args=}, {kwargs=}') 73 | # elif parameter not in instance_of_method: 74 | # logger.warning(f'{parameter} was not found in the instance {instance_of_method}') 75 | # else: 76 | # instance_of_method[parameter] = parsed_result 77 | 78 | @staticmethod 79 | def lazy_parsed_wip(t: Union[type, List]): 80 | def decorator(getter: Callable) -> Callable: 81 | was_parsed = False 82 | 83 | @functools.wraps(getter) 84 | def wrapper(*args, parsed: bool = True, **kwargs): 85 | nonlocal was_parsed 86 | if was_parsed or not parsed: 87 | return getter(*args, **kwargs) 88 | 89 | result = getter(*args, **kwargs) 90 | was_parsed = True 91 | if result is None: 92 | return None 93 | 94 | if str(t).startswith('typing.List['): 95 | elem_type = t.__args__[0] 96 | # TODO probably should not use the __init__ of the type 97 | parsed_result = [elem_type(elem) for elem in result] 98 | else: 99 | parsed_result = t(result) 100 | 101 | # setter = ThreeCommasParser.get_setter_with_getter(getter=getter) 102 | # setter(parsed_result) 103 | 104 | return parsed_result 105 | return wrapper 106 | return decorator 107 | 108 | @staticmethod 109 | def lazy_parsed(t: Union[type, List]): 110 | def decorator(getter: Callable) -> Callable: 111 | @functools.wraps(getter) 112 | def wrapper(*args, parsed: bool = True, **kwargs): 113 | result = getter(*args, **kwargs) 114 | if result is None: 115 | return None 116 | if not parsed: 117 | return result 118 | if str(t).startswith('typing.List['): 119 | elem_type = t.__args__[0] 120 | parsed_result = [elem_type(elem) for elem in result] 121 | else: 122 | parsed_result = t(result) 123 | return parsed_result 124 | return wrapper 125 | return decorator 126 | 127 | 128 | class ThreeCommasDict(dict): 129 | # TODO probably switch to prodict https://github.com/ramazanpolat/prodict 130 | def __init__(self, *args, **kwargs): 131 | if not args and not kwargs or (args and args[0] is None): 132 | return 133 | super().__init__(*args, **kwargs) 134 | 135 | @classmethod 136 | def deepcopy(cls, copy_from: dict): 137 | cp = copy.deepcopy(copy_from) 138 | return cls(cp) 139 | 140 | def __deepcopy__(self, memo=None): 141 | return self.__class__(copy.deepcopy(dict(self), memo=memo)) 142 | 143 | @classmethod 144 | def of_list(cls, list_of_d: List[dict]) -> List[cls]: 145 | if list_of_d is None: 146 | return list() 147 | return [cls(d) for d in list_of_d] 148 | 149 | def __repr__(self): 150 | return f'{self.__class__.__name__}({super().__repr__()})' 151 | 152 | 153 | class ThreeCommasModel(ThreeCommasDict): 154 | def __getattr__(self, name, parsed: bool = None): 155 | proxy_name = self._name_proxy.get(name) 156 | if proxy_name: 157 | name = proxy_name 158 | value = self.get(name) 159 | 160 | if value is None: 161 | return None 162 | if parsed is False: 163 | return value 164 | 165 | parser: Parser = self.__class__._parse_map.get(name) 166 | if parser is None: 167 | return value 168 | try: 169 | if parsed is None: 170 | return parser.parse(value=value) 171 | else: 172 | return parser.parse(value=value, parsed=parsed) 173 | except Exception: 174 | return value 175 | 176 | def __setattr__(self, name, value): 177 | proxy_name = self._name_proxy.get(name) 178 | if proxy_name is not None: 179 | self[proxy_name] = value 180 | else: 181 | self[name] = value 182 | 183 | TP = TypeVar('TP') 184 | 185 | def parsed(self: TP, parsed: bool) -> TP: 186 | return ParsedProxy(model=self, parsed=parsed) 187 | 188 | 189 | class ParsedProxy: 190 | MODEL_KEY = '_model' 191 | PARSED_KEY = '_parsed' 192 | 193 | def __init__(self, model: ThreeCommasModel, parsed: bool): 194 | self.__dict__[ParsedProxy.MODEL_KEY] = model 195 | self.__dict__[ParsedProxy.PARSED_KEY] = parsed 196 | 197 | def __getattr__(self, name): 198 | return self.__dict__[ParsedProxy.MODEL_KEY].__getattr__(name, parsed=self.__dict__[ParsedProxy.PARSED_KEY]) 199 | 200 | def __setattr__(self, key, value): 201 | self.model.__setattr__(key, value) 202 | 203 | 204 | class Parser: 205 | @staticmethod 206 | def parse(value: str, parsed: bool = None): 207 | raise NotImplemented("The method for parsing was not implemented") 208 | 209 | 210 | class IntParser(Parser): 211 | @staticmethod 212 | def parse(value: str, parsed: bool = True) -> Union[str, int]: 213 | return int(value) if parsed else value 214 | 215 | 216 | class FloatParser(Parser): 217 | @staticmethod 218 | def parse(value: str, parsed: bool = True) -> Union[str, float]: 219 | return float(value) if parsed else value 220 | 221 | 222 | class DatetimeParser(Parser): 223 | DATETIME_PATTERN = '%Y-%m-%dT%H:%M:%S.%fZ' 224 | 225 | @staticmethod 226 | def parse(value, parsed: bool = False) -> Union[str, datetime]: 227 | return datetime.datetime.strptime(value, DatetimeParser.DATETIME_PATTERN) if parsed else value 228 | -------------------------------------------------------------------------------- /src/three_commas/model/other_enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import List 3 | from aenum import extend_enum 4 | import logging 5 | 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class AbstractStringEnum(str, Enum): 11 | @classmethod 12 | def _missing_(cls, value): 13 | logger.warning(f"Enum value='{value}' for {cls} is not known. Will extend the Enum. Allowed values were {cls._list_values()}") 14 | return extend_enum(cls, value.upper(), value) 15 | 16 | @classmethod 17 | def _list_values(cls) -> List[str]: 18 | return list(cls._value2member_map_.keys()) 19 | # return list(map(lambda c: c.value, cls)) 20 | 21 | @classmethod 22 | def _has_value(cls, value: str): 23 | return value in cls._value2member_map_ 24 | 25 | @classmethod 26 | def _has_member(cls, member: str): 27 | return member in cls._member_names_ 28 | 29 | def __eq__(self, other): 30 | if isinstance(other, str): 31 | return self.value == other 32 | else: 33 | return super.__eq__(self, other) 34 | 35 | def __hash__(self): 36 | return hash(self.value) 37 | 38 | def __str__(self): 39 | return self.value 40 | -------------------------------------------------------------------------------- /src/three_commas/site.py: -------------------------------------------------------------------------------- 1 | from .api.ver1.bots import get_show_by_id 2 | from .model import * 3 | import requests 4 | import logging 5 | from .sys_utils import logged 6 | import json 7 | 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | BASE_URL = 'https://3commas.io' 13 | 14 | 15 | @logged 16 | def get_bot_profit_line_chart_data(bot_id: int): 17 | bot_model = get_show_by_id(bot_id=bot_id) 18 | url_secret = bot_model.get_url_secret() 19 | parameters = { 20 | 'secret': url_secret, 21 | } 22 | url = f'{BASE_URL}/bots/{bot_id}/profit_line_chart_data' 23 | response = requests.get(url=url, params=parameters) 24 | if not response.status_code == 200: 25 | return None 26 | obj = json.loads(response.text) 27 | return obj 28 | 29 | 30 | def get_bot_deals_history(bot_id: int): 31 | # not working 32 | bot_model: Bot = get_show(bot_id=bot_id) 33 | url_secret = bot_model.get_url_secret() 34 | parameters = { 35 | 'secret': url_secret, 36 | 'history': 'true', 37 | 'start_at_from': '', 38 | 'start_at_to': '', 39 | 'closed_at_from': '', 40 | 'closed_at_to': '', 41 | 'account_ids': '', 42 | 'bot_ids': f'{bot_id}', 43 | 'order_column': 'closed_at', 44 | 'order_direction': 'desc', 45 | 'page': 1, 46 | 'per_page': 10, 47 | } 48 | url = f'{BASE_URL}/deals/history' 49 | response = requests.get(url=url, params=parameters) 50 | if not response.status_code == 200: 51 | return None 52 | obj = json.loads(response.text) 53 | return obj 54 | -------------------------------------------------------------------------------- /src/three_commas/streams/__init__.py: -------------------------------------------------------------------------------- 1 | from .streams import smart_trades_stream_decorator as smart_trades 2 | from .streams import deals_stream_decorator as deals 3 | -------------------------------------------------------------------------------- /src/three_commas/streams/streams.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import websockets 3 | import json 4 | from ..sys_utils import create_signature 5 | from ..model import DealEntity, SmartTradeV2Entity 6 | from ..error import ThreeCommasException 7 | import logging 8 | from enum import Enum 9 | import functools 10 | import threading 11 | import os 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | BASE_URL = 'wss://ws.3commas.io/websocket' 17 | 18 | 19 | class StreamType(Enum): 20 | class StreamTypeConfig: 21 | def __init__(self, endpoint: str, channel: str, parse_type: type = None): 22 | self.endpoint = endpoint 23 | self.channel = channel 24 | self.parse_type = parse_type 25 | 26 | SMART_TRADES = StreamTypeConfig(endpoint='/smart_trades', channel='SmartTradesChannel', parse_type=SmartTradeV2Entity) 27 | DEALS = StreamTypeConfig(endpoint='/deals', channel='DealsChannel', parse_type=DealEntity) 28 | 29 | def get_endpoint(self): 30 | return self.value.endpoint 31 | 32 | def get_channel(self): 33 | return self.value.channel 34 | 35 | def get_parse_type(self): 36 | return self.value.parse_type 37 | 38 | def has_parse_type(self): 39 | return self.value.parse_type is not None 40 | 41 | 42 | class WebSocketMessageType(str, Enum): 43 | WELCOME = 'welcome' 44 | PING = 'ping' 45 | CONFIRM_SUBSCRIPTION = 'confirm_subscription' 46 | 47 | 48 | class WebSocketMessage(dict): 49 | 50 | def _has_type(self) -> bool: 51 | return 'type' in self 52 | 53 | def _has_identifier(self) -> bool: 54 | return 'identifier' in self 55 | 56 | def _is_type(self, t) -> bool: 57 | return self._has_type() and self.get_type() == t 58 | 59 | def get_type(self) -> str: 60 | return self.get('type') 61 | 62 | def get_identifier(self) -> str: 63 | return self.get('identifier') 64 | 65 | def get_message(self) -> dict: 66 | return self.get('message') 67 | 68 | def is_welcome(self) -> bool: 69 | return self._is_type(WebSocketMessageType.WELCOME) 70 | 71 | def is_ping(self) -> bool: 72 | return self._is_type(WebSocketMessageType.PING) 73 | 74 | def is_confirm_subscription(self) -> bool: 75 | return self._is_type(WebSocketMessageType.CONFIRM_SUBSCRIPTION) 76 | 77 | def is_stream_type(self, stream_type: StreamType): 78 | if not self._has_identifier(): 79 | return False 80 | identifier_dict = json.loads(self.get_identifier()) 81 | channel = identifier_dict.get('channel') 82 | return channel and channel == stream_type.get_channel() 83 | 84 | 85 | def smart_trades_stream_decorator(*args, **kwargs): 86 | return create_runner_for_stream_type(*args, stream_type=StreamType.SMART_TRADES, **kwargs) 87 | 88 | 89 | def deals_stream_decorator(*args, **kwargs): 90 | return create_runner_for_stream_type(*args, stream_type=StreamType.DEALS, **kwargs) 91 | 92 | 93 | def create_runner_for_stream_type(*args, stream_type: StreamType, api_key: str = None, api_secret: str = None): 94 | api_key = api_key or os.getenv('THREE_COMMAS_API_KEY') 95 | api_secret = api_secret or os.getenv('THREE_COMMAS_API_SECRET') 96 | if not api_key or not api_secret: 97 | raise ThreeCommasException('api_key or api_secret is not set. ' 98 | 'Set the THREE_COMMAS_API_KEY and THREE_COMMAS_API_SECRET environment variables.' 99 | 'Or pass the api_key and api_secret as parameters to the decorator.') 100 | 101 | def inner(function_to_wrap): 102 | initial_message = get_message_for(stream_type, api_key, api_secret) 103 | logger.info(f'Initializing a {stream_type.get_channel()}') 104 | 105 | @functools.wraps(function_to_wrap) 106 | async def stream_decorator(): 107 | full_url = f'{BASE_URL}{stream_type.get_endpoint()}' 108 | async with websockets.connect(full_url) as ws: 109 | msg = await ws.send(json.dumps(initial_message)) 110 | async for ws_message in ws: 111 | ws_dict_message = WebSocketMessage(json.loads(ws_message)) 112 | logger.debug(f'Websocket message {json.dumps(ws_dict_message)}') 113 | if ws_dict_message.is_stream_type(stream_type) and ws_dict_message.is_confirm_subscription(): 114 | logger.info(f'Confirmed subscription to {stream_type.get_channel()}') 115 | break 116 | 117 | async for ws_message in ws: 118 | ws_dict_message = WebSocketMessage(json.loads(ws_message)) 119 | logger.debug(f'Websocket message {json.dumps(ws_dict_message)}') 120 | if ws_dict_message.is_stream_type(stream_type): 121 | tc_message = ws_dict_message.get_message() 122 | if stream_type.has_parse_type(): 123 | tc_message = stream_type.get_parse_type()(tc_message) 124 | function_to_wrap(tc_message) 125 | 126 | # loop = asyncio.get_event_loop() 127 | # asyncio.set_event_loop(loop) 128 | # t = threading.Thread(target=loop.run_until_complete, args=(stream_decorator,)) 129 | # t.start() 130 | # asyncio.create_task(stream_decorator()) 131 | loop = asyncio.get_event_loop() 132 | task = loop.create_task(stream_decorator()) 133 | if not loop.is_running(): 134 | t = threading.Thread(target=loop.run_forever) 135 | t.start() 136 | return stream_decorator 137 | 138 | if len(args) == 1 and callable(args[0]): # this enables the decorator to be both parametrized or with no parameters 139 | return inner(function_to_wrap=args[0]) 140 | else: 141 | return inner 142 | 143 | 144 | def get_message_for(stream_type: StreamType, api_key, api_secret): 145 | signature = create_signature(stream_type.get_endpoint(), api_secret) 146 | identifier = { 147 | 'channel': stream_type.get_channel(), 148 | 'users': [ 149 | { 150 | 'api_key': api_key, 151 | 'signature': signature 152 | } 153 | ], 154 | } 155 | message = { 156 | 'identifier': json.dumps(identifier), 157 | "command": "subscribe" 158 | } 159 | return message 160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /src/three_commas/sys_utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import inspect 3 | import logging 4 | import functools 5 | from py3cw.request import Py3CW 6 | from typing import Callable, Union, Tuple 7 | import os 8 | import hmac 9 | import hashlib 10 | from .model.generated_enums import Mode 11 | from . import configuration 12 | from .error import ThreeCommasApiError 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | def get_parent_function_name() -> str: 18 | """ 19 | :return: The name of the function one level up the call stack where this function was called 20 | """ 21 | try: 22 | return sys._getframe(2).f_code.co_name 23 | except ValueError as e: 24 | logger.exception('Error occurred while fetching the name of the parent') 25 | 26 | 27 | def get_parent_module_name() -> str: 28 | """ 29 | :return: The name of the module one level up the call stack 30 | """ 31 | stack_frame = inspect.currentframe() 32 | while stack_frame: 33 | if stack_frame.f_code.co_name == '': 34 | return stack_frame.f_globals['__name__'] 35 | stack_frame = stack_frame.f_back 36 | 37 | 38 | def blur_api_keys(initial_dict: dict): 39 | result = dict(initial_dict) 40 | if 'api_key' in result: 41 | result['api_key'] = f'{result.get("api_key")[:5]}...' 42 | if 'api_secret' in result: 43 | result['api_secret'] = f'{result.get("api_secret")[:5]}...' 44 | return result 45 | 46 | 47 | def reduced_arg(arg): 48 | arg = str(arg) 49 | return arg if len(arg) < configuration.REDUCED_LOGGING_LIMIT else arg[:configuration.REDUCED_LOGGING_LIMIT] + '...' 50 | 51 | 52 | def transform_args_kwargs_for_logging(args: tuple, kwargs: dict, reduce_long_arguments: bool): 53 | if reduce_long_arguments: 54 | logging_args = ', '.join([reduced_arg(a) for a in args]) 55 | logging_kwargs = {k: reduced_arg(v) for k, v in kwargs} 56 | else: 57 | logging_args = args 58 | logging_kwargs = kwargs 59 | logging_kwargs = blur_api_keys(logging_kwargs) 60 | return logging_args, logging_kwargs 61 | 62 | 63 | def logged(*logged_args, 64 | with_logger: logging.Logger = None, 65 | log_return: bool = False, 66 | reduce_long_arguments: bool = False): 67 | """ 68 | :param logged_args: 69 | :param with_logger: Uses the passed logger to log. 70 | By default it will use the logger of the module where the annotation was called 71 | :param log_return: If True, will log the return after the execution of the function 72 | :param reduce_long_arguments: If True and the wrapping function is called with long arguments, the the log will be trimmed 73 | :return: 74 | """ 75 | 76 | if with_logger is None: 77 | parent_module_name = get_parent_module_name() 78 | if parent_module_name is None: 79 | with_logger = logger 80 | else: 81 | with_logger = logging.getLogger(parent_module_name) 82 | 83 | def inner(function_to_wrap): 84 | @functools.wraps(function_to_wrap) 85 | def wrapper(*wrapper_args, **wrapper_kwargs): 86 | if not configuration.THREE_COMMAS_LOG_API: 87 | return function_to_wrap(*wrapper_args, **wrapper_kwargs) 88 | 89 | logging_args, logging_kwargs = transform_args_kwargs_for_logging(wrapper_args, 90 | wrapper_kwargs, 91 | reduce_long_arguments) 92 | with_logger.debug(f"Called '{function_to_wrap.__name__}' with args={logging_args}, kwargs={logging_kwargs}") 93 | 94 | try: 95 | ret = function_to_wrap(*wrapper_args, **wrapper_kwargs) 96 | except Exception as e: 97 | with_logger.debug(f"Function '{function_to_wrap.__name__}' raised an exception {repr(e)}") 98 | raise e 99 | 100 | if log_return: 101 | with_logger.debug(f"Function '{function_to_wrap.__name__}' was executed and returned: {ret}") 102 | else: 103 | with_logger.debug(f"Function '{function_to_wrap.__name__}' was executed") 104 | return ret 105 | return wrapper 106 | 107 | if len(logged_args) == 1 and callable(logged_args[0]): 108 | return inner(function_to_wrap=logged_args[0]) 109 | return inner 110 | 111 | 112 | class Py3cwClosure: 113 | def __init__(self, 114 | py3cw: Py3CW, 115 | additional_headers: dict = None): 116 | self.py3cw = py3cw 117 | self.additional_headers = additional_headers 118 | 119 | def request(self, *args, **kwargs) -> Tuple[dict, Union[dict, list]]: 120 | return self.py3cw.request(*args, **kwargs, additional_headers=self.additional_headers) 121 | 122 | 123 | def with_py3cw(func: Callable) -> Callable: 124 | @functools.wraps(func) 125 | def wrapper(*args, 126 | forced_mode: Union[str, Mode] = None, 127 | additional_headers: dict = None, 128 | api_key: str = None, 129 | api_secret: str = None, 130 | request_options: dict = None, 131 | **kwargs): 132 | 133 | # request options 134 | request_options = request_options or dict() 135 | if request_options: 136 | logger.debug(f"Setting {request_options=}") 137 | 138 | # forced mode 139 | additional_headers = additional_headers or dict() 140 | additional_headers.update(get_forced_mode_headers(req_forced_mode=forced_mode)) 141 | 142 | # py3cw 143 | py3cw = get_py3cw(req_api_key=api_key, req_api_secret=api_secret, request_options=request_options) 144 | 145 | # create buffer 146 | py3cw_closure = Py3cwClosure(additional_headers=additional_headers, py3cw=py3cw) 147 | 148 | inject_py3cw_into_function(func=func, wrapper=py3cw_closure) 149 | 150 | return func(*args, **kwargs) 151 | return wrapper 152 | 153 | 154 | def inject_py3cw_into_function(func: Callable, wrapper: Union[Py3CW, Py3cwClosure]): 155 | func.__globals__['wrapper'] = wrapper 156 | 157 | 158 | def get_forced_mode_headers(req_forced_mode: Union[str, Mode] = None) -> dict: 159 | # request forced mode has precedence over global forced mode 160 | forced_mode = req_forced_mode or os.getenv('THREE_COMMAS_FORCED_MODE') 161 | if forced_mode is None: 162 | return dict() 163 | 164 | if str(forced_mode).lower() == 'real': 165 | logger.debug(f"Forced mode is set to 'real'") 166 | return get_real_headers() 167 | elif str(forced_mode).lower() == 'paper': 168 | logger.debug(f"Forced mode is set to 'paper'") 169 | return get_paper_headers() 170 | else: 171 | logger.warning(f'{forced_mode=} is not known. Will not set.') 172 | return dict() 173 | 174 | 175 | def get_py3cw(req_api_key: str = None, req_api_secret: str = None, request_options: dict = None) -> Py3CW: 176 | # request api keys has precedence over global api keys 177 | api_key = req_api_key or os.getenv("THREE_COMMAS_API_KEY") 178 | api_secret = req_api_secret or os.getenv("THREE_COMMAS_API_SECRET") 179 | if api_key is None or api_secret is None: 180 | raise RuntimeError("Please configure 'THREE_COMMAS_API_KEY' and 'THREE_COMMAS_API_SECRET'") 181 | return Py3CW(key=api_key, secret=api_secret, request_options=request_options) 182 | 183 | 184 | def verify_no_error(error, data): 185 | calling_function_name = get_parent_function_name() 186 | if error: 187 | error['function_name'] = calling_function_name 188 | logger.error(error) 189 | raise ThreeCommasApiError(error=error) 190 | if data is None: 191 | logger.warning(f'No data was received for function {calling_function_name}') 192 | raise ThreeCommasApiError(error={'msg': 'Data is None', 'function_name': calling_function_name}) 193 | 194 | 195 | def create_signature(payload, api_secret): 196 | signature = hmac.new(bytes(api_secret, 'latin-1'), 197 | msg=bytes(payload, 'latin-1'), 198 | digestmod=hashlib.sha256).hexdigest() 199 | return signature 200 | 201 | 202 | def get_paper_headers(): 203 | return {'Forced-Mode': 'paper'} 204 | 205 | 206 | def get_real_headers(): 207 | return {'Forced-Mode': 'real'} 208 | -------------------------------------------------------------------------------- /src/three_commas/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .bot_utils import * 2 | from .pairs_utils import * 3 | -------------------------------------------------------------------------------- /src/three_commas/utils/bot_utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from ..model import * 3 | from typing import List, Union 4 | 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | def _get_bot_math_arguments(bot: BotEntity) -> dict: 10 | return { 11 | 'base_order_volume': bot.get_base_order_volume(), 12 | 'safety_order_volume': bot.get_safety_order_volume(), 13 | 'max_safety_orders': bot.get_max_safety_orders(), 14 | 'martingale_volume_coefficient': bot.get_martingale_volume_coefficient(), 15 | 'max_active_deals': bot.get_max_active_deals(), 16 | } 17 | 18 | 19 | def calculate_so_multiplier(max_so: float, martingale: float) -> float: 20 | return max_so if martingale == 1 else (martingale ** max_so - 1) / (martingale-1) 21 | 22 | 23 | def get_max_bot_usage(bot: BotEntity) -> float: 24 | return calculate_max_bot_usage(**_get_bot_math_arguments(bot)) 25 | 26 | 27 | def calculate_max_bot_usage(base_order_volume: float, 28 | safety_order_volume: float, 29 | max_safety_orders: int, 30 | martingale_volume_coefficient: float, 31 | max_active_deals: int) -> float: 32 | return (base_order_volume + safety_order_volume * 33 | calculate_so_multiplier(max_safety_orders, martingale_volume_coefficient)) * max_active_deals 34 | 35 | 36 | def calculate_bo(max_bot_usage: float, 37 | max_active_deals: int, 38 | max_safety_orders: int, 39 | martingale_volume_coefficient: float, 40 | so: float) -> float: 41 | return max_bot_usage / max_active_deals - so * \ 42 | calculate_so_multiplier(max_safety_orders, martingale_volume_coefficient) 43 | 44 | 45 | def calculate_bo_with_so_bo_ratio(max_bot_usage: float, 46 | max_active_deals: int, 47 | max_safety_orders: int, 48 | martingale_volume_coefficient: float, 49 | so_bo_ratio: float) -> float: 50 | return max_bot_usage / max_active_deals /\ 51 | (1 + so_bo_ratio * calculate_so_multiplier(max_safety_orders, martingale_volume_coefficient)) 52 | 53 | 54 | def calculate_max_active_deals(max_bot_usage: float, 55 | max_safety_orders: int, 56 | martingale_volume_coefficient: float, 57 | base_order_volume: float, 58 | safety_order_volume: float) -> float: 59 | return max_bot_usage / (base_order_volume + safety_order_volume * 60 | calculate_so_multiplier(max_safety_orders, martingale_volume_coefficient)) 61 | 62 | 63 | def get_bot_quote(bot: BotEntity) -> Union[str, None]: 64 | pairs = bot.get_pairs() 65 | if not pairs: 66 | return None 67 | return pairs[0].split('_')[0].upper() 68 | 69 | 70 | def get_bot_base(bot: BotEntity) -> Union[str, None]: 71 | pairs = bot.get_pairs() 72 | if not pairs: 73 | return None 74 | return pairs[0].split('_')[1].upper() 75 | 76 | 77 | def bot_has_pair(bot: BotEntity, pair: str) -> bool: 78 | return bot.get_pairs() and pair in bot.get_pairs() 79 | 80 | 81 | def filter_list_bot_having_pair(bot_list: List[BotEntity], pair: str) -> List[BotEntity]: 82 | return [bot for bot in bot_list if bot_has_pair(bot, pair)] 83 | -------------------------------------------------------------------------------- /src/three_commas/utils/other_utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from src.three_commas.model import Bot 3 | from typing import List 4 | 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | def get_base_from_3c_pair(tc_pair: str, account_market_code: str) -> str: 10 | if account_market_code in {'ftx', 'binance', 'paper_trading'}: 11 | return tc_pair.split('_')[1].upper() 12 | elif account_market_code in {'ftx_futures'}: 13 | return tc_pair.split('_')[1].split('-')[0] 14 | else: 15 | raise RuntimeError(f'Not known market code {account_market_code} in get_base_from_3c_pair') 16 | 17 | 18 | def filter_market_pairs_with_quote(market_pairs: List[str], quote: str): 19 | return [pair for pair in market_pairs if pair.upper().startswith(quote.upper())] 20 | 21 | 22 | def get_quote_from_3c_pair(tc_pair: str) -> str: 23 | return tc_pair.split('_')[0].upper() 24 | 25 | 26 | def pair_is_quote(tc_pair: str, quote: str) -> bool: 27 | return get_quote_from_3c_pair(tc_pair).upper() == quote.upper() 28 | 29 | 30 | def map_spot_tc_pairs_to_bases(tc_pairs: list, account_market_code: str) -> list: 31 | return list(map(lambda pair: get_base_from_3c_pair(tc_pair=pair, account_market_code=account_market_code), tc_pairs)) 32 | 33 | 34 | def filter_tc_pairs_by_quote(pairs: list, quote: str) -> list: 35 | return list(filter(lambda pair: pair_is_quote(tc_pair=pair, quote=quote), pairs)) 36 | 37 | 38 | def construct_pair_from_quote_and_base(quote: str, base: str) -> str: 39 | return f"{quote.upper()}_{base.upper()}" 40 | 41 | 42 | def construct_futures_pair_from_base(base: str, account_market_code: str) -> str: 43 | if account_market_code in {'ftx_futures'}: 44 | return f'USD_{base.upper()}-PERP' 45 | else: 46 | raise RuntimeError(f'Not known market code {account_market_code} in construct_futures_pair_from_quote_and_base') 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/three_commas/utils/pairs_utils.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | def get_base_from_3c_pair(tc_pair: str, account_market_code: str = None) -> str: 4 | if account_market_code is None or account_market_code in {'ftx', 'binance', 'paper_trading'}: 5 | return tc_pair.split('_')[1].upper() 6 | elif account_market_code in {'ftx_futures'}: 7 | return tc_pair.split('_')[1].split('-')[0] 8 | else: 9 | raise RuntimeError(f'Not known market code {account_market_code} in get_base_from_3c_pair') 10 | 11 | 12 | def filter_market_pairs_with_quote(market_pairs: List[str], quote: str): 13 | return [pair for pair in market_pairs if pair.upper().startswith(quote.upper())] 14 | 15 | 16 | def pair_is_quote(tc_pair: str, quote: str) -> bool: 17 | return get_quote_from_3c_pair(tc_pair).upper() == quote.upper() 18 | 19 | 20 | def get_quote_from_3c_pair(tc_pair: str) -> str: 21 | return tc_pair.split('_')[0].upper() 22 | 23 | 24 | def map_spot_tc_pairs_to_bases(tc_pairs: list, account_market_code: str) -> list: 25 | return list(map(lambda pair: get_base_from_3c_pair(tc_pair=pair, account_market_code=account_market_code), tc_pairs)) 26 | 27 | 28 | def construct_pair_from_quote_and_base(quote: str, base: str) -> str: 29 | return f"{quote.upper()}_{base.upper()}" 30 | 31 | 32 | def filter_tc_pairs_by_quote(pairs: list, quote: str) -> list: 33 | return list(filter(lambda pair: pair_is_quote(tc_pair=pair, quote=quote), pairs)) 34 | 35 | 36 | def construct_futures_pair_from_base(base: str, account_market_code: str) -> str: 37 | if account_market_code in {'ftx_futures'}: 38 | return f'USD_{base.upper()}-PERP' 39 | else: 40 | raise RuntimeError(f'Not known market code {account_market_code} in construct_futures_pair_from_quote_and_base') 41 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgerodes/python-three-commas/3d73d098284d3be02fb67686593eb826a2c8520c/test/__init__.py -------------------------------------------------------------------------------- /test/sample_data/accounts/binance_account.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 49428550, 3 | "auto_balance_period": 12, 4 | "auto_balance_portfolio_id": null, 5 | "auto_balance_portfolio": null, 6 | "auto_balance_currency_change_limit": null, 7 | "autobalance_enabled": false, 8 | "hedge_mode_available": false, 9 | "hedge_mode_enabled": false, 10 | "is_locked": false, 11 | "smart_trading_supported": true, 12 | "smart_selling_supported": true, 13 | "available_for_trading": {}, 14 | "stats_supported": true, 15 | "trading_supported": true, 16 | "market_buy_supported": true, 17 | "market_sell_supported": true, 18 | "conditional_buy_supported": true, 19 | "bots_allowed": true, 20 | "bots_ttp_allowed": true, 21 | "bots_tsl_allowed": false, 22 | "gordon_bots_available": true, 23 | "multi_bots_allowed": true, 24 | "created_at": "2021-02-12T10:20:50.324Z", 25 | "updated_at": "2021-08-13T03:17:37.401Z", 26 | "last_auto_balance": null, 27 | "fast_convert_available": true, 28 | "grid_bots_allowed": true, 29 | "api_key_invalid": false, 30 | "nomics_id": "binance", 31 | "market_icon": "https://3commas.io/img/exchanges/binance.png", 32 | "deposit_enabled": true, 33 | "supported_market_types": [ 34 | "spot" 35 | ], 36 | "api_key": "FfwJSffffffffffSx4uRYisheEqjjdlGqw9wuNDjhWG9mxVmlgNE6SQYiCwJG5Bd", 37 | "name": "Binance", 38 | "auto_balance_method": null, 39 | "auto_balance_error": null, 40 | "customer_id": null, 41 | "subaccount_name": null, 42 | "lock_reason": null, 43 | "btc_amount": "0.0107240742867226213295968260148873852", 44 | "usd_amount": "1462.22670804007587897248283579052155399486", 45 | "day_profit_btc": "-0.000996303517642974085135013013413659271036", 46 | "day_profit_usd": "-69.42141852232220358310518508395459829764", 47 | "day_profit_btc_percentage": "-0.13", 48 | "day_profit_usd_percentage": "-0.06", 49 | "btc_profit": "0.0159957457640826213295968260148873852", 50 | "usd_profit": "-1013.70934464712412102751716420947844600514", 51 | "usd_profit_percentage": "-8.83", 52 | "btc_profit_percentage": "6.81", 53 | "total_btc_profit": "0.30528446273810167", 54 | "total_usd_profit": "1739.180179002895", 55 | "pretty_display_type": "Binance", 56 | "exchange_name": "Binance", 57 | "market_code": "binance" 58 | } -------------------------------------------------------------------------------- /test/sample_data/accounts/ftx_account.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 21179501, 3 | "auto_balance_period": 12, 4 | "auto_balance_portfolio_id": null, 5 | "auto_balance_portfolio": null, 6 | "auto_balance_currency_change_limit": null, 7 | "autobalance_enabled": false, 8 | "hedge_mode_available": false, 9 | "hedge_mode_enabled": false, 10 | "is_locked": false, 11 | "smart_trading_supported": true, 12 | "smart_selling_supported": true, 13 | "available_for_trading": {}, 14 | "stats_supported": true, 15 | "trading_supported": true, 16 | "market_buy_supported": true, 17 | "market_sell_supported": true, 18 | "conditional_buy_supported": true, 19 | "bots_allowed": true, 20 | "bots_ttp_allowed": true, 21 | "bots_tsl_allowed": false, 22 | "gordon_bots_available": true, 23 | "multi_bots_allowed": true, 24 | "created_at": "2021-11-01T20:15:40.182Z", 25 | "updated_at": "2021-11-01T20:17:00.836Z", 26 | "last_auto_balance": null, 27 | "fast_convert_available": false, 28 | "grid_bots_allowed": true, 29 | "api_key_invalid": false, 30 | "nomics_id": "ftx", 31 | "market_icon": "https://3commas.io/img/exchanges/ftx.png", 32 | "deposit_enabled": false, 33 | "supported_market_types": [ 34 | "spot" 35 | ], 36 | "api_key": "ffffffff-ff4SSsdWv5Y8qaaae5IYxV0g4qz4FJ-", 37 | "name": "FTX YA", 38 | "auto_balance_method": null, 39 | "auto_balance_error": null, 40 | "customer_id": null, 41 | "subaccount_name": "", 42 | "lock_reason": null, 43 | "btc_amount": "0.01593499846723526942723722", 44 | "usd_amount": "498.63317607757967619350102288", 45 | "day_profit_btc": "-0.00033451834489364136831349609375", 46 | "day_profit_usd": "-21.28649205997120158675221763215684880538", 47 | "day_profit_btc_percentage": "-0.69", 48 | "day_profit_usd_percentage": "-0.67", 49 | "btc_profit": "0.00583120596843706942723722", 50 | "usd_profit": "32.78920793359967619350102288", 51 | "usd_profit_percentage": "2.24", 52 | "btc_profit_percentage": "19.37", 53 | "total_btc_profit": "0.0031388560231358693", 54 | "total_usd_profit": "-502.8826011433603", 55 | "pretty_display_type": "Ftx", 56 | "exchange_name": "FTX", 57 | "market_code": "ftx" 58 | } -------------------------------------------------------------------------------- /test/sample_data/accounts/paper_account.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 39458715, 3 | "auto_balance_period": 12, 4 | "auto_balance_portfolio_id": null, 5 | "auto_balance_portfolio": null, 6 | "auto_balance_currency_change_limit": null, 7 | "autobalance_enabled": false, 8 | "hedge_mode_available": false, 9 | "hedge_mode_enabled": false, 10 | "is_locked": false, 11 | "smart_trading_supported": true, 12 | "smart_selling_supported": true, 13 | "available_for_trading": {}, 14 | "stats_supported": true, 15 | "trading_supported": true, 16 | "market_buy_supported": true, 17 | "market_sell_supported": true, 18 | "conditional_buy_supported": true, 19 | "bots_allowed": true, 20 | "bots_ttp_allowed": true, 21 | "bots_tsl_allowed": false, 22 | "gordon_bots_available": true, 23 | "multi_bots_allowed": true, 24 | "created_at": "2021-02-16T13:52:51.959Z", 25 | "updated_at": "2021-02-23T15:49:31.564Z", 26 | "last_auto_balance": null, 27 | "fast_convert_available": false, 28 | "grid_bots_allowed": true, 29 | "api_key_invalid": false, 30 | "nomics_id": "paper_trading", 31 | "market_icon": "https://3commas.io/img/exchanges/paper_trading.png", 32 | "deposit_enabled": false, 33 | "supported_market_types": [ 34 | "spot" 35 | ], 36 | "api_key": "32146fffffefefe", 37 | "name": "Paper Account 3312313", 38 | "auto_balance_method": null, 39 | "auto_balance_error": null, 40 | "customer_id": null, 41 | "subaccount_name": null, 42 | "lock_reason": null, 43 | "btc_amount": "335.74744532915003095200701385071933263153", 44 | "usd_amount": "14008884.2102127605919560880892039232728042945391", 45 | "day_profit_btc": "-5.216626442056392796219506881984364124065", 46 | "day_profit_usd": "-42249.4141175395892558814940755740539117054609", 47 | "day_profit_btc_percentage": "-3.09", 48 | "day_profit_usd_percentage": "-0.79", 49 | "btc_profit": "50.13439271150003095200701385071933263153", 50 | "usd_profit": "16666.4889077605919560880892039232728042945391", 51 | "usd_profit_percentage": "0.12", 52 | "btc_profit_percentage": "17.55", 53 | "total_btc_profit": "331.9283549509682", 54 | "total_usd_profit": "13821665.06722076", 55 | "pretty_display_type": "PaperTrading", 56 | "exchange_name": "Paper trading account", 57 | "market_code": "paper_trading" 58 | } -------------------------------------------------------------------------------- /test/sample_data/bots/btc/bot_show_btc.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 7675928, 3 | "account_id": 27435212, 4 | "is_enabled": true, 5 | "max_safety_orders": 4, 6 | "active_safety_orders_count": 4, 7 | "pairs": [ 8 | "BTC_1INCH", 9 | "BTC_AAVE", 10 | "BTC_ACM", 11 | "BTC_ADA", 12 | "BTC_ADX", 13 | "BTC_AERGO", 14 | "BTC_AGIX", 15 | "BTC_AGLD", 16 | "BTC_AION", 17 | "BTC_AKRO", 18 | "BTC_ALCX", 19 | "BTC_ALGO", 20 | "BTC_ALICE", 21 | "BTC_ALPACA", 22 | "BTC_ALPHA", 23 | "BTC_AMB", 24 | "BTC_AMP", 25 | "BTC_ANKR", 26 | "BTC_ANT", 27 | "BTC_ANY", 28 | "BTC_AR", 29 | "BTC_ARDR", 30 | "BTC_ARK", 31 | "BTC_ARPA", 32 | "BTC_ASR", 33 | "BTC_AST", 34 | "BTC_ATA", 35 | "BTC_ATM", 36 | "BTC_ATOM", 37 | "BTC_AUCTION", 38 | "BTC_AUDIO", 39 | "BTC_AUTO", 40 | "BTC_AVA", 41 | "BTC_AVAX", 42 | "BTC_AXS", 43 | "BTC_BADGER", 44 | "BTC_BAKE", 45 | "BTC_BAL", 46 | "BTC_BAND", 47 | "BTC_BAR", 48 | "BTC_BAT", 49 | "BTC_BCD", 50 | "BTC_BCH", 51 | "BTC_BEAM", 52 | "BTC_BEL", 53 | "BTC_BETA", 54 | "BTC_BICO", 55 | "BTC_BLZ", 56 | "BTC_BNB", 57 | "BTC_BNT", 58 | "BTC_BNX", 59 | "BTC_BOND", 60 | "BTC_BRD", 61 | "BTC_BTCST", 62 | "BTC_BTG", 63 | "BTC_BTS", 64 | "BTC_C98", 65 | "BTC_CAKE", 66 | "BTC_CELO", 67 | "BTC_CELR", 68 | "BTC_CFX", 69 | "BTC_CHESS", 70 | "BTC_CHR", 71 | "BTC_CHZ", 72 | "BTC_CITY", 73 | "BTC_CKB", 74 | "BTC_CLV", 75 | "BTC_CND", 76 | "BTC_COMP", 77 | "BTC_COS", 78 | "BTC_COTI", 79 | "BTC_CRV", 80 | "BTC_CTK", 81 | "BTC_CTSI", 82 | "BTC_CTXC", 83 | "BTC_CVC", 84 | "BTC_CVX", 85 | "BTC_DAR", 86 | "BTC_DASH", 87 | "BTC_DATA", 88 | "BTC_DCR", 89 | "BTC_DEGO", 90 | "BTC_DGB", 91 | "BTC_DIA", 92 | "BTC_DNT", 93 | "BTC_DOCK", 94 | "BTC_DODO", 95 | "BTC_DOGE", 96 | "BTC_DOT", 97 | "BTC_DREP", 98 | "BTC_DUSK", 99 | "BTC_DYDX", 100 | "BTC_EGLD", 101 | "BTC_ELF", 102 | "BTC_ENJ", 103 | "BTC_ENS", 104 | "BTC_EOS", 105 | "BTC_EPS", 106 | "BTC_ETC", 107 | "BTC_ETH", 108 | "BTC_EZ", 109 | "BTC_FARM", 110 | "BTC_FET", 111 | "BTC_FIDA", 112 | "BTC_FIL", 113 | "BTC_FIO", 114 | "BTC_FIRO", 115 | "BTC_FIS", 116 | "BTC_FLM", 117 | "BTC_FLOW", 118 | "BTC_FLUX", 119 | "BTC_FOR", 120 | "BTC_FORTH", 121 | "BTC_FRONT", 122 | "BTC_FTM", 123 | "BTC_FTT", 124 | "BTC_FXS", 125 | "BTC_GALA", 126 | "BTC_GAS", 127 | "BTC_GLM", 128 | "BTC_GNO", 129 | "BTC_GO", 130 | "BTC_GRS", 131 | "BTC_GRT", 132 | "BTC_GTC", 133 | "BTC_GTO", 134 | "BTC_GXS", 135 | "BTC_HARD", 136 | "BTC_HBAR", 137 | "BTC_HIGH", 138 | "BTC_HIVE", 139 | "BTC_HNT", 140 | "BTC_ICP", 141 | "BTC_ICX", 142 | "BTC_IDEX", 143 | "BTC_ILV", 144 | "BTC_INJ", 145 | "BTC_IOST", 146 | "BTC_IOTA", 147 | "BTC_IOTX", 148 | "BTC_IRIS", 149 | "BTC_JASMY", 150 | "BTC_JOE", 151 | "BTC_JST", 152 | "BTC_JUV", 153 | "BTC_KAVA", 154 | "BTC_KEEP", 155 | "BTC_KLAY", 156 | "BTC_KMD", 157 | "BTC_KNC", 158 | "BTC_KSM", 159 | "BTC_LAZIO", 160 | "BTC_LINA", 161 | "BTC_LINK", 162 | "BTC_LIT", 163 | "BTC_LOOM", 164 | "BTC_LPT", 165 | "BTC_LRC", 166 | "BTC_LSK", 167 | "BTC_LTC", 168 | "BTC_LTO", 169 | "BTC_LUNA", 170 | "BTC_MANA", 171 | "BTC_MATIC", 172 | "BTC_MBOX", 173 | "BTC_MC", 174 | "BTC_MDA", 175 | "BTC_MDT", 176 | "BTC_MDX", 177 | "BTC_MINA", 178 | "BTC_MIR", 179 | "BTC_MITH", 180 | "BTC_MKR", 181 | "BTC_MLN", 182 | "BTC_MOVR", 183 | "BTC_MTH", 184 | "BTC_MTL", 185 | "BTC_NANO", 186 | "BTC_NAS", 187 | "BTC_NAV", 188 | "BTC_NEAR", 189 | "BTC_NEBL", 190 | "BTC_NEO", 191 | "BTC_NKN", 192 | "BTC_NMR", 193 | "BTC_NU", 194 | "BTC_NULS", 195 | "BTC_NXS", 196 | "BTC_OAX", 197 | "BTC_OCEAN", 198 | "BTC_OG", 199 | "BTC_OGN", 200 | "BTC_OM", 201 | "BTC_OMG", 202 | "BTC_ONE", 203 | "BTC_ONG", 204 | "BTC_ONT", 205 | "BTC_ORN", 206 | "BTC_OXT", 207 | "BTC_PAXG", 208 | "BTC_PEOPLE", 209 | "BTC_PERL", 210 | "BTC_PERP", 211 | "BTC_PHA", 212 | "BTC_PHB", 213 | "BTC_PIVX", 214 | "BTC_PLA", 215 | "BTC_PNT", 216 | "BTC_POLS", 217 | "BTC_POLY", 218 | "BTC_POND", 219 | "BTC_PORTO", 220 | "BTC_POWR", 221 | "BTC_PROM", 222 | "BTC_PSG", 223 | "BTC_PYR", 224 | "BTC_QI", 225 | "BTC_QKC", 226 | "BTC_QLC", 227 | "BTC_QNT", 228 | "BTC_QSP", 229 | "BTC_QTUM", 230 | "BTC_QUICK", 231 | "BTC_RAD", 232 | "BTC_RAMP", 233 | "BTC_RARE", 234 | "BTC_REEF", 235 | "BTC_REN", 236 | "BTC_RENBTC", 237 | "BTC_REP", 238 | "BTC_REQ", 239 | "BTC_RGT", 240 | "BTC_RIF", 241 | "BTC_RLC", 242 | "BTC_RNDR", 243 | "BTC_ROSE", 244 | "BTC_RSR", 245 | "BTC_RUNE", 246 | "BTC_RVN", 247 | "BTC_SAND", 248 | "BTC_SANTOS", 249 | "BTC_SC", 250 | "BTC_SCRT", 251 | "BTC_SFP", 252 | "BTC_SKL", 253 | "BTC_SNM", 254 | "BTC_SNT", 255 | "BTC_SNX", 256 | "BTC_SOL", 257 | "BTC_SPELL", 258 | "BTC_SRM", 259 | "BTC_SSV", 260 | "BTC_STEEM", 261 | "BTC_STMX", 262 | "BTC_STORJ", 263 | "BTC_STPT", 264 | "BTC_STRAX", 265 | "BTC_STX", 266 | "BTC_SUPER", 267 | "BTC_SUSHI", 268 | "BTC_SXP", 269 | "BTC_SYS", 270 | "BTC_TCT", 271 | "BTC_TFUEL", 272 | "BTC_THETA", 273 | "BTC_TKO", 274 | "BTC_TLM", 275 | "BTC_TOMO", 276 | "BTC_TORN", 277 | "BTC_TRB", 278 | "BTC_TRIBE", 279 | "BTC_TRU", 280 | "BTC_TRX", 281 | "BTC_TVK", 282 | "BTC_TWT", 283 | "BTC_UMA", 284 | "BTC_UNFI", 285 | "BTC_UNI", 286 | "BTC_UST", 287 | "BTC_UTK", 288 | "BTC_VET", 289 | "BTC_VGX", 290 | "BTC_VIB", 291 | "BTC_VIDT", 292 | "BTC_VITE", 293 | "BTC_VOXEL", 294 | "BTC_WABI", 295 | "BTC_WAN", 296 | "BTC_WAVES", 297 | "BTC_WAXP", 298 | "BTC_WBTC", 299 | "BTC_WING", 300 | "BTC_WNXM", 301 | "BTC_WRX", 302 | "BTC_WTC", 303 | "BTC_XEM", 304 | "BTC_XLM", 305 | "BTC_XMR", 306 | "BTC_XRP", 307 | "BTC_XTZ", 308 | "BTC_XVG", 309 | "BTC_XVS", 310 | "BTC_YFI", 311 | "BTC_YFII", 312 | "BTC_YGG", 313 | "BTC_YOYO", 314 | "BTC_ZEC", 315 | "BTC_ZEN", 316 | "BTC_ZIL", 317 | "BTC_ZRX" 318 | ], 319 | "strategy_list": [ 320 | { 321 | "options": { 322 | "type": "original", 323 | "percent": "3" 324 | }, 325 | "strategy": "qfl" 326 | } 327 | ], 328 | "max_active_deals": 10, 329 | "active_deals_count": 1, 330 | "deletable?": false, 331 | "created_at": "2022-01-07T20:33:13.808Z", 332 | "updated_at": "2022-01-07T20:33:15.363Z", 333 | "trailing_enabled": true, 334 | "tsl_enabled": false, 335 | "deal_start_delay_seconds": 0, 336 | "stop_loss_timeout_enabled": false, 337 | "stop_loss_timeout_in_seconds": 0, 338 | "disable_after_deals_count": null, 339 | "deals_counter": null, 340 | "allowed_deals_on_same_pair": 2, 341 | "easy_form_supported": false, 342 | "close_deals_timeout": 259200, 343 | "url_secret": "ac5fd83632", 344 | "name": "Serge QFL-O v1| BTC ALL ", 345 | "take_profit": "1.85", 346 | "base_order_volume": "0.0001", 347 | "safety_order_volume": "0.0002", 348 | "safety_order_step_percentage": "2.0", 349 | "take_profit_type": "total", 350 | "type": "Bot::MultiBot", 351 | "martingale_volume_coefficient": "2.0", 352 | "martingale_step_coefficient": "1.5", 353 | "stop_loss_percentage": "0.0", 354 | "cooldown": "300", 355 | "btc_price_limit": "0.0", 356 | "strategy": "long", 357 | "min_volume_btc_24h": "300.0", 358 | "profit_currency": "quote_currency", 359 | "min_price": null, 360 | "max_price": null, 361 | "stop_loss_type": "stop_loss", 362 | "safety_order_volume_type": "quote_currency", 363 | "base_order_volume_type": "quote_currency", 364 | "account_name": "Paper Account 332783", 365 | "trailing_deviation": "0.5", 366 | "finished_deals_profit_usd": "4.06", 367 | "finished_deals_count": "16", 368 | "leverage_type": "not_specified", 369 | "leverage_custom_value": null, 370 | "start_order_type": "limit", 371 | "active_deals_usd_profit": "-0.22450002959978", 372 | "active_deals": [ 373 | { 374 | "id": 1199559494, 375 | "type": "Deal", 376 | "bot_id": 7675928, 377 | "max_safety_orders": 4, 378 | "deal_has_error": false, 379 | "from_currency_id": 0, 380 | "to_currency_id": 0, 381 | "account_id": 27435212, 382 | "active_safety_orders_count": 4, 383 | "created_at": "2022-01-08T14:36:28.242Z", 384 | "updated_at": "2022-01-08T18:19:28.804Z", 385 | "closed_at": null, 386 | "finished?": false, 387 | "current_active_safety_orders_count": 2, 388 | "current_active_safety_orders": 2, 389 | "completed_safety_orders_count": 2, 390 | "completed_manual_safety_orders_count": 0, 391 | "cancellable?": true, 392 | "panic_sellable?": true, 393 | "trailing_enabled": true, 394 | "tsl_enabled": false, 395 | "stop_loss_timeout_enabled": false, 396 | "stop_loss_timeout_in_seconds": 0, 397 | "active_manual_safety_orders": 0, 398 | "pair": "BTC_NEAR", 399 | "status": "bought", 400 | "localized_status": "Bought", 401 | "take_profit": "1.85", 402 | "base_order_volume": "0.0001", 403 | "safety_order_volume": "0.0002", 404 | "safety_order_step_percentage": "2.0", 405 | "leverage_type": "not_specified", 406 | "leverage_custom_value": null, 407 | "bought_amount": "2.2", 408 | "bought_volume": "0.000742235494", 409 | "bought_average_price": "0.00033737977", 410 | "base_order_average_price": "0.00034931897", 411 | "sold_amount": "0.0", 412 | "sold_volume": "0.0", 413 | "sold_average_price": "0", 414 | "take_profit_type": "total", 415 | "final_profit": "-0.00001514", 416 | "martingale_coefficient": "1.0", 417 | "martingale_volume_coefficient": "2.0", 418 | "martingale_step_coefficient": "1.5", 419 | "stop_loss_percentage": "0.0", 420 | "error_message": null, 421 | "profit_currency": "quote_currency", 422 | "stop_loss_type": "stop_loss", 423 | "safety_order_volume_type": "quote_currency", 424 | "base_order_volume_type": "quote_currency", 425 | "from_currency": "BTC", 426 | "to_currency": "NEAR", 427 | "current_price": "0.00033532", 428 | "take_profit_price": "0.00034397", 429 | "stop_loss_price": null, 430 | "final_profit_percentage": "0", 431 | "actual_profit_percentage": "-0.61", 432 | "bot_name": "Serge QFL-O v1| BTC ALL ", 433 | "account_name": "", 434 | "usd_final_profit": "-0.65", 435 | "actual_profit": "-0.000005269198", 436 | "actual_usd_profit": "-0.22450002959978", 437 | "failed_message": null, 438 | "reserved_base_coin": "0.000742235494", 439 | "reserved_second_coin": "2.2", 440 | "trailing_deviation": "0.5", 441 | "trailing_max_price": null, 442 | "tsl_max_price": null, 443 | "strategy": "long", 444 | "reserved_quote_funds": "0.002430963", 445 | "reserved_base_funds": 0 446 | } 447 | ] 448 | } -------------------------------------------------------------------------------- /test/sample_data/deals/usdt/deal_show_usdt.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1202348988, 3 | "type": "Deal", 4 | "bot_id": 7675699, 5 | "max_safety_orders": 5, 6 | "deal_has_error": false, 7 | "from_currency_id": 0, 8 | "to_currency_id": 0, 9 | "account_id": 27435212, 10 | "active_safety_orders_count": 2, 11 | "created_at": "2022-01-09T15:31:01.095Z", 12 | "updated_at": "2022-01-09T17:32:13.632Z", 13 | "closed_at": "2022-01-09T17:32:13.632Z", 14 | "finished?": true, 15 | "current_active_safety_orders_count": 0, 16 | "current_active_safety_orders": 0, 17 | "completed_safety_orders_count": 0, 18 | "completed_manual_safety_orders_count": 0, 19 | "cancellable?": false, 20 | "panic_sellable?": false, 21 | "trailing_enabled": true, 22 | "tsl_enabled": false, 23 | "stop_loss_timeout_enabled": false, 24 | "stop_loss_timeout_in_seconds": 0, 25 | "active_manual_safety_orders": 0, 26 | "pair": "USDT_IRIS", 27 | "status": "completed", 28 | "localized_status": "Completed", 29 | "take_profit": "1.85", 30 | "base_order_volume": "10.0", 31 | "safety_order_volume": "20.0", 32 | "safety_order_step_percentage": "2.0", 33 | "leverage_type": "not_specified", 34 | "leverage_custom_value": null, 35 | "bought_amount": "115.3", 36 | "bought_volume": "10.112688586", 37 | "bought_average_price": "0.08770762", 38 | "base_order_average_price": "0.08770762", 39 | "sold_amount": "115.3", 40 | "sold_volume": "10.479504006", 41 | "sold_average_price": "0.09088902", 42 | "take_profit_type": "total", 43 | "final_profit": "0.36681542", 44 | "martingale_coefficient": "1.0", 45 | "martingale_volume_coefficient": "1.8", 46 | "martingale_step_coefficient": "1.5", 47 | "stop_loss_percentage": "0.0", 48 | "error_message": null, 49 | "profit_currency": "quote_currency", 50 | "stop_loss_type": "stop_loss", 51 | "safety_order_volume_type": "quote_currency", 52 | "base_order_volume_type": "quote_currency", 53 | "from_currency": "USDT", 54 | "to_currency": "IRIS", 55 | "current_price": "0.0", 56 | "take_profit_price": null, 57 | "stop_loss_price": null, 58 | "final_profit_percentage": "3.63", 59 | "actual_profit_percentage": "0", 60 | "bot_name": "Nando QFL 21 APR original | USDT ALL", 61 | "account_name": "", 62 | "usd_final_profit": "0.37", 63 | "actual_profit": "0.36681542", 64 | "actual_usd_profit": "0.36681542", 65 | "failed_message": null, 66 | "reserved_base_coin": "-0.36681542", 67 | "reserved_second_coin": "0.0", 68 | "trailing_deviation": "0.5", 69 | "trailing_max_price": "0.09135", 70 | "tsl_max_price": null, 71 | "strategy": "long", 72 | "reserved_quote_funds": 0, 73 | "reserved_base_funds": 0, 74 | "buy_steps": [], 75 | "bot_events": [ 76 | { 77 | "message": "Placing base order. Price: 0.08756 USDT Size: 10.104424 USDT (115.4 IRIS)", 78 | "created_at": "2022-01-09T15:31:01.249Z" 79 | }, 80 | { 81 | "message": "Updating base order", 82 | "created_at": "2022-01-09T15:31:50.241Z" 83 | }, 84 | { 85 | "message": "Placing base order. Price: 0.08762 USDT Size: 10.102586 USDT (115.3 IRIS)", 86 | "created_at": "2022-01-09T15:31:50.468Z" 87 | }, 88 | { 89 | "message": "Base order executed. Price: 0.08770762 USDT. Size: 10.11268859 USDT (115.3 IRIS)", 90 | "created_at": "2022-01-09T15:31:50.511Z" 91 | }, 92 | { 93 | "message": "Trailing TakeProfit order will be placed when price reaches 0.08942 USDT. Size: 10.310126 USDT (115.3 IRIS), the price should rise for 2.08% to start trailing", 94 | "created_at": "2022-01-09T15:31:50.521Z" 95 | }, 96 | { 97 | "message": "Placing safety trade. Price: 0.08595 USDT Size: 20.000565 USDT (232.7 IRIS)", 98 | "created_at": "2022-01-09T15:31:50.689Z" 99 | }, 100 | { 101 | "message": "Placing safety trade. Price: 0.08332 USDT Size: 36.002572 USDT (432.1 IRIS)", 102 | "created_at": "2022-01-09T15:31:50.819Z" 103 | }, 104 | { 105 | "message": "Trailing Take Profit Activated, current profit is 1.91%", 106 | "created_at": "2022-01-09T16:49:08.082Z" 107 | }, 108 | { 109 | "message": "Placing TakeProfit trade. Price: market Size: ≈10.501524 USDT (115.3 IRIS)", 110 | "created_at": "2022-01-09T17:32:02.994Z" 111 | }, 112 | { 113 | "message": "Cancelling buy order. Price: 0.08332 USDT. Size: 0.0 USDT (432.1 IRIS). Success.", 114 | "created_at": "2022-01-09T17:32:13.471Z" 115 | }, 116 | { 117 | "message": "Cancelling buy order. Price: 0.08595 USDT. Size: 0.0 USDT (232.7 IRIS). Success.", 118 | "created_at": "2022-01-09T17:32:13.626Z" 119 | }, 120 | { 121 | "message": "Deal completed. Profit: +0.36681542 USDT (0.37 $) (3.63% from total volume (1.85% before trailing)) 💰💰💰. #profit about 2 hours", 122 | "created_at": "2022-01-09T17:32:13.644Z" 123 | } 124 | ] 125 | } -------------------------------------------------------------------------------- /test/sample_data/deals/usdt/deals_market_orders_usdt.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "order_id": "715146974", 4 | "order_type": "BUY", 5 | "deal_order_type": "Safety", 6 | "cancellable": false, 7 | "status_string": "Cancelled", 8 | "created_at": "2022-01-08T16:32:28.557Z", 9 | "updated_at": "2022-01-08T21:47:03.326Z", 10 | "quantity": "2.85", 11 | "quantity_remaining": "2.85", 12 | "total": "0.0", 13 | "rate": "22.8", 14 | "average_price": "0.0" 15 | }, 16 | { 17 | "order_id": "715474401", 18 | "order_type": "BUY", 19 | "deal_order_type": "Safety", 20 | "cancellable": false, 21 | "status_string": "Cancelled", 22 | "created_at": "2022-01-08T18:24:23.196Z", 23 | "updated_at": "2022-01-08T21:47:03.137Z", 24 | "quantity": "5.53", 25 | "quantity_remaining": "5.53", 26 | "total": "0.0", 27 | "rate": "21.1", 28 | "average_price": "0.0" 29 | }, 30 | { 31 | "order_id": "715675538", 32 | "order_type": "SELL", 33 | "deal_order_type": "Take Profit", 34 | "cancellable": false, 35 | "status_string": "Filled", 36 | "created_at": "2022-01-08T21:47:02.708Z", 37 | "updated_at": "2022-01-08T21:47:02.708Z", 38 | "quantity": "2.74", 39 | "quantity_remaining": "0.0", 40 | "total": "68.8694616", 41 | "rate": "0.0", 42 | "average_price": "25.16" 43 | }, 44 | { 45 | "order_id": "715050324", 46 | "order_type": "BUY", 47 | "deal_order_type": "Safety", 48 | "cancellable": false, 49 | "status_string": "Filled", 50 | "created_at": "2022-01-08T15:08:55.845Z", 51 | "updated_at": "2022-01-08T18:24:22.972Z", 52 | "quantity": "1.51", 53 | "quantity_remaining": "0.0", 54 | "total": "36.1704343", 55 | "rate": "23.93", 56 | "average_price": "23.93" 57 | }, 58 | { 59 | "order_id": "715050313", 60 | "order_type": "BUY", 61 | "deal_order_type": "Safety", 62 | "cancellable": false, 63 | "status_string": "Filled", 64 | "created_at": "2022-01-08T15:08:55.715Z", 65 | "updated_at": "2022-01-08T16:32:28.325Z", 66 | "quantity": "0.82", 67 | "quantity_remaining": "0.0", 68 | "total": "20.2660458", 69 | "rate": "24.69", 70 | "average_price": "24.69" 71 | }, 72 | { 73 | "order_id": "715050297", 74 | "order_type": "BUY", 75 | "deal_order_type": "Base", 76 | "cancellable": false, 77 | "status_string": "Filled", 78 | "created_at": "2022-01-08T15:08:55.464Z", 79 | "updated_at": "2022-01-08T15:08:55.464Z", 80 | "quantity": "0.41", 81 | "quantity_remaining": "0.0", 82 | "total": "10.3300197", 83 | "rate": "25.17", 84 | "average_price": "25.17" 85 | } 86 | ] -------------------------------------------------------------------------------- /test/sample_data/errors/api_key_has_no_permission_error.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": true, 3 | "msg": "Other error occurred: access_denied Api key doesn't have enough permissions None." 4 | } -------------------------------------------------------------------------------- /test/sample_data/errors/api_key_invalid_or_expired_error.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": true, 3 | "msg": "Other error occurred: api_key_invalid_or_expired Unauthorized. Invalid or expired api key. None." 4 | } -------------------------------------------------------------------------------- /test/sample_data/errors/bo_too_small_no_pair.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": true, 3 | "msg": "Other error occurred: record_invalid Invalid parameters {'base_order_volume': ['Base order size is too small. Min: 9.4674']}." 4 | } -------------------------------------------------------------------------------- /test/sample_data/errors/bo_too_small_with_pair.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": true, 3 | "msg": "Other error occurred: record_invalid Invalid parameters {'base_order_volume': ['Base order size is too small. Min: 33.35, USDT_YFI']}." 4 | } -------------------------------------------------------------------------------- /test/sample_data/errors/multiple_bo_so_errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": true, 3 | "msg": "Other error occurred: record_invalid Invalid parameters {'max_active_deals': ['must be less than or equal to 8'], 'base_order_volume': ['Base order size is too small. Min: 10.0, USDT_1INCH', 'Base order size is too small. Min: 10.0, USDT_AAVE', 'Base order size is too small. Min: 10.0, USDT_ACM', 'Base order size is too small. Min: 10.0, USDT_ADA'], 'safety_order_volume': ['Safety trade size is too small. Min: 10.0, USDT_1INCH', 'Safety trade size is too small. Min: 10.0, USDT_AAVE', 'Safety trade size is too small. Min: 10.0, USDT_ACM', 'Safety trade size is too small. Min: 10.0, USDT_ADA']}." 4 | } 5 | -------------------------------------------------------------------------------- /test/sample_data/errors/signature_invalid.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": true, 3 | "msg": "Other error occurred: signature_invalid Provided signature is invalid None." 4 | } -------------------------------------------------------------------------------- /test/test_3c_parameter_parser.py: -------------------------------------------------------------------------------- 1 | from src.three_commas.model import BotEntity 2 | import json 3 | import datetime 4 | 5 | 6 | def test_attribute_with_question_mark(): 7 | bot = BotEntity({ 8 | 'deletable?': False 9 | }) 10 | 11 | assert bot.deletable is False 12 | 13 | bot.deletable = True 14 | 15 | assert bot.deletable is True 16 | assert bot.get('deletable?') is True 17 | 18 | 19 | def test_parsed_is_accepting_all_values(): 20 | bot = BotEntity({ 21 | 'base_order_volume': '1.1' 22 | }) 23 | 24 | assert bot.base_order_volume == 1.1 25 | assert bot.parsed(None).base_order_volume == 1.1 26 | assert bot.parsed(True).base_order_volume == 1.1 27 | assert bot.parsed(False).base_order_volume == "1.1" 28 | 29 | 30 | def test_missing_attributes(): 31 | bot = BotEntity() 32 | 33 | assert bot.take_profit is None 34 | assert bot.parsed(True).take_profit is None 35 | assert bot.parsed(False).take_profit is None 36 | assert bot.parsed(None).take_profit is None 37 | 38 | 39 | def test_parsing(): 40 | bot = BotEntity({ 41 | 'finished_deals_count': '123', 42 | 'base_order_volume': '1.0', 43 | 'created_at': '2019-01-01T00:00:00.000Z', 44 | }) 45 | 46 | # Int parsing. By default is parsed 47 | assert bot.finished_deals_count == 123 48 | assert bot.parsed(True).finished_deals_count == 123 49 | assert bot.parsed(False).finished_deals_count == '123' 50 | 51 | # Float parsing. By default is parsed 52 | assert bot.parsed(True).base_order_volume == 1.0 53 | assert bot.base_order_volume == 1.0 54 | assert bot.parsed(False).base_order_volume == '1.0' 55 | 56 | # Date parsing. By default is not parsed 57 | assert bot.created_at == '2019-01-01T00:00:00.000Z' 58 | assert bot.parsed(True).created_at == datetime.datetime(2019, 1, 1, 0, 0, 0, 0) 59 | assert bot.parsed(False).created_at == '2019-01-01T00:00:00.000Z' 60 | 61 | 62 | def test_set_attributes(): 63 | bot = BotEntity({ 64 | 'finished_deals_count': '123', 65 | 'base_order_volume': '1.0', 66 | 'created_at': '2019-01-01T00:00:00.000Z', 67 | "pairs": [ 68 | "BTC_1INCH", 69 | "BTC_AAVE" 70 | ], 71 | 'deletable?': False 72 | }) 73 | 74 | bot.finished_deals_count = '456' 75 | assert bot.finished_deals_count == 456 76 | 77 | bot.base_order_volume = '3.2' 78 | assert bot.base_order_volume == 3.2 79 | 80 | bot.created_at = '2022-01-01T00:00:00.000Z' 81 | assert bot.created_at == '2022-01-01T00:00:00.000Z' 82 | assert bot.parsed(True).created_at == datetime.datetime(2022, 1, 1, 0, 0, 0, 0) 83 | 84 | bot.pairs = ['BTC_ETH'] 85 | assert bot.pairs == ['BTC_ETH'] 86 | 87 | bot.deletable = True 88 | assert bot.deletable is True 89 | 90 | 91 | def model_equals_dict(): 92 | d = {'finished_deals_count': '123'} 93 | bot = BotEntity({'finished_deals_count': '123'}) 94 | 95 | assert bot == d 96 | assert bot != {'finished_deals_count': '456'} 97 | 98 | 99 | def test_json_equality_after_parsing(): 100 | bot = BotEntity({ 101 | 'finished_deals_count': '123', 102 | 'base_order_volume': '1.0', 103 | 'deletable?': False 104 | }) 105 | 106 | str_bot = json.dumps(bot) 107 | recreated_bot = BotEntity(json.loads(str_bot)) 108 | 109 | assert recreated_bot == bot 110 | 111 | -------------------------------------------------------------------------------- /test/test_bot_models.py: -------------------------------------------------------------------------------- 1 | from src.three_commas.model.generated_models import DealEntity, BotEntity 2 | import json 3 | 4 | 5 | def test_bot_events_are_parsed(): 6 | filepath = 'test/sample_data/bots/btc/bot_show_with_events_btc.json' 7 | #filepath = './sample_data/bots/btc/bot_show_with_events_btc.json' 8 | with open(filepath, 'r+') as f: 9 | bot_show: BotEntity = BotEntity(json.loads(f.read())) 10 | # TODO 11 | # assert isinstance(bot_show.get_bot_events()[0], BotEvent) 12 | # assert isinstance(bot_show.get_active_deals()[0], Deal) 13 | -------------------------------------------------------------------------------- /test/test_enums.py: -------------------------------------------------------------------------------- 1 | from src.three_commas.model import * 2 | from src.three_commas.model.generated_enums import * 3 | import json 4 | 5 | 6 | def test_enum_equality(): 7 | assert BotScope.ENABLED == BotScope.ENABLED 8 | assert BotScope.ENABLED != BotScope.DISABLED 9 | 10 | assert BotScope.ENABLED == 'enabled' 11 | assert BotScope.ENABLED != 'disabled' 12 | 13 | assert BotScope.ENABLED == BotScope.ENABLED.value 14 | assert BotScope.ENABLED != BotScope.DISABLED.value 15 | 16 | assert BotScope.ENABLED != 'random_string' 17 | 18 | assert 'enabled' == BotScope.ENABLED 19 | assert 'disabled' != BotScope.ENABLED 20 | 21 | 22 | def test_enum_hashability(): 23 | assert BotScope.ENABLED in {BotScope.ENABLED} 24 | assert BotScope.ENABLED in {'enabled'} 25 | assert BotScope.ENABLED not in {BotScope.DISABLED} 26 | assert BotScope.ENABLED not in {'disabled'} 27 | 28 | 29 | def test_enum_nullability(): 30 | pass 31 | # TBD 32 | 33 | 34 | def test_enum_parsing(): 35 | filepath = 'test/sample_data/deals/usdt/deal_show_usdt.json' 36 | # filepath = './sample_data/deals/usdt/deal_show_usdt.json' 37 | with open(filepath, 'r+') as f: 38 | j: dict = json.loads(f.read()) 39 | deal: DealEntity = DealEntity(j) 40 | # assert isinstance(deal.status, DealStatus) 41 | # assert isinstance(deal.status.parsed(parsed=False), str) 42 | # assert not status.is_active() 43 | # assert status.is_completed() 44 | -------------------------------------------------------------------------------- /test/test_generated_models.py: -------------------------------------------------------------------------------- 1 | from src.three_commas.model.generated_models import BotEntity 2 | import json 3 | 4 | 5 | def test_bot_events_are_parsed(): 6 | filepath = 'test/sample_data/bots/btc/bot_show_with_events_btc.json' 7 | # filepath = './sample_data/bots/btc/bot_show_with_events_btc.json' 8 | with open(filepath, 'r+') as f: 9 | j: dict = json.loads(f.read()) 10 | bot: BotEntity = BotEntity(j) 11 | # TODO 12 | -------------------------------------------------------------------------------- /test/test_models_field_types.py: -------------------------------------------------------------------------------- 1 | import json 2 | import datetime 3 | 4 | 5 | def test_deal_market_orders(): 6 | with open('test/sample_data/deals/usdt/deals_market_orders_usdt.json', 'r') as f: 7 | j = json.loads(f.read()) 8 | # dmo_list = DealMarketOrder.of_list(j) 9 | # dmo: DealMarketOrder = dmo_list[0] 10 | # assert isinstance(dmo.get_order_id(), int) 11 | # assert isinstance(dmo.get_created_at(parsed=True), datetime.datetime) 12 | # assert isinstance(dmo.get_updated_at(parsed=True), datetime.datetime) 13 | # assert isinstance(dmo.get_quantity(), float) 14 | # assert isinstance(dmo.get_quantity_remaining(), float) 15 | # assert isinstance(dmo.get_total(), float) 16 | # assert isinstance(dmo.get_rate(), float) 17 | # assert isinstance(dmo.get_average_price(), float) 18 | -------------------------------------------------------------------------------- /test/test_three_commas_errors.py: -------------------------------------------------------------------------------- 1 | from src.three_commas.error import ThreeCommasApiError 2 | import json 3 | 4 | 5 | def read_error_from_json(file_path) -> ThreeCommasApiError: 6 | with open(file_path, 'r+') as f: 7 | error = json.loads(f.read()) 8 | error_model = ThreeCommasApiError(error) 9 | return error_model 10 | 11 | 12 | def test_bo_to_small_tc_error_with_pair(): 13 | error = read_error_from_json('test/sample_data/errors/bo_too_small_with_pair.json') 14 | 15 | assert error.is_base_order_to_small_error() 16 | bo_error = error.get_base_order_to_small_error() 17 | assert len(bo_error) == 1 18 | assert bo_error[0].amount == 33.35 19 | assert bo_error[0].pair == 'USDT_YFI' 20 | 21 | 22 | def test_multiple_bo_error(): 23 | error = read_error_from_json('test/sample_data/errors/multiple_bo_so_errors.json') 24 | 25 | bo_error = error.get_base_order_to_small_error() 26 | assert len(bo_error) == 4 27 | assert set(map(lambda be: be.pair, bo_error)) == {'USDT_1INCH', 'USDT_AAVE', 'USDT_ACM', 'USDT_ADA'} 28 | assert list(map(lambda be: be.amount, bo_error)) == [10.0, 10.0, 10.0, 10.0] 29 | 30 | 31 | def test_bo_to_small_tc_error_no_pair(): 32 | error = read_error_from_json('test/sample_data/errors/bo_too_small_no_pair.json') 33 | 34 | bo_error = error.get_base_order_to_small_error() 35 | assert bo_error[0].amount == 9.4674 36 | assert not bo_error[0].pair 37 | 38 | 39 | def test_no_bo_error(): 40 | error = read_error_from_json('test/sample_data/errors/signature_invalid.json') 41 | 42 | bo_error = error.get_base_order_to_small_error() 43 | assert len(bo_error) == 0 44 | assert not error.is_base_order_to_small_error() 45 | 46 | error = ThreeCommasApiError({'custom_message': 'some error occured'}) 47 | bo_error = error.get_base_order_to_small_error() 48 | assert len(bo_error) == 0 49 | 50 | 51 | def test_api_key_invalid_or_expired(): 52 | error = read_error_from_json('test/sample_data/errors/api_key_invalid_or_expired_error.json') 53 | assert error.is_api_key_invalid_or_expired() 54 | 55 | 56 | def test_api_key_has_no_permission_error(): 57 | error = read_error_from_json('test/sample_data/errors/api_key_has_no_permission_error.json') 58 | assert error.is_api_key_has_no_permission_error() 59 | 60 | -------------------------------------------------------------------------------- /type_generators/api_generator.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from pathlib import Path 4 | from typing import List, Union, Dict, Iterable 5 | 6 | IN_PATH = '3commas_swaggerdoc_2022_01_24.json' 7 | OUT_FOLDER = './generated_api' 8 | INDENT = ' ' 9 | 10 | 11 | class SwaggerJson(dict): 12 | def get_paths(self) -> Dict[str, dict]: 13 | return self.get('paths') 14 | 15 | 16 | class PathStructure: 17 | def __init__(self, version, endpoint, sub_endpoint): 18 | self.version = version 19 | self.endpoint = endpoint 20 | self.sub_endpoint = sub_endpoint 21 | 22 | @staticmethod 23 | def of_path(api_path: str): 24 | if api_path.startswith('/'): 25 | api_path = api_path[1:] 26 | split = api_path.split('/') 27 | version = split[0] 28 | endpoint = split[1] 29 | sub_endpoint = '/'.join(split[2:]) 30 | return PathStructure(version, endpoint, sub_endpoint) 31 | 32 | 33 | class PathsElement: 34 | def __init__(self, path: str, path_definition): 35 | self.path = path 36 | self.path_structure = PathStructure.of_path(path) 37 | self.path_definition = path_definition 38 | 39 | 40 | class SignatureElement: 41 | def __init__(self, parameter_name: str, parameter_type: str, required=None): 42 | self.parameter_name = parameter_name 43 | self.parameter_type = parameter_type 44 | self.default_value = required 45 | 46 | 47 | def generate_api(): 48 | with open(IN_PATH, 'r') as f: 49 | swagger_json = json.loads(f.read()) 50 | swagger = SwaggerJson(swagger_json) 51 | paths = swagger.get_paths() 52 | 53 | create_all_folder_and_files(paths.keys()) 54 | 55 | iteration_limit = 1 56 | for path, path_definition in paths.items(): 57 | if iteration_limit <= 0: 58 | break 59 | pe = PathsElement(path, path_definition) 60 | 61 | for http_method in pe.path_definition.keys(): 62 | function_name: str = create_function_name(http_method, pe) 63 | function_signature: str = create_function_signature(http_method, pe.path_definition) 64 | return_type: str = None 65 | description = create_description(http_method, pe, pe.path_definition) 66 | function_body = 'pass' 67 | 68 | write_function(pe, function_name, function_signature, return_type, description, function_body) 69 | 70 | iteration_limit -= 1 71 | 72 | 73 | def write_function(pe: PathsElement, function_name, function_signature, return_type, description, function_body): 74 | with open(get_file_for_ps(pe.path_structure), 'a') as f: 75 | code_buffer = list() 76 | code_buffer.append(f"def {function_name}{function_signature}{f' -> {return_type} ' if return_type else ''}:") 77 | code_buffer.append(f'{INDENT}"""') 78 | code_buffer.append(INDENT + description) 79 | code_buffer.append(f'{INDENT}"""') 80 | code_buffer.append(f'{INDENT}{function_body}') 81 | code = '\n'.join(code_buffer) 82 | f.write(code) 83 | 84 | 85 | def create_description(http_method: str, pe: PathsElement, path_definition: dict) -> str: 86 | return f"""{http_method.upper()} {pe.path} 87 | {path_definition.get(http_method).get('description')}""" 88 | 89 | 90 | def create_function_name(http_method: str, pe: PathsElement) -> str: 91 | return f'{http_method}_{pe.path_structure.sub_endpoint}' 92 | 93 | 94 | def create_function_signature(http_method: str, path_definition: dict) -> str: 95 | type_map = { 96 | 'integer': 'int', 97 | 'string': 'str', 98 | 'boolean': 'bool', 99 | 'number': 'float' 100 | } 101 | signature_elements: List[str] = list() 102 | parameters: List[dict] = path_definition.get(http_method).get('parameters') 103 | for p in parameters: 104 | name = p.get('name') 105 | typ = p.get('type') 106 | required = p.get('required') 107 | sig_element = f'{name}: {type_map.get(typ)}' 108 | if not required: 109 | sig_element += ' = None' 110 | signature_elements.append(sig_element) 111 | 112 | return f'({", ".join(signature_elements)})' 113 | 114 | 115 | def create_folder_and_files(ps: PathStructure): 116 | Path(get_folder_for_ps(ps)).mkdir(parents=True, exist_ok=True) 117 | Path(get_file_for_ps(ps)).touch(exist_ok=True) 118 | 119 | 120 | def get_file_for_ps(ps: PathStructure): 121 | return f'{OUT_FOLDER}/{ps.version}/{ps.endpoint}.py' 122 | 123 | 124 | def get_folder_for_ps(ps: PathStructure): 125 | return f'{OUT_FOLDER}/{ps.version}' 126 | 127 | 128 | def create_all_folder_and_files(paths: Iterable): 129 | for p in paths: 130 | ps = PathStructure.of_path(p) 131 | create_folder_and_files(ps) 132 | 133 | if __name__ == '__main__': 134 | generate_api() 135 | -------------------------------------------------------------------------------- /type_generators/auto_api_from_swaggerdoc.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Dict, List 3 | from collections import defaultdict 4 | from pathlib import Path 5 | import os 6 | from py3cw.config import API_METHODS as PY3CW_API_METHODS 7 | import datetime 8 | import re 9 | from parsing_and_return_mapping import PARSING_MAPPING, endpoint_returns, endpoint_consumes 10 | 11 | 12 | INDENT = ' ' * 4 13 | PARENT_FOLDER_NAME = '../src/three_commas/api' 14 | MODEL_FILE_NAME = '../src/three_commas/model/generated_models.py' 15 | 16 | 17 | def get_path_variables(path: str): 18 | path_variables_list: list = list(filter(lambda e: '{' in e, path.split('/'))) 19 | path_variable_1 = path_variables_list.pop(0).replace('{', '').replace('}', '') if path_variables_list else None 20 | path_variable_2 = path_variables_list.pop(0).replace('{', '').replace('}', '') if path_variables_list else None 21 | return path_variable_1, path_variable_2 22 | 23 | 24 | def create_docstring(verb: str, path: str, parameters: list, description, return_type=None): 25 | if not parameters and not description: 26 | return None 27 | 28 | code = list() 29 | code.append(f'{INDENT}"""') 30 | code.append(f'{INDENT}{verb.upper()} {path}') 31 | if description: 32 | code.append(f'{INDENT}{description}') 33 | code.append(f'') 34 | 35 | # if parameters: 36 | # parameters.sort(key=lambda p: p.get('required'), reverse=True) 37 | # for p in parameters: 38 | # _in = p.get('in') 39 | # param_name = p.get('name') 40 | # param_type = p.get('type') 41 | # required = p.get('required') 42 | # param_description = p.get('description') 43 | # enum = p.get('enum') 44 | # 45 | # param_docstring = list() 46 | # if required: 47 | # param_docstring.append('REQUIRED') 48 | # if param_type: 49 | # param_docstring.append(param_type) 50 | # if enum: 51 | # param_docstring.append("values: " + str(enum)) 52 | # if param_description: 53 | # param_docstring.append(param_description) 54 | # 55 | # code.append(f'{INDENT}:param {param_name}: ' + ', '.join(param_docstring)) 56 | 57 | if return_type: 58 | code.append(f'{INDENT}:return:{str(return_type)}') 59 | 60 | code.append(f'{INDENT}"""') 61 | return '\n'.join(code) 62 | 63 | 64 | def get_api_version_from_path(path: str): 65 | return path.split('/')[1] 66 | 67 | 68 | def get_major_endpoint_from_path(path: str): 69 | return path.split('/')[2] 70 | 71 | 72 | def get_sub_path(path: str): 73 | return '/'.join(path.split('/')[3:]) 74 | 75 | 76 | def make_ids_uniform_for_path(sub_path): 77 | second_replaced = re.sub(r'\{[^}]*\}', '{sub_id}', sub_path) 78 | return re.sub(r'\{[^}]*\}', '{id}', second_replaced, 1) 79 | 80 | 81 | def create_function_logic(verb: str, path: str, parameters: List[dict], return_type: str = None, function_has_payload: bool = None) -> str: 82 | path_variable_1, path_variable_2 = get_path_variables(path) 83 | 84 | version = get_api_version_from_path(path) 85 | endpoint = get_major_endpoint_from_path(path) 86 | sub_path = get_sub_path(path) 87 | py3cw_parsed_sub_path = make_ids_uniform_for_path(sub_path) 88 | 89 | if version == 'v2': 90 | endpoint = 'smart_trades_v2' 91 | 92 | py3cw_endpoint = PY3CW_API_METHODS.get(endpoint) 93 | 94 | py3cw_entity = '' 95 | if endpoint in PY3CW_API_METHODS: 96 | py3cw_entity = endpoint 97 | 98 | py3cw_action = '' 99 | # print(endpoint) 100 | if py3cw_endpoint: 101 | for k, v in py3cw_endpoint.items(): 102 | if verb.upper() == v[0] and py3cw_parsed_sub_path == v[1]: 103 | py3cw_action = k 104 | 105 | code = list() 106 | 107 | code.append(f'{INDENT}error, data = wrapper.request(') 108 | code.append(f"{INDENT*2}entity='{py3cw_entity}',") 109 | code.append(f"{INDENT*2}action='{py3cw_action}',") 110 | if path_variable_1: 111 | # code.append(f"{INDENT*2}action_id=str({path_variable_1}),") 112 | code.append(f"{INDENT*2}action_id=str(id),") 113 | if path_variable_2: 114 | # code.append(f"{INDENT*2}action_sub_id=str({path_variable_2}),") 115 | code.append(f"{INDENT*2}action_sub_id=str(sub_id),") 116 | if function_has_payload: 117 | code.append(f"{INDENT*2}payload=payload,") 118 | code.append(f"{INDENT})") 119 | if return_type: 120 | if return_type.startswith('List['): 121 | list_element_type = return_type.split('[')[1].split(']')[0] 122 | if list_element_type in {"str", "float", "int", "bool", "dict", "list"}: 123 | code.append(f"{INDENT}return ThreeCommasApiError(error), data") 124 | else: 125 | code.append(f"{INDENT}return ThreeCommasApiError(error), {list_element_type}.of_list(data)") 126 | else: 127 | code.append(f"{INDENT}return ThreeCommasApiError(error), {return_type}(data)") 128 | else: 129 | code.append(f"{INDENT}return ThreeCommasApiError(error), data") 130 | 131 | return '\n'.join(code) 132 | 133 | 134 | def get_str_repr_for_type(parsed_type: type): 135 | if parsed_type in {str, float, int, bool}: 136 | return parsed_type.__name__ 137 | if isinstance(parsed_type, str): 138 | return parsed_type 139 | if parsed_type is datetime.datetime: 140 | return 'datetime.datetime' 141 | 142 | return parsed_type.__name__ 143 | 144 | 145 | def create_models(swaggerdoc: Dict[str, dict]): 146 | swagger_type_2_py_type = { 147 | 'number': 'float', 148 | 'string': 'str', 149 | 'integer': 'int', 150 | 'object': 'dict', 151 | 'array': 'list', 152 | 'boolean': 'bool', 153 | } 154 | proxy_parse_type_mapping = { 155 | float: 'FloatParser', 156 | int: 'IntParser', 157 | datetime.datetime: 'DatetimeParser', 158 | } 159 | superclass = 'ThreeCommasModel' 160 | code = list() 161 | code.append(f'from __future__ import annotations') 162 | code.append('from .models import ThreeCommasModel, FloatParser, IntParser, DatetimeParser, ParsedProxy') 163 | code.append('import datetime') 164 | code.append('from typing import Union') 165 | code.append(f'') 166 | code.append(f'') 167 | 168 | for model_name, model_definition in swaggerdoc.get('definitions').items(): 169 | _parse_map = dict() 170 | _name_proxy = dict() 171 | # proxy_parse_type_parsing_map = dict() 172 | code.append(f'class {model_name}({superclass}):') 173 | # code.append(f'{INDENT}def __init__(self, d: dict = None):') 174 | # code.append(f'{INDENT*2}super().__init__(d)') 175 | for json_attribute_name, attribute_definition in model_definition.get('properties').items(): 176 | swagger_type = attribute_definition.get('type') 177 | py_type = swagger_type_2_py_type.get(swagger_type) 178 | example = attribute_definition.get('example') 179 | model_attribute_name = json_attribute_name.replace('?', '') 180 | parsed_type = None 181 | 182 | if example and isinstance(example, str): 183 | # '2019-01-01T00:00:00.000Z' 184 | # TODO the formats are messed up 185 | pat = re.compile(r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z') 186 | if pat.match(example) is not None: 187 | parsed_type = 'datetime.datetime' 188 | 189 | if parsed_type is None: 190 | model_parsings = PARSING_MAPPING.get(model_name) 191 | if model_parsings and json_attribute_name in model_parsings: 192 | parsed_type = model_parsings.get(json_attribute_name) 193 | 194 | parse_type = proxy_parse_type_mapping.get(parsed_type) 195 | if parse_type: 196 | _parse_map[model_attribute_name] = parse_type 197 | # proxy_parse_type_parsing_map[model_attribute_name] = proxy_type 198 | if json_attribute_name.endswith('?'): 199 | # proxy_parse_type_parsing_map[model_attribute_name] = 'QuestionMarkProxy' 200 | _name_proxy[model_attribute_name] = json_attribute_name 201 | 202 | attribute_type = f'Union[{py_type}, {get_str_repr_for_type(parsed_type)}]' if parse_type else f'{py_type}' 203 | codeline = f'{INDENT}{model_attribute_name}: {attribute_type}' 204 | 205 | code.append(codeline) 206 | 207 | # code.append(f'') 208 | # code.append(f'{INDENT}def parsed(self, parsed: bool) -> {model_name}:') 209 | # code.append(f'{INDENT*2}return ParsedProxy(model=self, parsed=parsed)') 210 | 211 | code.append(f'') 212 | code.append(f'{INDENT}_parse_map = {"{"}') 213 | for model_attribute_name, parse_type in _parse_map.items(): 214 | code.append(f"{INDENT*2}'{model_attribute_name}': {parse_type},") 215 | code.append(f'{INDENT}{"}"}') 216 | 217 | code.append(f'{INDENT}_name_proxy = {"{"}') 218 | for model_attribute_name, json_name in _name_proxy.items(): 219 | code.append(f"{INDENT*2}'{model_attribute_name}': '{json_name}',") 220 | code.append(f'{INDENT}{"}"}') 221 | 222 | code.append(f'') 223 | code.append(f'') 224 | code_str = '\n'.join(code) 225 | 226 | with open(MODEL_FILE_NAME, 'w') as f: 227 | f.write(code_str) 228 | 229 | 230 | def generate(): 231 | with open('3commas_swaggerdoc_2022_01_24.json', 'r') as f: 232 | swaggerdoc: Dict[str, dict] = json.loads(f.read()) 233 | structured_code: Dict[str, list] = defaultdict(list) 234 | 235 | for path, definition in swaggerdoc.get('paths').items(): 236 | split: list = path.split('/') 237 | 238 | http_verbs = list(definition.keys()) 239 | endpoint_list = list(filter(lambda e: '{' not in e, split))[1:] 240 | version = endpoint_list.pop(0) 241 | endpoint = endpoint_list.pop(0) 242 | sub_endpoint = '_'.join(endpoint_list) if endpoint_list else '' 243 | path_variable_1, path_variable_2 = get_path_variables(path) 244 | 245 | # function_parameters = path_variable_1 or '' 246 | function_parameters = 'id' if path_variable_1 else '' 247 | if path_variable_2: 248 | # function_parameters += f', {path_variable_2}' 249 | function_parameters += f', sub_id' 250 | 251 | for verb in http_verbs: 252 | # function_has_payload = endpoint_consumes(verb, path) 253 | function_has_payload = True 254 | if function_has_payload and "payload: dict" not in function_parameters: 255 | function_parameters += f'{", " if function_parameters else ""}payload: dict = None' 256 | 257 | description = definition.get(verb).get('description') 258 | # operationId = definition.get(verb).get('operationId') 259 | parameters = definition.get(verb).get('parameters') 260 | 261 | function_name = f'{verb}{"_" + sub_endpoint if sub_endpoint else ""}{"_by_id" if path_variable_1 else ""}' 262 | return_type = endpoint_returns(verb, path) 263 | 264 | code = list() 265 | function_logic = create_function_logic(verb, path, parameters, return_type, function_has_payload) 266 | 267 | endpoint_found_in_py3cw = True 268 | if '' in function_logic or '' in function_logic: 269 | endpoint_found_in_py3cw = False 270 | if not endpoint_found_in_py3cw: 271 | code.append("''' This endpoint was not present in the py3cw module") 272 | 273 | return_type_statement = '' 274 | if return_type: 275 | return_type_statement = f' -> Tuple[ThreeCommasApiError, {return_type}]' 276 | 277 | code.append(f'@logged') 278 | code.append(f'@with_py3cw') 279 | code.append(f'def {function_name}({function_parameters}){return_type_statement}:') 280 | docstring = create_docstring(verb, path, parameters, description) 281 | if docstring: 282 | code.append(docstring) 283 | code.append(function_logic) 284 | 285 | if not endpoint_found_in_py3cw: 286 | code.append("'''") 287 | 288 | code.append('') 289 | code.append('') 290 | code.append('') 291 | 292 | structured_code[f'{version}/{endpoint}'].append('\n'.join(code)) 293 | 294 | create_models(swaggerdoc) 295 | 296 | for k, v in structured_code.items(): 297 | imports = list() 298 | imports.append("from py3cw.request import Py3CW") 299 | imports.append("from ...model import *") 300 | imports.append("from ...error import ThreeCommasApiError") 301 | imports.append("from typing import Tuple, List") 302 | imports.append("import logging") 303 | imports.append("from ...sys_utils import logged, with_py3cw, Py3cwClosure") 304 | imports.append("") 305 | imports.append("") 306 | imports.append("logger = logging.getLogger(__name__)") 307 | imports.append("wrapper: Py3cwClosure = None") 308 | imports.append("") 309 | imports.append("") 310 | imports.append("") 311 | v.insert(0, '\n'.join(imports)) 312 | 313 | for path, c in structured_code.items(): 314 | full_path = f'{PARENT_FOLDER_NAME}/{path}.py' 315 | if not os.path.exists(full_path): 316 | os.makedirs(os.path.dirname(full_path), exist_ok=True) 317 | with open(full_path, 'w') as f2: 318 | f2.write(''.join(c)) 319 | 320 | with open(f'{PARENT_FOLDER_NAME}/__init__.py', 'w') as f3: 321 | f3.write('from . import ver1, v2') 322 | f3.write('\n') 323 | with open(f'{PARENT_FOLDER_NAME}/v2/__init__.py', 'w') as f4: 324 | f4.write('from . import smart_trades') 325 | f4.write('\n') 326 | with open(f'{PARENT_FOLDER_NAME}/ver1/__init__.py', 'w') as f5: 327 | f5.write('from . import accounts, bots, deals, grid_bots, marketplace, users') 328 | f5.write('\n') 329 | 330 | 331 | if __name__ == '__main__': 332 | generate() 333 | -------------------------------------------------------------------------------- /type_generators/auto_api_from_swaggerdoc_2.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | from symbol import parameters 3 | from typing import List 4 | 5 | 6 | @dataclasses.dataclass 7 | class FunctionParameter: 8 | name: str 9 | type: str 10 | default_value: str 11 | 12 | 13 | @dataclasses.dataclass 14 | class ThreeCommasApiFunction: 15 | INDENT = ' ' * 4 16 | verb: str 17 | path: str 18 | swagger_description: str 19 | parameters: List[FunctionParameter] 20 | return_type: str 21 | docstring: str 22 | entity: str 23 | action: str 24 | action_id_name: str 25 | sub_id_name: str 26 | 27 | 28 | # ideas 29 | # api.ver1.accounts.by_id.get(id=) 30 | # api.ver1.accounts.get() # use __call__() 31 | # api.ver1.accounts.get.by_id_and_sub_id(id=, sub_id=) 32 | # api.ver1.accounts.get.by_id(id=, sub_id=) 33 | # api.get.ver1.accounts.by_id(id=, sub_id=) # not preffered, better to have all account endpoint clustered in one file 34 | # api.get.ver1.accounts.remove.post(id=) 35 | # api.get.ver1.accounts.get(id=) 36 | # api.get.ver1.accounts.get() 37 | 38 | -------------------------------------------------------------------------------- /type_generators/enum_generator.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | INDENT = ' ' 4 | 5 | 6 | class EnumProperties: 7 | def __init__(self, name, values): 8 | self.name = name 9 | self.values = values 10 | 11 | 12 | enums_list = [ 13 | EnumProperties('DealStatus', 14 | values=['active', 'finished', 'completed', 'cancelled', 'failed']), 15 | EnumProperties('BotScope', 16 | values=['enabled', 'disabled']), 17 | EnumProperties('Mode', 18 | values=['paper', 'real']), 19 | EnumProperties('AccountMarketCode', 20 | values=['paper_trading', 'binance', 'bitfinex', 'bitstamp', 'bittrex', 'gdax', 'gemini', 21 | 'huobi', 'kucoin', 'okex', 'poloniex', 'bitmex', 'kraken', 'gate_io', 'binance_margin', 22 | 'bybit', 'binance_us', 'binance_futures', 'deribit', 'ftx', 'ftx_us', 'bybit_usdt_perpetual', 23 | 'binance_futures_coin', 'bybit_spot', 'gate_io_usdt_perpetual', 'gate_io_btc_perpetual', 24 | 'ethereumwallet', 'trx'] 25 | ) 26 | 27 | ] 28 | 29 | 30 | def generate_enums(): 31 | with open('../src/three_commas/model/generated_enums.py', 'w') as f: 32 | file_buffer = list() 33 | # imports 34 | file_buffer.append('from .other_enums import AbstractStringEnum') 35 | 36 | for ep in enums_list: 37 | file_buffer.append('') 38 | file_buffer.append('') 39 | file_buffer.append(f'class {ep.name}(AbstractStringEnum):') 40 | for value in ep.values: 41 | file_buffer.append(f"{INDENT}{value.upper()} = '{value}'") 42 | file_buffer.extend(create_enum_functions(ep)) 43 | 44 | file_buffer.append('') 45 | 46 | generated_code = '\n'.join(file_buffer) 47 | f.write(generated_code) 48 | 49 | 50 | def create_enum_functions(ep: EnumProperties): 51 | file_buffer = list() 52 | 53 | for value in ep.values: 54 | file_buffer.append('') 55 | file_buffer.append(f"{INDENT}def is_{value}(self) -> bool:") 56 | file_buffer.append(f"{INDENT*2}return self == {ep.name}.{value.upper()}") 57 | 58 | return file_buffer 59 | 60 | 61 | if __name__ == '__main__': 62 | generate_enums() 63 | -------------------------------------------------------------------------------- /type_generators/parsing_and_return_mapping.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import * 3 | 4 | 5 | # {endpoint_path : str_class_to_parse_to} 6 | ENDPOINT_PRODUCTION_MAP = { 7 | 'GET /ver1/bots/{bot_id}/show': 'BotEntity', 8 | 'GET /ver1/bots': 'List[BotEntity]', 9 | 'GET /v2/smart_trades': 'List[SmartTradeV2Entity]', 10 | 'GET /v2/smart_trades/{id}': 'SmartTradeV2Entity', 11 | 'POST /v2/smart_trades': 'SmartTradeV2Entity', 12 | 'GET /ver1/accounts/market_pairs': 'List[str]', 13 | 'GET /ver1/accounts/{account_id}': 'AccountEntity', 14 | 'POST /ver1/accounts/new': 'AccountEntity', 15 | } 16 | 17 | 18 | def endpoint_returns(verb: str, endpoint: str): 19 | return ENDPOINT_PRODUCTION_MAP.get(f'{verb.upper()} {endpoint}') 20 | 21 | 22 | # {endpoint_path : str_class_to_consume} 23 | ENDPOINT_CONSUMPTION_MAP = { 24 | 'POST /v2/smart_trades': 'SmartTradeV2Entity', 25 | 'GET /ver1/accounts/market_pairs': 'dict', 26 | } 27 | 28 | 29 | def endpoint_consumes(verb: str, endpoint: str): 30 | return ENDPOINT_CONSUMPTION_MAP.get(f'{verb.upper()} {endpoint}') 31 | 32 | FIELD_INITIAL_TYPE_MAPPINGS = { 33 | 'AccountEntity': { 34 | 'supported_market_types': List[str] # swaggerdoc states "str" 35 | } 36 | } 37 | 38 | 39 | # {name_of_model : {name_of_attr: parse_to}} 40 | PARSING_MAPPING = { 41 | 'DealEntity': { 42 | 'created_at': datetime.datetime, 43 | 'updated_at': datetime.datetime, 44 | 'closed_at': datetime.datetime, 45 | 'take_profit': float, 46 | 'base_order_volume': float, 47 | 'safety_order_volume': float, 48 | 'safety_order_step_percentage': float, 49 | 'bought_amount': float, 50 | 'bought_volume': float, 51 | 'bought_average_price': float, 52 | 'base_order_average_price': float, 53 | 'sold_amount': float, 54 | 'sold_volume': float, 55 | 'sold_average_price': float, 56 | 'final_profit': float, 57 | 'martingale_coefficient': float, 58 | 'martingale_volume_coefficient': float, 59 | 'martingale_step_coefficient': float, 60 | 'stop_loss_percentage': float, 61 | 'current_price': float, 62 | 'take_profit_price': float, 63 | 'final_profit_percentage': float, 64 | 'actual_profit_percentage': float, 65 | 'usd_final_profit': float, 66 | 'actual_profit': float, 67 | 'actual_usd_profit': float, 68 | 'reserved_base_coin': float, 69 | 'reserved_second_coin': float, 70 | 'trailing_deviation': float, 71 | 'trailing_max_price': float, 72 | 'reserved_quote_funds': float, 73 | 'reserved_base_funds': float, 74 | }, 75 | 'BotEntity': { 76 | 'created_at': datetime.datetime, 77 | 'updated_at': datetime.datetime, 78 | 'take_profit': float, 79 | 'base_order_volume': float, 80 | 'safety_order_volume': float, 81 | 'safety_order_step_percentage': float, 82 | 'martingale_volume_coefficient': float, 83 | 'martingale_step_coefficient': float, 84 | 'stop_loss_percentage': float, 85 | 'btc_price_limit': float, 86 | 'min_volume_btc_24h': float, 87 | 'trailing_deviation': float, 88 | 'finished_deals_profit_usd': float, 89 | 'finished_deals_count': int, 90 | 'active_deals_usd_profit': float, 91 | }, 92 | 'DealMarketOrderEntity': { 93 | 'order_id': int, 94 | 'created_at': datetime.datetime, 95 | 'updated_at': datetime.datetime, 96 | 'quantity': float, 97 | 'quantity_remaining': float, 98 | 'total': float, 99 | 'rate': float, 100 | 'average_price': float, 101 | }, 102 | 'PieChartDataElement': { 103 | 'coinmarketcapid': int, 104 | 'btc_value': float, 105 | 'usd_value': float, 106 | }, 107 | 'AccountEntity': { 108 | 'created_at': datetime.datetime, 109 | 'updated_at': datetime.datetime, 110 | 'btc_amount': float, 111 | 'usd_amount': float, 112 | 'day_profit_btc': float, 113 | 'day_profit_usd': float, 114 | 'day_profit_btc_percentage': float, 115 | 'day_profit_usd_percentage': float, 116 | 'btc_profit': float, 117 | 'usd_profit': float, 118 | 'usd_profit_percentage': float, 119 | 'btc_profit_percentage': float, 120 | 'total_btc_profit': float, 121 | 'total_usd_profit': float, 122 | }, 123 | 'SmartTradeV2Trade': { 124 | 'average_price': float, 125 | 'initial_amount': float, 126 | 'initial_total': float, 127 | 'realised_amount': float, 128 | 'realised_total': float, 129 | 'created_at': datetime.datetime, 130 | 'updated_at': datetime.datetime, 131 | 'realised_percentage': float, 132 | 'initial_price': float, 133 | 'realised_price': float, 134 | } 135 | } 136 | --------------------------------------------------------------------------------