├── .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 |
--------------------------------------------------------------------------------