├── .circleci └── config.yml ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── help.md └── workflows │ ├── codeql-analysis.yml │ └── python-publish.yml ├── .gitignore ├── .pyup.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.rst ├── SECURITY.md ├── requirements.txt ├── setup.cfg ├── setup.py └── upbit ├── __init__.py ├── authentication.py ├── client.py ├── models.py ├── pkginfo.py ├── utils.py └── websocket.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Use the latest 2.1 version of CircleCI pipeline process engine. 2 | # See: https://circleci.com/docs/2.0/configuration-reference 3 | version: 2.1 4 | 5 | # Define a job to be invoked later in a workflow. 6 | # See: https://circleci.com/docs/2.0/configuration-reference/#jobs 7 | jobs: 8 | say-hello: 9 | # Specify the execution environment. You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub. 10 | # See: https://circleci.com/docs/2.0/configuration-reference/#docker-machine-macos-windows-executor 11 | docker: 12 | - image: cimg/base:stable 13 | # Add steps to the job 14 | # See: https://circleci.com/docs/2.0/configuration-reference/#steps 15 | steps: 16 | - checkout 17 | - run: 18 | name: "Upbit Client" 19 | command: "echo Welcome to Yu Jhin SDKs for Python" 20 | 21 | # Invoke jobs via workflows 22 | # See: https://circleci.com/docs/2.0/configuration-reference/#workflows 23 | workflows: 24 | say-hello-workflow: 25 | jobs: 26 | - say-hello 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/help.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Help 3 | about: Ask for help here. 4 | title: '' 5 | labels: help wanted 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '43 9 * * 0' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /.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 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: '3.x' 22 | 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install setuptools wheel twine 27 | 28 | - name: Build and publish 29 | env: 30 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 31 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 32 | run: | 33 | python setup.py sdist bdist_wheel 34 | twine check dist/* 35 | twine upload dist/* 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # vscode settings 132 | .vscode/ 133 | 134 | # pycharm settings 135 | .idea/ -------------------------------------------------------------------------------- /.pyup.yml: -------------------------------------------------------------------------------- 1 | # autogenerated pyup.io config file 2 | # see https://pyup.io/docs/configuration/ for all available options 3 | 4 | schedule: '' 5 | update: false 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | ujhin942@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 uJhin (https://github.com/uJhin) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://raw.githubusercontent.com/uJhin/upbit-client/main/logo/logo.png 2 | :align: center 3 | 4 | - `Base Repository `_ 5 | - `Python Upbit Client Repository `_ 6 | 7 | Upbit OPEN API Client 8 | ###################### 9 | - @Author: `uJhin `_ 10 | - @GitHub: https://github.com/uJhin/upbit-client/ 11 | - @Official Documents: https://ujhin.github.io/upbit-client-docs/ 12 | 13 | Install 14 | ******* 15 | - pip command 16 | 17 | .. code:: console 18 | 19 | pip install upbit-client 20 | 21 | - git command 22 | 23 | .. code:: console 24 | 25 | git clone https://github.com/uJhin/python-upbit-client.git 26 | 27 | 28 | Quick Start 29 | *************** 30 | 31 | REST Client 32 | =========== 33 | 34 | - Check Your API Keys 35 | 36 | .. code:: python 37 | 38 | # /v1/api_keys 39 | 40 | from upbit.client import Upbit 41 | 42 | access_key = "Your Access Key" 43 | secret_key = "Your Secret Key" 44 | 45 | client = Upbit(access_key, secret_key) 46 | api_keys = client.APIKey.APIKey_info() 47 | print(api_keys['result']) 48 | 49 | 50 | - Buy Currency 51 | 52 | .. code:: python 53 | 54 | # /v1/orders 55 | 56 | from upbit.client import Upbit 57 | 58 | access_key = "Your Access Key" 59 | secret_key = "Your Secret Key" 60 | 61 | client = Upbit(access_key, secret_key) 62 | order = client.Order.Order_new( 63 | market='KRW-BTC', 64 | side='bid', 65 | volume='0.1', 66 | price='3000000', 67 | ord_type='limit' 68 | ) 69 | print(order['result']) 70 | 71 | 72 | - Sell Currency 73 | 74 | .. code:: python 75 | 76 | # /v1/orders 77 | 78 | from upbit.client import Upbit 79 | 80 | access_key = "Your Access Key" 81 | secret_key = "Your Secret Key" 82 | 83 | client = Upbit(access_key, secret_key) 84 | order = client.Order.Order_new( 85 | market='KRW-BTC', 86 | side='ask', 87 | volume='0.1', 88 | price='3000000', 89 | ord_type='limit' 90 | ) 91 | print(order['result']) 92 | 93 | WebSocket Client 94 | ================ 95 | 96 | - Get Real-Time Ticker 97 | 98 | .. code:: python 99 | 100 | # Using WebSocket 101 | 102 | import json 103 | import asyncio 104 | 105 | from upbit.websocket import UpbitWebSocket 106 | 107 | 108 | # Definition async function 109 | async def ticker(sock, payload): 110 | async with sock as conn: 111 | await conn.send(payload) 112 | while True: 113 | recv = await conn.recv() 114 | data = recv.decode('utf8') 115 | result = json.loads(data) 116 | print(result) 117 | 118 | 119 | sock = UpbitWebSocket() 120 | 121 | currencies = ['KRW-BTC', 'KRW-ETH'] 122 | type_field = sock.generate_type_field( 123 | type='ticker', 124 | codes=currencies, 125 | ) 126 | payload = sock.generate_payload( 127 | type_fields=[type_field] 128 | ) 129 | 130 | event_loop = asyncio.get_event_loop() 131 | event_loop.run_until_complete( ticker(sock, payload) ) 132 | 133 | Donation 134 | ********* 135 | .. image:: https://img.shields.io/badge/BTC-3NVw2seiTQddGQwc1apqudKxuTqebpyL3s-blue?style=flat-square&logo=bitcoin 136 | :alt: uJhin's BTC 137 | .. image:: https://img.shields.io/badge/ETH-0x60dd373f59862d9df776596889b997e24bee42eb-blue?style=flat-square&logo=ethereum 138 | :alt: uJhin's ETH 139 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | arrow==1.2.3 2 | attrs==23.1.0 3 | bravado==11.0.3 4 | bravado-core==5.17.1 5 | certifi==2024.7.4 6 | charset-normalizer==3.1.0 7 | fqdn==1.5.1 8 | idna==3.7 9 | isoduration==20.11.0 10 | jsonpointer==2.3 11 | jsonref==1.1.0 12 | jsonschema==4.17.3 13 | monotonic==1.6 14 | msgpack==1.0.5 15 | PyJWT==2.7.0 16 | pyrsistent==0.19.3 17 | python-dateutil==2.8.2 18 | pytz==2023.3 19 | PyYAML==6.0 20 | requests==2.32.0 21 | rfc3339-validator==0.1.4 22 | rfc3987==1.3.8 23 | simplejson==3.19.1 24 | six==1.16.0 25 | swagger-spec-validator==3.0.3 26 | typing_extensions==4.5.0 27 | upbit-client==1.3.2.0 28 | uri-template==1.2.0 29 | urllib3==2.2.2 30 | webcolors==1.13 31 | websockets==11.0.3 32 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | 2 | [metadata] 3 | description-file = README.rst 4 | 5 | [wheel] 6 | universal = True 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | 2 | from setuptools import setup 3 | from setuptools import find_packages 4 | 5 | from upbit import pkginfo 6 | 7 | 8 | with open('README.rst') as readme: 9 | long_description = readme.read() 10 | 11 | 12 | setup( 13 | name = pkginfo.PACKAGE_NAME, 14 | version = pkginfo.CURRENT_VERSION, 15 | packages = find_packages(), 16 | install_requires = [ 17 | 'bravado>=11.0.2' 18 | , 'PyJWT>=2.4.0' 19 | , 'websockets>=10.3' 20 | ], 21 | extras_require = { 22 | 'fido': [ 23 | 'fido>=4.2.1' 24 | ] 25 | }, 26 | python_requires = '>=3.8', 27 | classifiers = [ 28 | 'Programming Language :: Python :: 3.8' 29 | , 'Programming Language :: Python :: 3.9' 30 | , 'Programming Language :: Python :: 3.10' 31 | , 'Programming Language :: Python :: 3.11' 32 | , 'Programming Language :: Python :: 3.12' 33 | ], 34 | keywords = [ 35 | 'Upbit' 36 | , 'upbit' 37 | , 'upbit-client' 38 | , 'Upbit-Client' 39 | , 'Upbit_client' 40 | , 'Upbit-api-connector' 41 | , 'upbit-api-connector' 42 | , 'Upbit_api_connector' 43 | , 'upbit_api_connector' 44 | ], 45 | url = 'https://github.com/uJhin/upbit-client', 46 | download_url = 'https://github.com/uJhin/upbit-client/releases', 47 | license = 'MIT License', 48 | author = 'ujhin', 49 | author_email = 'ujhin942@gmail.com', 50 | description = 'Upbit OPEN API Client', 51 | long_description_content_type = 'text/x-rst', 52 | long_description = long_description 53 | ) 54 | -------------------------------------------------------------------------------- /upbit/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | [Upbit Client] 3 | 4 | Please read the official Upbit Client document. 5 | Documents: https://ujhin.github.io/upbit-client-docs/ 6 | 7 | - Upbit OPEN API Version: 1.3.4 8 | - Author: ujhin 9 | - Email: ujhin942@gmail.com 10 | - GitHub: https://github.com/uJhin 11 | - Official OPEN API Documents: https://docs.upbit.com 12 | - Official Support Email: open-api@upbit.com 13 | """ 14 | 15 | from . import pkginfo 16 | 17 | 18 | __all__ = [ "client", "websocket" ] 19 | __version__ = pkginfo.CURRENT_VERSION 20 | -------------------------------------------------------------------------------- /upbit/authentication.py: -------------------------------------------------------------------------------- 1 | 2 | from bravado.requests_client import Authenticator 3 | 4 | 5 | class APIKeyAuthenticator(Authenticator): 6 | 7 | import jwt 8 | import uuid 9 | 10 | from urllib.parse import urlencode 11 | 12 | 13 | MAPPER = "swg_mapper.json" 14 | QUERY_PARAMS = ( "uuids", "txids", "identifiers", "states" ) 15 | 16 | 17 | def __init__( 18 | self, 19 | host : str, 20 | access_key: str, 21 | secret_key: str 22 | ): 23 | 24 | super(APIKeyAuthenticator, self).__init__(host) 25 | 26 | self.host = host 27 | self.access_key = access_key 28 | self.secret_key = secret_key 29 | 30 | self.urlencode = APIKeyAuthenticator.urlencode 31 | self.jwt = APIKeyAuthenticator.jwt 32 | self.algorithms = self.jwt.algorithms 33 | self.algo = "HS512" 34 | 35 | 36 | def matches(self, url): 37 | return self.MAPPER not in url 38 | 39 | 40 | def apply(self, request): 41 | payload = self.generate_payload(request) 42 | 43 | request.headers["User-Agent" ] = "ujhin's Upbit SDKs" 44 | request.headers["Accept-Encoding"] = "gzip, deflate" 45 | request.headers["Accept" ] = "*/*" 46 | request.headers["Connection" ] = "keep-alive" 47 | request.headers["Authorization" ] = payload 48 | return request 49 | 50 | 51 | def generate_payload(self, request): 52 | params = request.params 53 | data = request.data 54 | 55 | payload = { 56 | 'access_key': self.access_key, 57 | 'nonce': str(self.uuid.uuid4()) 58 | } 59 | if isinstance(data, dict): 60 | params.update(data) 61 | if params: 62 | query = self.generate_query(params) 63 | 64 | sha512 = self.get_hash_algo(self.algo) 65 | sha512.update(query.encode()) 66 | query_hash = sha512.hexdigest() 67 | 68 | payload["query_hash" ] = query_hash 69 | payload["query_hash_alg"] = sha512.name 70 | 71 | jwt_token = self.jwt.encode(payload, self.secret_key, algorithm=self.algo) 72 | authorize_token = f"Bearer {jwt_token}" 73 | return authorize_token 74 | 75 | 76 | def generate_query(self, params): 77 | query = self.urlencode({ 78 | k: v 79 | for k, v in params.items() 80 | if k.lower() not in self.QUERY_PARAMS 81 | }) 82 | for query_param in self.QUERY_PARAMS: 83 | if params.get(query_param): 84 | param = params.pop(query_param) 85 | params[f"{query_param}[]"] = param 86 | query_params = '&'.join([ 87 | f"{query_param}[]={q}" 88 | for q in param 89 | ]) 90 | query = f"{query}&{query_params}" if query else query_params 91 | return query 92 | 93 | 94 | def get_hash_algo(self, algo): 95 | algorithms = self.algorithms.get_default_algorithms() 96 | algo = algorithms.get(algo, "HS512") 97 | hash_algo = algo.hash_alg() 98 | return hash_algo 99 | -------------------------------------------------------------------------------- /upbit/client.py: -------------------------------------------------------------------------------- 1 | 2 | from . import models 3 | 4 | 5 | class Upbit: 6 | """ 7 | Upbit Client 8 | Please read the official Upbit Client document. 9 | Documents: https://ujhin.github.io/upbit-client-docs/ 10 | 11 | - Base URL: https://api.upbit.com 12 | - Base Path: /v1 13 | - Upbit OPEN API Version: 1.3.4 14 | - Author: ujhin 15 | - Email: ujhin942@gmail.com 16 | - GitHub: https://github.com/uJhin 17 | - Official OPEN API Documents: https://docs.upbit.com 18 | - Official Support Email: open-api@upbit.com 19 | """ 20 | 21 | def __init__( 22 | self, 23 | access_key: str = None, 24 | secret_key: str = None, 25 | **kwargs 26 | ): 27 | 28 | self.__client = models.ClientModel( 29 | access_key=access_key, 30 | secret_key=secret_key, 31 | **kwargs 32 | ).SWGClient 33 | 34 | self.__APIKey = models.APIKey(self.__client) 35 | self.__Account = models.Account(self.__client) 36 | self.__Candle = models.Candle(self.__client) 37 | self.__Deposit = models.Deposit(self.__client) 38 | self.__Market = models.Market(self.__client) 39 | self.__Order = models.Order(self.__client) 40 | self.__Trade = models.Trade(self.__client) 41 | self.__Withdraw = models.Withdraw(self.__client) 42 | 43 | 44 | @property 45 | def APIKey(self): 46 | return self.__APIKey 47 | @property 48 | def Account(self): 49 | return self.__Account 50 | @property 51 | def Candle(self): 52 | return self.__Candle 53 | @property 54 | def Deposit(self): 55 | return self.__Deposit 56 | @property 57 | def Market(self): 58 | return self.__Market 59 | @property 60 | def Order(self): 61 | return self.__Order 62 | @property 63 | def Trade(self): 64 | return self.__Trade 65 | @property 66 | def Withdraw(self): 67 | return self.__Withdraw 68 | 69 | 70 | def __str__(self): 71 | return self.__repr__() 72 | 73 | def __repr__(self): 74 | return f"UpbitClient({models.HOST})" 75 | -------------------------------------------------------------------------------- /upbit/models.py: -------------------------------------------------------------------------------- 1 | 2 | from bravado.requests_client import RequestsClient 3 | from bravado.client import SwaggerClient 4 | 5 | from .authentication import APIKeyAuthenticator 6 | from .utils import HTTPFutureExtractor 7 | 8 | 9 | HOST = "https://api.upbit.com" 10 | SPEC_URI = "https://raw.githubusercontent.com/uJhin/upbit-client/main/mapper/swg_mapper.json" 11 | 12 | 13 | class ClientModel: 14 | """ 15 | Client Base Model 16 | """ 17 | 18 | def __init__( 19 | self, 20 | access_key: str = None, 21 | secret_key: str = None, 22 | **kwargs 23 | ): 24 | 25 | arg_config = kwargs.get("config") 26 | arg_spec_uri = kwargs.get("spec_uri") 27 | config = { 28 | "also_return_response": False, 29 | "validate_responses" : False, 30 | "use_models" : False, 31 | "host" : HOST 32 | } if not arg_config else arg_config 33 | spec_uri = SPEC_URI if not arg_spec_uri else arg_spec_uri 34 | 35 | if access_key and secret_key: 36 | 37 | request_client = RequestsClient() 38 | request_client.authenticator = APIKeyAuthenticator( 39 | host=config["host"], 40 | access_key=access_key, 41 | secret_key=secret_key 42 | ) 43 | 44 | self.__client = SwaggerClient.from_url( 45 | spec_url=spec_uri, 46 | http_client=request_client, 47 | config=config 48 | ) 49 | 50 | else: 51 | 52 | self.__client = SwaggerClient.from_url( 53 | spec_url=spec_uri, 54 | config=config 55 | ) 56 | 57 | @property 58 | def SWGClient(self) -> SwaggerClient: 59 | return self.__client 60 | 61 | 62 | class APIKey: 63 | """ 64 | API Key 65 | """ 66 | 67 | def __init__(self, client): 68 | self.__client = client 69 | 70 | def APIKey_info(self) -> dict: 71 | """ 72 | [GET] API 키 리스트 조회 73 | 74 | ## API 키 목록 및 만료 일자를 조회합니다. 75 | """ 76 | 77 | future = self.__client.APIKey.APIKey_info() 78 | return HTTPFutureExtractor.future_extraction(future) 79 | 80 | 81 | class Account: 82 | """ 83 | 계좌 84 | """ 85 | 86 | def __init__(self, client): 87 | self.__client = client 88 | 89 | def Account_info(self) -> dict: 90 | """ 91 | [GET] 전체 계좌 조회 92 | 93 | ## 내가 보유한 자산 리스트를 보여줍니다. 94 | """ 95 | 96 | future = self.__client.Account.Account_info() 97 | return HTTPFutureExtractor.future_extraction(future) 98 | 99 | def Account_wallet(self) -> dict: 100 | """ 101 | [GET] 입출금 현황 102 | 103 | ## 입출금 현황 및 블록 상태를 조회합니다. 104 | 105 | [NOTE] 입출금 현황 데이터는 실제 서비스 상태와 다를 수 있습니다. 106 | 107 | 입출금 현황 API에서 제공하는 입출금 상태, 블록 상태 정보는 수 분 정도 지연되어 반영될 수 있습니다. 108 | 본 API는 참고용으로만 사용하시길 바라며 실제 입출금을 수행하기 전에는 반드시 업비트 공지사항 및 입출금 현황 페이지를 참고해주시기 바랍니다. 109 | """ 110 | 111 | future = self.__client.Account.Account_wallet() 112 | return HTTPFutureExtractor.future_extraction(future) 113 | 114 | 115 | class Candle: 116 | """ 117 | 캔들; 봉 118 | """ 119 | 120 | def __init__(self, client): 121 | self.__client = client 122 | 123 | def Candle_minutes(self, **kwargs) -> dict: 124 | """ 125 | [GET] 시세 캔들 조회 (분 단위) 126 | 127 | ## 분(Minute) 캔들 128 | 129 | :param unit: 분 단위. 130 | 가능한 값: 1, 3, 5, 15, 10, 30, 60, 240 131 | :type unit: int 132 | 133 | :param market: 마켓 코드 (ex. KRW-BTC) 134 | :type market: str 135 | 136 | :param to: 마지막 캔들 시각 (exclusive). 137 | 포맷: `yyyy-MM-dd'T'HH:mm:ssXXX` or `yyyy-MM-dd HH:mm:ss`. 138 | 비워서 요청 시 가장 최근 캔들 (optional) 139 | :type to: str 140 | 141 | :param count: 캔들 개수 (최대 200개까지 요청 가능) (optional) 142 | :type count: int 143 | """ 144 | 145 | future = self.__client.Candle.Candle_minutes(**kwargs) 146 | return HTTPFutureExtractor.future_extraction(future) 147 | 148 | def Candle_days(self, **kwargs) -> dict: 149 | """ 150 | [GET] 시세 캔들 조회 (일 단위) 151 | 152 | ## 일(Day) 캔들 153 | 154 | :param market: 마켓 코드 (ex. KRW-BTC) 155 | :type market: str 156 | 157 | :param to: 마지막 캔들 시각 (exclusive). 158 | 포맷: `yyyy-MM-dd'T'HH:mm:ssXXX` or `yyyy-MM-dd HH:mm:ss`. 159 | 비워서 요청 시 가장 최근 캔들 (optional) 160 | :type to: str 161 | 162 | :param count: 캔들 개수 (optional) 163 | :type count: int 164 | 165 | :param convertingPriceUnit: 종가 환산 화폐 단위 (생략 가능, KRW로 명시할 시 원화 환산 가격을 반환.) 166 | `convertingPriceUnit` 파라미터의 경우, 원화 마켓이 아닌 다른 마켓(ex. BTC, ETH)의 일봉 요청 시 167 | 종가를 명시된 파라미터 값으로 환산해 `converted_trade_price` 필드에 추가하여 반환합니다. 168 | 현재는 원화(`KRW`) 로 변환하는 기능만 제공하며 추후 기능을 확장할 수 있습니다. (Default: KRW) (optional) 169 | :type convertingPriceUnit: str 170 | """ 171 | 172 | future = self.__client.Candle.Candle_days(**kwargs) 173 | return HTTPFutureExtractor.future_extraction(future) 174 | 175 | def Candle_weeks(self, **kwargs) -> dict: 176 | """ 177 | [GET] 시세 캔들 조회 (주 단위) 178 | 179 | ## 주(Week) 캔들 180 | 181 | :param market: 마켓 코드 (ex. KRW-BTC) 182 | :type market: str 183 | 184 | :param to: 마지막 캔들 시각 (exclusive). 185 | 포맷 : `yyyy-MM-dd'T'HH:mm:ssXXX` or `yyyy-MM-dd HH:mm:ss`. 186 | 비워서 요청 시 가장 최근 캔들 (optional) 187 | :type to: str 188 | 189 | :param count: 캔들 개수 (optional) 190 | :type count: int 191 | """ 192 | 193 | future = self.__client.Candle.Candle_weeks(**kwargs) 194 | return HTTPFutureExtractor.future_extraction(future) 195 | 196 | def Candle_month(self, **kwargs) -> dict: 197 | """ 198 | [GET] 시세 캔들 조회 (월 단위) 199 | 200 | ## 월(Month) 캔들 201 | 202 | :param market: 마켓 코드 (ex. KRW-BTC) 203 | :type market: str 204 | 205 | :param to: 마지막 캔들 시각 (exclusive). 206 | 포맷: `yyyy-MM-dd'T'HH:mm:ssXXX` or `yyyy-MM-dd HH:mm:ss`. 207 | 비워서 요청 시 가장 최근 캔들 (optional) 208 | :type to: str 209 | 210 | :param count: 캔들 개수 (optional) 211 | :type count: int 212 | """ 213 | 214 | future = self.__client.Candle.Candle_month(**kwargs) 215 | return HTTPFutureExtractor.future_extraction(future) 216 | 217 | 218 | class Deposit: 219 | """ 220 | 입금 221 | """ 222 | 223 | def __init__(self, client): 224 | self.__client = client 225 | 226 | def Deposit_coin_address(self, **kwargs) -> dict: 227 | """ 228 | [GET] 개별 입금 주소 조회 229 | 230 | ## 개별 입금 주소 조회 231 | 232 | [NOTE] 입금 주소 조회 요청 API 유의사항 233 | 234 | 입금 주소 생성 요청 이후 아직 발급되지 않은 상태일 경우 `deposit_address` 가 `null` 일 수 있습니다. 235 | * 네트워크가 일치하지 않는 경우 정상 입출금이 진행되지 않을 수 있습니다. 236 | 사용하는 주소와 네트워크가 정확히 일치하는지 재확인 후 이용을 부탁드립니다. 237 | 238 | :param currency: Currency 코드 239 | :type currency: str 240 | 241 | :param net_type: 입금 네트워크 242 | :type net_type: str 243 | """ 244 | 245 | future = self.__client.Deposit.Deposit_coin_address(**kwargs) 246 | return HTTPFutureExtractor.future_extraction(future) 247 | 248 | def Deposit_coin_addresses(self) -> dict: 249 | """ 250 | [GET] 전체 입금 주소 조회 251 | 252 | ## 내가 보유한 자산 리스트를 보여줍니다. 253 | [NOTE] 입금 주소 조회 요청 API 유의사항 254 | 255 | 입금 주소 생성 요청 이후 아직 발급되지 않은 상태일 경우 `deposit_address` 가 `null` 일 수 있습니다. 256 | """ 257 | 258 | future = self.__client.Deposit.Deposit_coin_addresses() 259 | return HTTPFutureExtractor.future_extraction(future) 260 | 261 | def Deposit_generate_coin_address(self, **kwargs) -> dict: 262 | """ 263 | [POST] 입금 주소 생성 요청 264 | 265 | 입금 주소 생성을 요청한다. 266 | 267 | [NOTE] 입금 주소 생성 요청 API 유의사항 268 | 269 | 입금 주소의 생성은 서버에서 비동기적으로 이뤄집니다. 270 | 비동기적 생성 특성상 요청과 동시에 입금 주소가 발급되지 않을 수 있습니다. 271 | 주소 발급 요청 시 결과로 `Response1` 이 반환되며 주소 발급 완료 이전까지 계속 `Response1` 이 반환됩니다. 272 | 주소가 발급된 이후부터는 새로운 주소가 발급되는 것이 아닌 이전에 발급된 주소가 `Response2` 형태로 반환됩니다. 273 | 정상적으로 주소가 생성되지 않는다면 일정 시간 이후 해당 API를 다시 호출해주시길 부탁드립니다. 274 | 275 | :param currency: Currency 코드 276 | :type currency: str 277 | 278 | :param net_type: 입금 네트워크 279 | :type net_type: str 280 | """ 281 | 282 | future = self.__client.Deposit.Deposit_generate_coin_address(**kwargs) 283 | return HTTPFutureExtractor.future_extraction(future) 284 | 285 | def Deposit_info(self, **kwargs) -> dict: 286 | """ 287 | [GET] 개별 입금 조회 288 | 289 | ## 개별 입금 조회 290 | 291 | :param uuid: 입금 UUID (optional) 292 | :type uuid: str 293 | 294 | :param txid: 입금 TXID (optional) 295 | :type txid: str 296 | 297 | :param currency: Currency 코드 (optional) 298 | :type currency: str 299 | """ 300 | 301 | future = self.__client.Deposit.Deposit_info(**kwargs) 302 | return HTTPFutureExtractor.future_extraction(future) 303 | 304 | def Deposit_info_all(self, **kwargs) -> dict: 305 | """ 306 | [GET] 입금 리스트 조회 307 | 308 | ## 입금 리스트 조회 309 | 310 | :param currency: Currency 코드 (optional) 311 | :type currency: str 312 | 313 | :param state: 출금 상태 (optional) 314 | - submitting : 처리 중 315 | - submitted : 처리완료 316 | - almost_accepted : 입금 대기 중 317 | - rejected : 거절 318 | - accepted : 승인됨 319 | - processing : 처리 중 320 | :type state: str 321 | 322 | :param uuids: 입금 UUID의 목록 (optional) 323 | :type uuids: list 324 | 325 | :param txids: 입금 TXID의 목록 (optional) 326 | :type txids: list 327 | 328 | :param limit: 개수 제한 (default: 100, max: 100) (optional) 329 | :type limit: int 330 | 331 | :param page: 페이지 수, default: 1 (optional) 332 | :type page: int 333 | 334 | :param order_by: 정렬 방식 (optional) 335 | - asc : 오름차순 336 | - desc : 내림차순 (default) 337 | :type order_by: str 338 | """ 339 | 340 | future = self.__client.Deposit.Deposit_info_all(**kwargs) 341 | return HTTPFutureExtractor.future_extraction(future) 342 | 343 | 344 | class Market: 345 | """ 346 | 마켓(시장) 347 | """ 348 | 349 | def __init__(self, client): 350 | self.__client = client 351 | 352 | def Market_info_all(self, **kwargs) -> dict: 353 | """ 354 | [GET] 마켓 코드 조회 355 | 356 | ## 업비트에서 거래 가능한 마켓 목록 357 | 358 | :param isDetails: 유의종목 필드과 같은 상세 정보 노출 여부(선택 파라미터)(Default: False) (optional) 359 | :type isDetails: bool 360 | """ 361 | 362 | future = self.__client.Market.Market_info_all(**kwargs) 363 | return HTTPFutureExtractor.future_extraction(future) 364 | 365 | 366 | class Order: 367 | """ 368 | 주문 369 | """ 370 | 371 | def __init__(self, client): 372 | self.__client = client 373 | 374 | def Order_orderbook(self, **kwargs) -> dict: 375 | """ 376 | [GET] 시세 호가 정보(Orderbook) 조회 377 | 378 | ## 호가 정보 조회 379 | 380 | :param markets: 마켓 코드 목록 (ex. [KRW-BTC, KRW-ADA]) 381 | """ 382 | 383 | future = self.__client.Order.Order_orderbook(**kwargs) 384 | return HTTPFutureExtractor.future_extraction(future) 385 | 386 | def Order_chance(self, **kwargs) -> dict: 387 | """ 388 | [GET] 주문 가능 정보 389 | 390 | ## 마켓별 주문 가능 정보를 확인한다. 391 | 392 | :param market: Market ID 393 | """ 394 | 395 | future = self.__client.Order.Order_chance(**kwargs) 396 | return HTTPFutureExtractor.future_extraction(future) 397 | 398 | def Order_info(self, **kwargs): 399 | """ 400 | [GET] 개별 주문 조회 401 | 402 | ## 주문 UUID를 통해 개별 주문건을 조회한다. 403 | 404 | [NOTE] `uuid` 혹은 `identifier` 둘 중 하나의 값이 반드시 포함되어야 합니다. 405 | 406 | :param uuid: 주문 UUID (optional) 407 | :type uuid: str 408 | 409 | :param identifier: 조회용 사용자 지정 값 (optional) 410 | :type identifier: str 411 | """ 412 | 413 | future = self.__client.Order.Order_info(**kwargs) 414 | return HTTPFutureExtractor.future_extraction(future) 415 | 416 | def Order_info_all(self, **kwargs) -> dict: 417 | """ 418 | [GET] 주문 리스트 조회 419 | 420 | ## 주문 리스트를 조회한다. 421 | 422 | [NOTE] states 파라미터 변경 안내 (2021. 03. 22 ~) 423 | 424 | 2021년 3월 22일부터 미체결 주문(wait, watch)과 완료 주문(done, cancel)을 혼합하여 조회하실 수 없습니다. 425 | 426 | 예시1) done, cancel 주문을 한 번에 조회 => 가능 427 | 예시2) wait, done 주문을 한 번에 조회 => 불가능 (각각 API 호출 필요) 428 | 429 | 자세한 내용은 개발자 센터 공지사항을 참조 부탁드립니다. 430 | 431 | :param market: 마켓 아이디 (optional) 432 | :type market: str 433 | 434 | :param state: 주문 상태 (optional) 435 | - wait : 체결 대기 (default) 436 | - done : 전체 체결 완료 437 | - cancel : 주문 취소 438 | :type state: str 439 | 440 | :param states: 주문 상태의 목록 (optional) 441 | :type states: list 442 | 443 | :param uuids: 주문 UUID의 목록 (optional) 444 | :type uuids: list 445 | 446 | :param identifiers: 주문 identifier의 목록 (optional) 447 | :type identifiers: array 448 | 449 | :param page: 페이지 수, default: 1 (optional) 450 | :type page: int 451 | 452 | :param limit: 요청 개수, default: 100 (optional) 453 | :type limit: int 454 | 455 | :param order_by: 정렬 방식 (optional) 456 | - asc : 오름차순 457 | - desc : 내림차순 (default) 458 | :type order_by: str 459 | """ 460 | 461 | future = self.__client.Order.Order_info_all(**kwargs) 462 | return HTTPFutureExtractor.future_extraction(future) 463 | 464 | def Order_new(self, **kwargs) -> dict: 465 | """ 466 | [POST] 주문하기 467 | 468 | ## 주문 요청을 한다. 469 | [NOTE] 원화 마켓 가격 단위를 확인하세요. 470 | 471 | 원화 마켓에서 주문을 요청 할 경우, [원화 마켓 주문 가격 단위](https://docs.upbit.com/docs/market-info-trade-price-detail)를 확인하여 값을 입력해주세요. 472 | 473 | [NOTE] identifier 파라미터 사용 474 | 475 | `identifier`는 서비스에서 발급하는 `uuid`가 아닌 이용자가 직접 발급하는 키값으로, 주문을 조회하기 위해 할당하는 값입니다. 476 | 해당 값은 사용자의 전체 주문 내 유일한 값을 전달해야하며, 비록 주문 요청시 오류가 발생하더라도 같은 값으로 다시 요청을 보낼 수 없습니다. 477 | 주문의 성공 / 실패 여부와 관계없이 중복해서 들어온 `identifier` 값에서는 중복 오류가 발생하니, 매 요청시 새로운 값을 생성해주세요. 478 | 479 | [NOTE] 시장가 주문 480 | 481 | 시장가 주문은 `ord_type` 필드를 `price` or `market` 으로 설정해야됩니다. 482 | 매수 주문의 경우 `ord_type`을 `price`로 설정하고 `volume`을 `null` 혹은 제외해야됩니다. 483 | 매도 주문의 경우 `ord_type`을 `market`로 설정하고 `price`을 `null` 혹은 제외해야됩니다. 484 | 485 | 486 | :param market: 마켓 ID (필수) 487 | :type market: str 488 | 489 | :param side: 주문 종류 (필수) 490 | - bid : 매수 491 | - ask : 매도 492 | :type side: str 493 | 494 | :param volume: 주문량 (지정가, 시장가 매도 시 필수) (Default: null) (optional) 495 | :type volume: str 496 | 497 | :param price: 주문 가격. (지정가, 시장가 매수 시 필수) 498 | ex) KRW-BTC 마켓에서 1BTC당 1,000 KRW로 거래할 경우, 값은 1000 이 된다. 499 | ex) KRW-BTC 마켓에서 1BTC당 매도 1호가가 500 KRW 인 경우, 시장가 매수 시 값을 1000으로 세팅하면 2BTC가 매수된다. (수수료가 존재하거나 매도 1호가의 수량에 따라 상이할 수 있음) (Default: null) (optional) 500 | :type price: str 501 | 502 | :param ord_type: 주문 타입 (필수) 503 | - limit : 지정가 주문 504 | - price : 시장가 주문(매수) 505 | - market : 시장가 주문(매도) 506 | :type ord_type: str 507 | 508 | :param identifier: 조회용 사용자 지정값 (선택) (optional) 509 | :type identifier: str 510 | """ 511 | 512 | future = self.__client.Order.Order_new(**kwargs) 513 | return HTTPFutureExtractor.future_extraction(future) 514 | 515 | def Order_cancel(self, **kwargs) -> dict: 516 | """ 517 | [DELETE] 주문 취소 접수 518 | 519 | ## 주문 UUID를 통해 해당 주문에 대한 취소 접수를 한다. 520 | 521 | [NOTE] `uuid` 혹은 `identifier` 둘 중 하나의 값이 반드시 포함되어야 합니다. 522 | 523 | :param uuid: 취소할 주문의 UUID (optional) 524 | :type uuid: str 525 | 526 | :param identifier: 조회용 사용자 지정 값 (optional) 527 | :type identifier: str 528 | """ 529 | 530 | future = self.__client.Order.Order_cancel(**kwargs) 531 | return HTTPFutureExtractor.future_extraction(future) 532 | 533 | 534 | class Trade: 535 | """ 536 | 거래 537 | """ 538 | 539 | def __init__(self, client): 540 | self.__client = client 541 | 542 | def Trade_ticker(self, **kwargs) -> dict: 543 | """ 544 | [GET] 시세 Ticker 조회 545 | 546 | ## 현재가 정보 547 | 548 | 요청 당시 종목의 스냅샷을 반환한다. 549 | 550 | :param markets: 반점으로 구분되는 마켓 코드 (ex. KRW-BTC, BTC-BCC) 551 | :type markets: str 552 | """ 553 | 554 | future = self.__client.Trade.Trade_ticker(**kwargs) 555 | return HTTPFutureExtractor.future_extraction(future) 556 | 557 | def Trade_ticks(self, **kwargs) -> dict: 558 | """ 559 | [GET] 시세 체결 조회 560 | 561 | ## 최근 체결 내역 562 | 563 | :param market: 마켓 코드 (ex. KRW-BTC, BTC-BCC) 564 | :type market: str 565 | 566 | :param to: 마지막 체결 시각. 567 | 형식: `[HHmmss 또는 HH:mm:ss]`. 568 | 비워서 요청시 가장 최근 데이터 (optional) 569 | :type to: str 570 | 571 | :param count: 체결 개수 (optional) 572 | :type count: int 573 | 574 | :param cursor: 페이지네이션 커서 (sequentialId) 575 | `sequential_id` 필드는 체결의 유일성 판단을 위한 근거로 쓰일 수 있습니다. 576 | 하지만 체결의 순서를 보장하지는 못합니다. (optional) 577 | :type cursor: str 578 | 579 | :param daysAgo: 최근 체결 날짜 기준 7일 이내의 이전 데이터 조회 가능. 580 | 비워서 요청 시 가장 최근 체결 날짜 반환. (범위: 1 ~ 7) (optional) 581 | :type daysAgo: int 582 | """ 583 | 584 | future = self.__client.Trade.Trade_ticks(**kwargs) 585 | return HTTPFutureExtractor.future_extraction(future) 586 | 587 | 588 | class Withdraw: 589 | """ 590 | 출금 591 | """ 592 | 593 | def __init__(self, client): 594 | self.__client = client 595 | 596 | def Withdraw_chance(self, **kwargs) -> dict: 597 | """ 598 | [GET] 출금 가능 정보 599 | 600 | ## 해당 통화의 가능한 출금 정보를 확인한다. 601 | 602 | :param currency: 자산 코드 603 | :type currency: str 604 | 605 | :param net_type: 출금 네트워크 606 | :type net_type: str 607 | """ 608 | 609 | future = self.__client.Withdraw.Withdraw_chance(**kwargs) 610 | return HTTPFutureExtractor.future_extraction(future) 611 | 612 | def Withdraw_coin(self, **kwargs) -> dict: 613 | """ 614 | [POST] 코인 출금하기 615 | 616 | ## 코인 출금을 요청한다. 617 | 618 | [NOTE] 바로출금 이용 시 유의사항 619 | 620 | 업비트 회원의 주소가 아닌 주소로 바로출금을 요청하는 경우, 출금이 정상적으로 수행되지 않습니다. 621 | 반드시 주소를 확인 후 출금을 진행하시기 바랍니다. 622 | * 네트워크가 일치하지 않는 경우 정상 입출금이 진행되지 않을 수 있습니다. 623 | 사용하는 주소와 네트워크가 정확히 일치하는지 재확인 후 이용을 부탁드립니다. 624 | 625 | :param currency: 자산 코드 626 | :type currency: str 627 | 628 | :param net_type: 출금 네트워크 629 | :type net_type: str 630 | 631 | :param amount: 출금 수량 632 | :type amount: str 633 | 634 | :param address: 출금 가능 주소에 등록된 출금 주소 635 | :type address: str 636 | 637 | :param secondary_address: 2차 출금 주소 (필요한 코인에 한해서) (optional) 638 | :type secondary_address: str 639 | 640 | :param transaction_type: 출금 유형 (optional) 641 | - default : 일반출금 642 | - internal : 바로출금 643 | :type transaction_type: str 644 | """ 645 | 646 | future = self.__client.Withdraw.Withdraw_coin(**kwargs) 647 | return HTTPFutureExtractor.future_extraction(future) 648 | 649 | def Withdraw_info(self, **kwargs) -> dict: 650 | """ 651 | [GET] 개별 출금 조회 652 | 653 | ## 출금 UUID를 통해 개별 출금 정보를 조회한다. 654 | 655 | :param uuid: 출금 UUID (optional) 656 | :type uuid: str 657 | 658 | :param txid: 출금 TXID (optional) 659 | :type txid: str 660 | 661 | :param currency: Currency 코드 (optional) 662 | :type currency: str 663 | """ 664 | 665 | future = self.__client.Withdraw.Withdraw_info(**kwargs) 666 | return HTTPFutureExtractor.future_extraction(future) 667 | 668 | def Withdraw_info_all(self, **kwargs) -> dict: 669 | """ 670 | [GET] 출금 리스트 조회 671 | 672 | ## 출금 리스트를 조회한다. 673 | 674 | :param currency: Currency 코드 (optional) 675 | :type currency: str 676 | 677 | :param state: 출금 상태 (optional) 678 | - submitting : 처리 중 679 | - submitted : 처리 완료 680 | - almost_accepted : 출금대기중 681 | - rejected : 거부 682 | - accepted : 승인됨 683 | - processing : 처리 중 684 | - done : 완료 685 | - canceled : 취소됨 686 | :type state: str 687 | 688 | :param uuids: 출금 UUID의 목록 (optional) 689 | :type uuids: list 690 | 691 | :param txids: 출금 TXID의 목록 (optional) 692 | :type txids: list 693 | 694 | :param limit: 개수 제한 (default: 100, max: 100) (optional) 695 | :type limit: int 696 | 697 | :param page: 페이지 수, default: 1 (optional) 698 | :type page: int 699 | 700 | :param order_by: 정렬 방식 (optional) 701 | - asc : 오름차순 702 | - desc : 내림차순 (default) 703 | :type order_by: str 704 | """ 705 | 706 | future = self.__client.Withdraw.Withdraw_info_all(**kwargs) 707 | return HTTPFutureExtractor.future_extraction(future) 708 | 709 | def Withdraw_krw(self, **kwargs) -> dict: 710 | """ 711 | [POST] 원화 출금하기 712 | 713 | ## 원화 출금을 요청한다. 등록된 출금 계좌로 출금된다. 714 | 715 | :param amount: 출금 원화 수량 716 | :type amount: str 717 | 718 | :param two_factor_type: 2차 인증 수단 (optional) 719 | - kakao_pay: 카카오페이 인증 (default) 720 | - naver: 네이버 인증 721 | :type two_factor_type: str 722 | """ 723 | 724 | future = self.__client.Withdraw.Withdraw_krw(**kwargs) 725 | return HTTPFutureExtractor.future_extraction(future) 726 | 727 | def Withdraw_coin_addresses(self, **kwargs) -> dict: 728 | """ 729 | [GET] 출금 허용 주소 리스트 조회 730 | 731 | ## 등록된 출금 허용 주소 목록을 조회한다. 732 | 733 | [NOTE] 출금 기능을 이용하기 위해서는 주소 등록이 필요합니다. 734 | 735 | Open API를 통해 디지털 자산을 출금하기 위해서는 출금 허용 주소 등록이 필요합니다. 736 | * 네트워크가 일치하지 않는 경우 정상 입출금이 진행되지 않을 수 있습니다. 737 | 사용하는 주소와 네트워크가 정확히 일치하는지 재확인 후 이용을 부탁드립니다. 738 | 739 | ### 출금 허용 주소 등록 방법 740 | 업비트 웹 > [MY] > [Open API 관리] > [디지털 자산 출금주소 관리] 페이지에서 진행하실 수 있습니다. 741 | """ 742 | 743 | future = self.__client.Withdraw.Withdraw_coin_addresses(**kwargs) 744 | return HTTPFutureExtractor.future_extraction(future) 745 | 746 | -------------------------------------------------------------------------------- /upbit/pkginfo.py: -------------------------------------------------------------------------------- 1 | """ 2 | [Upbit Client] 3 | 4 | Please read the official Upbit Client document. 5 | Documents: https://ujhin.github.io/upbit-client-docs/ 6 | 7 | - Upbit OPEN API Version: 1.3.4 8 | - Author: ujhin 9 | - Email: ujhin942@gmail.com 10 | - GitHub: https://github.com/uJhin 11 | - Official OPEN API Documents: https://docs.upbit.com 12 | - Official Support Email: open-api@upbit.com 13 | """ 14 | 15 | import logging 16 | import requests 17 | 18 | from distutils.version import LooseVersion 19 | 20 | 21 | def _get_versions(package_name): 22 | url = f"https://pypi.org/pypi/{package_name}/json" 23 | resp = requests.get(url) 24 | data = resp.json() 25 | versions = data["releases"].keys() 26 | return sorted(versions, key=LooseVersion, reverse=True) 27 | 28 | 29 | PACKAGE_NAME = "upbit-client" 30 | 31 | OPEN_API_VERSION = "1.3.4" 32 | CURRENT_VERSION = OPEN_API_VERSION+".1" 33 | 34 | RELEASED_VERSION = _get_versions(PACKAGE_NAME) 35 | LATEST_VERSION = RELEASED_VERSION[0] 36 | 37 | 38 | if LATEST_VERSION != CURRENT_VERSION: 39 | logging.basicConfig(format="[%(levelname)s] %(message)s") 40 | logging.warning( 41 | f"{PACKAGE_NAME} is currently a newer version: {LATEST_VERSION}\n" 42 | f"Please update to the latest version using the pip command:" 43 | f"`pip install --upgrade {PACKAGE_NAME}`" 44 | ) 45 | -------------------------------------------------------------------------------- /upbit/utils.py: -------------------------------------------------------------------------------- 1 | 2 | import typing as t 3 | 4 | 5 | class HTTPFutureExtractor: 6 | 7 | @staticmethod 8 | def remaining_request(headers: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]: 9 | """ 10 | Check Request limit times 11 | 12 | Please read the official Upbit Client document. 13 | Documents: https://ujhin.github.io/upbit-client-docs/ 14 | """ 15 | 16 | remaining = headers['Remaining-Req'] 17 | return { 18 | k: v 19 | for k, v in [ 20 | param.split('=') 21 | for param 22 | in remaining.split('; ') 23 | ] 24 | } 25 | 26 | @staticmethod 27 | def future_extraction(http_future) -> t.Dict[str, t.Any]: 28 | resp = http_future.future.result() 29 | remaining = HTTPFutureExtractor.remaining_request(resp.headers) 30 | 31 | # resp.raise_for_status() 32 | 33 | result = { 34 | "remaining_request": remaining, 35 | "response": { 36 | "url": resp.url, 37 | "headers": resp.headers, 38 | "status_code": resp.status_code, 39 | "reason": resp.reason, 40 | "text": resp.text, 41 | "content": resp.content, 42 | "ok": resp.ok 43 | } 44 | } 45 | 46 | try: 47 | result['result'] = resp.json() 48 | except: 49 | result['result'] = { 50 | "error": { 51 | "message": resp.text, 52 | "name": resp.reason 53 | } 54 | } 55 | finally: 56 | return result 57 | 58 | 59 | class Validator: 60 | 61 | @staticmethod 62 | def validate_price(price: t.Union[int, float, str]) -> float: 63 | """ 64 | Please read the official Upbit Client document. 65 | Documents: https://ujhin.github.io/upbit-client-docs/ 66 | 67 | [Order price units] 68 | ~10 : 0.01 69 | ~100 : 0.1 70 | ~1,000 : 1 71 | ~10,000 : 5 72 | ~100,000 : 10 73 | ~500,000 : 50 74 | ~1,000,000 : 100 75 | ~2,000,000 : 500 76 | +2,000,000 : 1,000 77 | """ 78 | 79 | price = float(price) 80 | unit = 0.01 81 | if price <= 10: 82 | unit = 0.01 83 | elif price <= 100: 84 | unit = 0.1 85 | elif price <= 1_000: 86 | unit = 1 87 | elif price <= 10_000: 88 | unit = 5 89 | elif price <= 100_000: 90 | unit = 10 91 | elif price <= 500_000: 92 | unit = 50 93 | elif price <= 1_000_000: 94 | unit = 100 95 | elif price <= 2_000_000: 96 | unit = 500 97 | elif price > 2_000_000: 98 | unit = 1000 99 | else: 100 | raise ValueError('Invalid Price') 101 | return price - (price % unit) 102 | -------------------------------------------------------------------------------- /upbit/websocket.py: -------------------------------------------------------------------------------- 1 | 2 | import websockets 3 | import uuid 4 | import json 5 | 6 | import typing as t 7 | 8 | 9 | class UpbitWebSocket: 10 | """ 11 | Upbit WebSocket Client 12 | Please read the official Upbit Client document. 13 | Documents: https://ujhin.github.io/upbit-client-docs/ 14 | 15 | - Base URI: wss://api.upbit.com/websocket/v1 16 | - Author: ujhin 17 | - Email: ujhin942@gmail.com 18 | - GitHub: https://github.com/uJhin 19 | - Official OPEN API Documents: https://docs.upbit.com 20 | - Official Support Email: open-api@upbit.com 21 | """ 22 | 23 | WEBSOCKET_URI = "wss://api.upbit.com/websocket/v1" 24 | FIELD_TYPES = ( "ticker", "trade", "orderbook" ) 25 | FIELD_FORMATS = ( "SIMPLE", "DEFAULT" ) 26 | 27 | 28 | def __init__( 29 | self, 30 | uri : str = None, 31 | ping_interval: t.Union[int, float] = None, 32 | ping_timeout : t.Union[int, float] = None 33 | ): 34 | 35 | self.__uri = uri if uri else UpbitWebSocket.WEBSOCKET_URI 36 | self.__conn = None 37 | self.connect( 38 | ping_interval=ping_interval, 39 | ping_timeout=ping_timeout 40 | ) 41 | 42 | 43 | @property 44 | def URI(self): 45 | return self.__uri 46 | 47 | @URI.setter 48 | def URI(self, uri): 49 | self.__uri = uri 50 | 51 | @property 52 | def Connection(self): 53 | return self.__conn 54 | 55 | @Connection.setter 56 | def Connection(self, conn): 57 | self.__conn = conn 58 | 59 | 60 | def connect( 61 | self, 62 | ping_interval: t.Union[int, float] = None, 63 | ping_timeout : t.Union[int, float] = None 64 | ): 65 | """ 66 | :param ping_interval: ping 간격 제한 67 | :type ping_interval: Union[int, float] 68 | 69 | :param ping_timeout: ping 시간 초과 제한 70 | :type ping_timeout: Union[int, float] 71 | """ 72 | 73 | self.Connection = websockets.connect( 74 | uri=self.URI, 75 | ping_interval=ping_interval, 76 | ping_timeout=ping_timeout 77 | ) 78 | 79 | 80 | async def ping(self, decode: str = "utf8"): 81 | """ 82 | Client to Server PING 83 | """ 84 | async with self as conn: 85 | await conn.send("PING") 86 | recv = await conn.recv() 87 | pong = recv.decode(decode) 88 | return json.loads(pong) 89 | 90 | 91 | @staticmethod 92 | def generate_orderbook_codes( 93 | currencies: t.List[str], 94 | counts : t.List[int] = None 95 | ) -> t.List[str]: 96 | """ 97 | :param currencies: 수신할 `orderbook` field 마켓 코드 리스트 98 | :type currencies: list[str, ...] 99 | 100 | :param counts: 각 마켓 코드 리스트의 index에 해당하는 수신할 `orderbook` 갯수 101 | :type counts: list[int, ...] 102 | """ 103 | 104 | codes = [ 105 | f"{currency}.{count}" 106 | for currency, count 107 | in zip(currencies, counts) 108 | ] if counts else currencies 109 | return codes 110 | 111 | 112 | @staticmethod 113 | def generate_type_field( 114 | type : str, 115 | codes : t.List[str], 116 | isOnlySnapshot: bool = None, 117 | isOnlyRealtime: bool = None, 118 | ) -> t.Dict[str, t.Any]: 119 | """ 120 | :param type: 수신할 시세 타입 (현재가: `ticker`, 체결: `trade`, 호가: `orderbook`) 121 | :type type: str 122 | 123 | :param codes: 수신할 시세 종목 정보 124 | :type codes: list[str, ...] 125 | 126 | :param isOnlySnapshot: 시세 스냅샷만 제공 여부 127 | :type isOnlySnapshot: bool 128 | 129 | :param isOnlyRealtime: 실시간 시세만 제공 여부 130 | :type isOnlyRealtime: bool 131 | """ 132 | 133 | field = {} 134 | 135 | t = type.lower() 136 | if t in UpbitWebSocket.FIELD_TYPES: 137 | field["type"] = t 138 | else: 139 | raise ValueError("'type' is not available") 140 | 141 | codes = [code.upper() for code in codes] 142 | field["codes"] = codes 143 | 144 | if isOnlySnapshot is not None: 145 | field["isOnlySnapshot"] = isOnlySnapshot 146 | if isOnlyRealtime is not None: 147 | field["isOnlyRealtime"] = isOnlyRealtime 148 | 149 | return field 150 | 151 | 152 | @staticmethod 153 | def generate_payload( 154 | type_fields: t.List[t.Dict[str, t.Any]], 155 | ticket : str = None, 156 | format : str = "DEFAULT" 157 | ) -> str: 158 | """ 159 | :param type_fields: Type Fields 160 | :type type_fields: list[dict, ...] 161 | 162 | :param format: 포맷 (`SIMPLE`: 간소화된 필드명, `DEFAULT`: 기본값 (생략 가능)) 163 | :type format: str 164 | 165 | :param ticket: 식별값 166 | :type ticket: str 167 | """ 168 | 169 | payload = [] 170 | 171 | ticket = ticket if ticket else str(uuid.uuid4()) 172 | payload.append( { "ticket": ticket } ) 173 | 174 | payload.extend(type_fields) 175 | 176 | fmt = format.upper() 177 | fmt = fmt if fmt in UpbitWebSocket.FIELD_FORMATS else "DEFAULT" 178 | payload.append( { "format": fmt } ) 179 | 180 | return json.dumps(payload) 181 | 182 | 183 | async def __aenter__(self): 184 | return await self.Connection.__aenter__() 185 | 186 | async def __aexit__(self, exc_type, exc_value, traceback): 187 | await self.Connection.__aexit__(exc_type, exc_value, traceback) 188 | 189 | def __str__(self): 190 | return self.__repr__() 191 | 192 | def __repr__(self): 193 | return f"UpbitWebSocket({self.URI})" 194 | --------------------------------------------------------------------------------