├── .github └── workflows │ └── pythonpackage.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── mypy.ini ├── pyproject.toml ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── test_futures_public_api.py ├── test_leverage_trade_api.py ├── test_public_api.py └── test_trade_api.py └── zaifapi ├── __init__.py ├── api_common ├── __init__.py ├── response.py ├── url.py └── validator.py ├── api_error.py ├── exchange_api ├── __init__.py ├── public.py └── trade.py ├── impl.py └── oauth.py /.github/workflows/pythonpackage.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: [3.6, 3.7, 3.8] 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v1 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install . 23 | - name: Lint with flake8 24 | run: | 25 | pip install flake8 26 | # stop the build if there are Python syntax errors or undefined names 27 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 28 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 29 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 30 | - name: Black format 31 | run: | 32 | pip install black 33 | black --check . 34 | - name: List with mypy 35 | run: | 36 | pip install mypy 37 | mypy . 38 | - name: Test with pytest 39 | run: | 40 | pip install pytest 41 | pytest 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | .mypy_cache/ 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | .idea/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # IPython Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # dotenv 81 | .env 82 | 83 | # virtualenv 84 | venv/ 85 | ENV/ 86 | 87 | # Spyder project settings 88 | .spyderproject 89 | 90 | # Rope project settings 91 | .ropeproject 92 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 3.6 4 | - 3.7 5 | - 3.8 6 | 7 | sudo: false 8 | 9 | install: 10 | - python setup.py build --build-base=".build-$TRAVIS_PYTHON_VERSION" install 11 | 12 | script: 13 | - python setup.py test 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Akira-Taniguchi 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.md: -------------------------------------------------------------------------------- 1 | ![](https://img.shields.io/apm/l/vim-mode.svg) ![](https://img.shields.io/badge/Python-after%20v3-red.svg) 2 | 3 | :snake: zaifapi [![](https://img.shields.io/pypi/v/zaifapi.svg)](https://pypi.org/project/zaifapi/) [![](https://travis-ci.org/techbureau/zaifapi.svg?branch=master)](https://travis-ci.org/techbureau/zaifapi) 4 | ====================== 5 | zaifが公開しているAPIを簡単に呼べるようにしました。 6 | 本モジュールはテックビューロ非公式です。ご利用は自己責任でご自由にどうぞ。 7 | 8 | 使い方 9 | ------ 10 | 1.pipコマンドを実行し、モジュールをダウンロードしてください 11 | 12 | pip install zaifapi 13 | 14 | 2.クラスをインポートし、下記例の用に使用してください 15 | 16 | ```python 17 | from zaifapi import * 18 | 19 | zaif = ZaifPublicApi() 20 | zaif.last_price('btc_jpy') 21 | 22 | zaif = ZaifTradeApi(key, secret) 23 | zaif.trade(currency_pair='btc_jpy', 24 | action='ask', 25 | amount=2, 26 | price=400000, 27 | limit=200000) 28 | 29 | zaif = ZaifFuturesPublicApi() 30 | zaif.last_price(group_id=1, currency_pair='btc_jpy') 31 | 32 | zaif = ZaifLeverageTradeApi(key, secret) 33 | zaif.create_position(type='margin', 34 | currency_pair='btc_jpy', 35 | action='bid', 36 | amount=1, 37 | price=300000, 38 | leverage=2) 39 | ``` 40 | 41 | より詳しい機能については、[**Wiki**](https://github.com/techbureau/zaifapi/wiki)にてご確認ください。 42 | 43 | 44 | 関連情報 45 | -------- 46 | * [[Zaif]Pythonで簡単に仮想通貨の取引が出来るようにしてみた(Qiita)](http://qiita.com/Akira-Taniguchi/items/e52930c881adc6ecfe07) 47 | 48 | ライセンス 49 | ---------- 50 | Distributed under the [MIT License][mit]. 51 | [MIT]: http://www.opensource.org/licenses/mit-license.php 52 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | warn_return_any = True 3 | warn_unused_configs = True 4 | ignore_missing_imports = True 5 | strict_equality = True 6 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 100 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 100 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import io 2 | import re 3 | from setuptools import setup, find_packages 4 | 5 | 6 | with io.open("README.md", encoding="utf-8") as fp: 7 | readme = fp.read() 8 | 9 | with io.open("zaifapi/__init__.py", encoding="utf-8") as fp: 10 | match = re.search(r"__version__ = \"(.*?)\"", fp.read()) 11 | if not match: 12 | raise Exception("invalid version string") 13 | version = match.group(1) 14 | 15 | setup( 16 | name="zaifapi", 17 | version=version, 18 | description="Zaif Api Library", 19 | long_description=readme, 20 | long_description_content_type="text/markdown", 21 | url="https://github.com/techbureau/zaifapi", 22 | author="AkiraTaniguchi, DaikiShiroi", 23 | author_email="dededededaiou2003@yahoo.co.jp", 24 | packages=find_packages(), 25 | license="MIT", 26 | keywords="zaif bit coin btc xem mona jpy virtual currency block chain", 27 | classifiers=[ 28 | "Development Status :: 4 - Beta", 29 | "Programming Language :: Python", 30 | "Programming Language :: Python :: 3", 31 | "Programming Language :: Python :: 3.6", 32 | "Programming Language :: Python :: 3.7", 33 | "Programming Language :: Python :: 3.8", 34 | "Intended Audience :: Developers", 35 | "License :: OSI Approved :: MIT License", 36 | ], 37 | install_requires=["requests", "websocket-client", "Cerberus"], 38 | ) 39 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techbureau/zaifapi/5b7db7d6abdc76b4e911a74457140b3faf0b7317/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_futures_public_api.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch, MagicMock 3 | from zaifapi import ZaifFuturesPublicApi 4 | from zaifapi.api_error import ZaifApiValidationError 5 | 6 | 7 | class TestPublicApi(unittest.TestCase): 8 | @classmethod 9 | def setUpClass(cls): 10 | cls.api = ZaifFuturesPublicApi() 11 | cls.api._url._api_name = "test_futures" 12 | 13 | def setUp(self): 14 | self.response = MagicMock() 15 | self.response.status_code = 200 16 | self.response.text = "{}" 17 | self.assertEqual(self.api._url.get_absolute_url(), "https://api.zaif.jp/test_futures/1") 18 | 19 | def tearDown(self): 20 | self.assertEqual(self.api._url.get_absolute_url(), "https://api.zaif.jp/test_futures/1") 21 | 22 | def test_last_price(self): 23 | with patch("requests.get") as mock_get: 24 | currency_pair = "test_jpy" 25 | mock_get.return_value = self.response 26 | self.api.last_price(currency_pair=currency_pair, group_id=17) 27 | mock_get.assert_called_once_with( 28 | "https://api.zaif.jp/test_futures/1/last_price/17/test_jpy", params={} 29 | ) 30 | 31 | def test_ticker(self): 32 | with patch("requests.get") as mock_get: 33 | currency_pair = "test_jpy" 34 | mock_get.return_value = self.response 35 | self.api.ticker(currency_pair=currency_pair, group_id="all") 36 | mock_get.assert_called_once_with( 37 | "https://api.zaif.jp/test_futures/1/ticker/all/test_jpy", params={} 38 | ) 39 | 40 | def test_trades(self): 41 | with patch("requests.get") as mock_get: 42 | currency_pair = "test_jpy" 43 | mock_get.return_value = self.response 44 | self.api.trades(currency_pair=currency_pair, group_id=1212) 45 | mock_get.assert_called_once_with( 46 | "https://api.zaif.jp/test_futures/1/trades/1212/test_jpy", params={} 47 | ) 48 | 49 | def test_depth(self): 50 | with patch("requests.get") as mock_get: 51 | currency_pair = "test_jpy" 52 | mock_get.return_value = self.response 53 | self.api.depth(currency_pair=currency_pair, group_id="group_id") 54 | mock_get.assert_called_once_with( 55 | "https://api.zaif.jp/test_futures/1/depth/group_id/test_jpy", params={} 56 | ) 57 | 58 | def test_groups(self): 59 | with patch("requests.get") as mock_get: 60 | mock_get.return_value = self.response 61 | self.api.groups(group_id=3) 62 | mock_get.assert_called_once_with( 63 | "https://api.zaif.jp/test_futures/1/groups/3", params={} 64 | ) 65 | 66 | def test_swap_history(self): 67 | with patch("requests.get") as mock_get: 68 | currency_pair = "test_jpy" 69 | mock_get.return_value = self.response 70 | self.api.swap_history(currency_pair=currency_pair, group_id=3, page=5) 71 | mock_get.assert_called_once_with( 72 | "https://api.zaif.jp/test_futures/1/swap_history/3/test_jpy/5", params={} 73 | ) 74 | 75 | def test_swap_history_missing_page_arg(self): 76 | with patch("requests.get") as mock_get: 77 | currency_pair = "test_jpy" 78 | mock_get.return_value = self.response 79 | self.api.swap_history(currency_pair=currency_pair, group_id=3) 80 | mock_get.assert_called_once_with( 81 | "https://api.zaif.jp/test_futures/1/swap_history/3/test_jpy", params={} 82 | ) 83 | 84 | def test_swap_history_with_invalid_page_arg(self): 85 | with patch("requests.get") as mock_get: 86 | currency_pair = "test_jpy" 87 | mock_get.return_value = self.response 88 | # page argument is invalid type 89 | with self.assertRaises(ZaifApiValidationError): 90 | self.api.swap_history(currency_pair=currency_pair, group_id=3, page="5") 91 | 92 | 93 | if __name__ == "__main__": 94 | unittest.main() 95 | -------------------------------------------------------------------------------- /tests/test_leverage_trade_api.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch, MagicMock 3 | from zaifapi import ZaifLeverageTradeApi 4 | from urllib.parse import parse_qs 5 | 6 | 7 | class TestPublicApi(unittest.TestCase): 8 | @classmethod 9 | def setUpClass(cls): 10 | cls.api = ZaifLeverageTradeApi(key="test_key", secret="test_secret") 11 | cls.api._url._host = "test_leverage_trade.com" 12 | cls.api._get_header = MagicMock(return_value={"key": "key", "sign": "sign"}) 13 | cls.api._get_nonce = MagicMock(return_value=1111111111) 14 | 15 | def setUp(self): 16 | self.response = MagicMock() 17 | self.response.status_code = 200 18 | self.response.text = '{"success": 1, "return": "return"}' 19 | self.assertEqual(self.api._url.get_absolute_url(), "https://test_leverage_trade.com/tlapi") 20 | 21 | def tearDown(self): 22 | self.assertEqual(self.api._url.get_absolute_url(), "https://test_leverage_trade.com/tlapi") 23 | 24 | def test_get_positions(self): 25 | with patch("requests.post") as mock_post: 26 | mock_post.return_value = self.response 27 | self.api.get_positions( 28 | type="futures", 29 | from_num=0, 30 | count=1, 31 | from_id=2, 32 | end_id=3, 33 | order="DESC", 34 | since=1111, 35 | end=2222, 36 | currency_pair="test_jpy", 37 | ) 38 | 39 | params = { 40 | "method": ["get_positions"], 41 | "nonce": ["1111111111"], 42 | "from": ["0"], 43 | "count": ["1"], 44 | "from_id": ["2"], 45 | "end_id": ["3"], 46 | "order": ["DESC"], 47 | "since": ["1111"], 48 | "end": ["2222"], 49 | "currency_pair": ["test_jpy"], 50 | "type": ["futures"], 51 | } 52 | 53 | self.assertEqual(mock_post.call_args[0], ("https://test_leverage_trade.com/tlapi",)) 54 | self.assertDictEqual(parse_qs(mock_post.call_args[1]["data"]), params) 55 | self.assertDictEqual(mock_post.call_args[1]["headers"], {"key": "key", "sign": "sign"}) 56 | 57 | def test_position_history(self): 58 | with patch("requests.post") as mock_post: 59 | mock_post.return_value = self.response 60 | self.api.position_history(type="futures", group_id=1, leverage_id=12) 61 | 62 | params = { 63 | "method": ["position_history"], 64 | "nonce": ["1111111111"], 65 | "type": ["futures"], 66 | "group_id": ["1"], 67 | "leverage_id": ["12"], 68 | } 69 | 70 | self.assertEqual(mock_post.call_args[0], ("https://test_leverage_trade.com/tlapi",)) 71 | self.assertDictEqual(parse_qs(mock_post.call_args[1]["data"]), params) 72 | self.assertDictEqual(mock_post.call_args[1]["headers"], {"key": "key", "sign": "sign"}) 73 | 74 | def test_active_positions(self): 75 | with patch("requests.post") as mock_post: 76 | mock_post.return_value = self.response 77 | self.api.active_positions(type="futures", group_id=1, currency_pair="test_jpy") 78 | 79 | params = { 80 | "method": ["active_positions"], 81 | "nonce": ["1111111111"], 82 | "type": ["futures"], 83 | "group_id": ["1"], 84 | "currency_pair": ["test_jpy"], 85 | } 86 | 87 | self.assertEqual(mock_post.call_args[0], ("https://test_leverage_trade.com/tlapi",)) 88 | self.assertDictEqual(parse_qs(mock_post.call_args[1]["data"]), params) 89 | self.assertDictEqual(mock_post.call_args[1]["headers"], {"key": "key", "sign": "sign"}) 90 | 91 | def test_create_position(self): 92 | with patch("requests.post") as mock_post: 93 | mock_post.return_value = self.response 94 | self.api.create_position( 95 | type="futures", 96 | group_id=1, 97 | action="ask", 98 | price=12345, 99 | amount=12, 100 | leverage=5, 101 | limit=123, 102 | stop=12345566, 103 | currency_pair="test_jpy", 104 | ) 105 | 106 | params = { 107 | "method": ["create_position"], 108 | "nonce": ["1111111111"], 109 | "currency_pair": ["test_jpy"], 110 | "type": ["futures"], 111 | "group_id": ["1"], 112 | "action": ["ask"], 113 | "price": ["12345"], 114 | "amount": ["12"], 115 | "leverage": ["5"], 116 | "limit": ["123"], 117 | "stop": ["12345566"], 118 | } 119 | 120 | self.assertEqual(mock_post.call_args[0], ("https://test_leverage_trade.com/tlapi",)) 121 | self.assertDictEqual(parse_qs(mock_post.call_args[1]["data"]), params) 122 | self.assertDictEqual(mock_post.call_args[1]["headers"], {"key": "key", "sign": "sign"}) 123 | 124 | def test_change_position(self): 125 | with patch("requests.post") as mock_post: 126 | mock_post.return_value = self.response 127 | self.api.change_position( 128 | type="futures", group_id=1, price=12345, leverage_id=5, limit=123, stop=12345566 129 | ) 130 | 131 | params = { 132 | "method": ["change_position"], 133 | "nonce": ["1111111111"], 134 | "type": ["futures"], 135 | "group_id": ["1"], 136 | "price": ["12345"], 137 | "leverage_id": ["5"], 138 | "limit": ["123"], 139 | "stop": ["12345566"], 140 | } 141 | 142 | self.assertEqual(mock_post.call_args[0], ("https://test_leverage_trade.com/tlapi",)) 143 | self.assertDictEqual(parse_qs(mock_post.call_args[1]["data"]), params) 144 | self.assertDictEqual(mock_post.call_args[1]["headers"], {"key": "key", "sign": "sign"}) 145 | 146 | def test_cancel_position(self): 147 | with patch("requests.post") as mock_post: 148 | mock_post.return_value = self.response 149 | self.api.cancel_position(type="futures", group_id=1, leverage_id=5) 150 | 151 | params = { 152 | "method": ["cancel_position"], 153 | "nonce": ["1111111111"], 154 | "type": ["futures"], 155 | "group_id": ["1"], 156 | "leverage_id": ["5"], 157 | } 158 | 159 | self.assertEqual(mock_post.call_args[0], ("https://test_leverage_trade.com/tlapi",)) 160 | self.assertDictEqual(parse_qs(mock_post.call_args[1]["data"]), params) 161 | self.assertDictEqual(mock_post.call_args[1]["headers"], {"key": "key", "sign": "sign"}) 162 | 163 | 164 | if __name__ == "__main__": 165 | unittest.main() 166 | -------------------------------------------------------------------------------- /tests/test_public_api.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch, MagicMock 3 | from zaifapi import ZaifPublicApi 4 | 5 | 6 | class TestPublicApi(unittest.TestCase): 7 | @classmethod 8 | def setUpClass(cls): 9 | cls.api = ZaifPublicApi() 10 | cls.api._url._api_name = "test_public" 11 | 12 | def setUp(self): 13 | self.response = MagicMock() 14 | self.response.status_code = 200 15 | self.response.text = "{}" 16 | self.assertEqual(self.api._url.get_absolute_url(), "https://api.zaif.jp/test_public/1") 17 | 18 | def tearDown(self): 19 | self.assertEqual(self.api._url.get_absolute_url(), "https://api.zaif.jp/test_public/1") 20 | 21 | def test_last_price(self): 22 | with patch("requests.get") as mock_get: 23 | currency_pair = "test_jpy" 24 | mock_get.return_value = self.response 25 | self.api.last_price(currency_pair=currency_pair) 26 | mock_get.assert_called_once_with( 27 | "https://api.zaif.jp/test_public/1/last_price/test_jpy", params={} 28 | ) 29 | 30 | def test_ticker(self): 31 | with patch("requests.get") as mock_get: 32 | currency_pair = "test_jpy" 33 | mock_get.return_value = self.response 34 | self.api.ticker(currency_pair=currency_pair) 35 | mock_get.assert_called_once_with( 36 | "https://api.zaif.jp/test_public/1/ticker/test_jpy", params={} 37 | ) 38 | 39 | def test_trades(self): 40 | with patch("requests.get") as mock_get: 41 | currency_pair = "test_jpy" 42 | mock_get.return_value = self.response 43 | self.api.trades(currency_pair=currency_pair) 44 | mock_get.assert_called_once_with( 45 | "https://api.zaif.jp/test_public/1/trades/test_jpy", params={} 46 | ) 47 | 48 | def test_depth(self): 49 | with patch("requests.get") as mock_get: 50 | currency_pair = "test_jpy" 51 | mock_get.return_value = self.response 52 | self.api.depth(currency_pair=currency_pair) 53 | mock_get.assert_called_once_with( 54 | "https://api.zaif.jp/test_public/1/depth/test_jpy", params={} 55 | ) 56 | 57 | def test_currency_pairs(self): 58 | with patch("requests.get") as mock_get: 59 | currency_pair = "test_jpy" 60 | mock_get.return_value = self.response 61 | self.api.currency_pairs(currency_pair=currency_pair) 62 | mock_get.assert_called_once_with( 63 | "https://api.zaif.jp/test_public/1/currency_pairs/test_jpy", params={} 64 | ) 65 | 66 | def test_currency(self): 67 | with patch("requests.get") as mock_get: 68 | currency_pair = "test_coin" 69 | mock_get.return_value = self.response 70 | self.api.currencies(currency=currency_pair) 71 | mock_get.assert_called_once_with( 72 | "https://api.zaif.jp/test_public/1/currencies/test_coin", params={} 73 | ) 74 | 75 | 76 | if __name__ == "__main__": 77 | unittest.main() 78 | -------------------------------------------------------------------------------- /tests/test_trade_api.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch, MagicMock 3 | from zaifapi import ZaifTradeApi 4 | from urllib.parse import urlencode 5 | from urllib.parse import parse_qs 6 | 7 | 8 | class TestPublicApi(unittest.TestCase): 9 | @classmethod 10 | def setUpClass(cls): 11 | cls.api = ZaifTradeApi(key="test_key", secret="test_secret") 12 | cls.api._url._host = "test_trade.com" 13 | cls.api._get_header = MagicMock(return_value={"key": "key", "sign": "sign"}) 14 | cls.api._get_nonce = MagicMock(return_value=1111111111) 15 | 16 | def setUp(self): 17 | self.response = MagicMock() 18 | self.response.status_code = 200 19 | self.response.text = '{"success": 1, "return": "return"}' 20 | self.assertEqual(self.api._url.get_absolute_url(), "https://test_trade.com/tapi") 21 | 22 | def tearDown(self): 23 | self.assertEqual(self.api._url.get_absolute_url(), "https://test_trade.com/tapi") 24 | 25 | def test_get_info(self): 26 | with patch("requests.post") as mock_post: 27 | mock_post.return_value = self.response 28 | self.api.get_info() 29 | params = urlencode({"method": "get_info", "nonce": 1111111111}) 30 | 31 | mock_post.assert_called_once_with( 32 | "https://test_trade.com/tapi", data=params, headers={"key": "key", "sign": "sign"} 33 | ) 34 | 35 | def test_get_info2(self): 36 | with patch("requests.post") as mock_post: 37 | mock_post.return_value = self.response 38 | self.api.get_info2() 39 | params = urlencode({"method": "get_info2", "nonce": 1111111111}) 40 | 41 | mock_post.assert_called_once_with( 42 | "https://test_trade.com/tapi", data=params, headers={"key": "key", "sign": "sign"} 43 | ) 44 | 45 | def test_get_personal_info(self): 46 | with patch("requests.post") as mock_post: 47 | mock_post.return_value = self.response 48 | self.api.get_personal_info() 49 | params = urlencode({"method": "get_personal_info", "nonce": 1111111111}) 50 | 51 | mock_post.assert_called_once_with( 52 | "https://test_trade.com/tapi", data=params, headers={"key": "key", "sign": "sign"} 53 | ) 54 | 55 | def test_get_id_info(self): 56 | with patch("requests.post") as mock_post: 57 | mock_post.return_value = self.response 58 | self.api.get_id_info() 59 | params = urlencode({"method": "get_id_info", "nonce": 1111111111}) 60 | 61 | mock_post.assert_called_once_with( 62 | "https://test_trade.com/tapi", data=params, headers={"key": "key", "sign": "sign"} 63 | ) 64 | 65 | def test_trade_history(self): 66 | with patch("requests.post") as mock_post: 67 | mock_post.return_value = self.response 68 | self.api.trade_history( 69 | from_num=0, 70 | count=1, 71 | from_id=2, 72 | end_id=3, 73 | order="DESC", 74 | since=1111, 75 | end=2222, 76 | currency_pair="test_jpy", 77 | is_token=False, 78 | ) 79 | 80 | params = { 81 | "method": ["trade_history"], 82 | "nonce": ["1111111111"], 83 | "from": ["0"], 84 | "count": ["1"], 85 | "from_id": ["2"], 86 | "end_id": ["3"], 87 | "order": ["DESC"], 88 | "since": ["1111"], 89 | "end": ["2222"], 90 | "currency_pair": ["test_jpy"], 91 | "is_token": ["False"], 92 | } 93 | 94 | self.assertEqual(mock_post.call_args[0], ("https://test_trade.com/tapi",)) 95 | self.assertDictEqual(parse_qs(mock_post.call_args[1]["data"]), params) 96 | self.assertDictEqual(mock_post.call_args[1]["headers"], {"key": "key", "sign": "sign"}) 97 | 98 | def test_active_orders(self): 99 | with patch("requests.post") as mock_post: 100 | mock_post.return_value = self.response 101 | self.api.active_orders(currency_pair="test_jpy", is_token=False, is_token_both=True) 102 | 103 | params = { 104 | "method": ["active_orders"], 105 | "nonce": ["1111111111"], 106 | "currency_pair": ["test_jpy"], 107 | "is_token": ["False"], 108 | "is_token_both": ["True"], 109 | } 110 | 111 | self.assertEqual(mock_post.call_args[0], ("https://test_trade.com/tapi",)) 112 | self.assertDictEqual(parse_qs(mock_post.call_args[1]["data"]), params) 113 | self.assertDictEqual(mock_post.call_args[1]["headers"], {"key": "key", "sign": "sign"}) 114 | 115 | def test_withdraw_history(self): 116 | with patch("requests.post") as mock_post: 117 | mock_post.return_value = self.response 118 | self.api.withdraw_history( 119 | from_num=0, 120 | count=1, 121 | from_id=2, 122 | end_id=3, 123 | order="DESC", 124 | since=1111, 125 | end=2222, 126 | currency="test_coin", 127 | is_token=False, 128 | ) 129 | 130 | params = { 131 | "method": ["withdraw_history"], 132 | "nonce": ["1111111111"], 133 | "from": ["0"], 134 | "count": ["1"], 135 | "from_id": ["2"], 136 | "end_id": ["3"], 137 | "order": ["DESC"], 138 | "since": ["1111"], 139 | "end": ["2222"], 140 | "currency": ["test_coin"], 141 | "is_token": ["False"], 142 | } 143 | 144 | self.assertEqual(mock_post.call_args[0], ("https://test_trade.com/tapi",)) 145 | self.assertDictEqual(parse_qs(mock_post.call_args[1]["data"]), params) 146 | self.assertDictEqual(mock_post.call_args[1]["headers"], {"key": "key", "sign": "sign"}) 147 | 148 | def test_deposit_history(self): 149 | with patch("requests.post") as mock_post: 150 | mock_post.return_value = self.response 151 | self.api.deposit_history( 152 | from_num=0, 153 | count=1, 154 | from_id=2, 155 | end_id=3, 156 | order="DESC", 157 | since=1111, 158 | end=2222, 159 | currency="test_coin", 160 | is_token=False, 161 | ) 162 | 163 | params = { 164 | "method": ["deposit_history"], 165 | "nonce": ["1111111111"], 166 | "from": ["0"], 167 | "count": ["1"], 168 | "from_id": ["2"], 169 | "end_id": ["3"], 170 | "order": ["DESC"], 171 | "since": ["1111"], 172 | "end": ["2222"], 173 | "currency": ["test_coin"], 174 | "is_token": ["False"], 175 | } 176 | 177 | self.assertEqual(mock_post.call_args[0], ("https://test_trade.com/tapi",)) 178 | self.assertDictEqual(parse_qs(mock_post.call_args[1]["data"]), params) 179 | self.assertDictEqual(mock_post.call_args[1]["headers"], {"key": "key", "sign": "sign"}) 180 | 181 | def test_withdraw(self): 182 | with patch("requests.post") as mock_post: 183 | mock_post.return_value = self.response 184 | self.api.withdraw( 185 | currency="test_coin", 186 | address="test_address", 187 | message="test_message", 188 | amount=1000, 189 | opt_fee=1, 190 | ) 191 | 192 | params = { 193 | "method": ["withdraw"], 194 | "nonce": ["1111111111"], 195 | "currency": ["test_coin"], 196 | "address": ["test_address"], 197 | "message": ["test_message"], 198 | "amount": ["1000"], 199 | "opt_fee": ["1"], 200 | } 201 | 202 | self.assertEqual(mock_post.call_args[0], ("https://test_trade.com/tapi",)) 203 | self.assertDictEqual(parse_qs(mock_post.call_args[1]["data"]), params) 204 | self.assertDictEqual(mock_post.call_args[1]["headers"], {"key": "key", "sign": "sign"}) 205 | 206 | def test_cancel_order(self): 207 | with patch("requests.post") as mock_post: 208 | mock_post.return_value = self.response 209 | self.api.cancel_order(order_id=123, is_token=True, currency_pair="test_jpy") 210 | 211 | params = { 212 | "method": ["cancel_order"], 213 | "nonce": ["1111111111"], 214 | "currency_pair": ["test_jpy"], 215 | "order_id": ["123"], 216 | "is_token": ["True"], 217 | } 218 | 219 | self.assertEqual(mock_post.call_args[0], ("https://test_trade.com/tapi",)) 220 | self.assertDictEqual(parse_qs(mock_post.call_args[1]["data"]), params) 221 | self.assertDictEqual(mock_post.call_args[1]["headers"], {"key": "key", "sign": "sign"}) 222 | 223 | def test_trade(self): 224 | with patch("requests.post") as mock_post: 225 | mock_post.return_value = self.response 226 | self.api.trade( 227 | currency_pair="test_jpy", 228 | action="bid", 229 | price=12345, 230 | amount=12, 231 | limit=123456, 232 | comment="test", 233 | ) 234 | 235 | params = { 236 | "method": ["trade"], 237 | "nonce": ["1111111111"], 238 | "currency_pair": ["test_jpy"], 239 | "action": ["bid"], 240 | "price": ["12345"], 241 | "amount": ["12"], 242 | "limit": ["123456"], 243 | "comment": ["test"], 244 | } 245 | 246 | self.assertEqual(mock_post.call_args[0], ("https://test_trade.com/tapi",)) 247 | self.assertDictEqual(parse_qs(mock_post.call_args[1]["data"]), params) 248 | self.assertDictEqual(mock_post.call_args[1]["headers"], {"key": "key", "sign": "sign"}) 249 | 250 | 251 | if __name__ == "__main__": 252 | unittest.main() 253 | -------------------------------------------------------------------------------- /zaifapi/__init__.py: -------------------------------------------------------------------------------- 1 | from .exchange_api import ( 2 | ZaifTradeApi, 3 | ZaifPublicApi, 4 | ZaifTokenTradeApi, 5 | ZaifPublicStreamApi, 6 | ZaifLeverageTradeApi, 7 | ZaifFuturesPublicApi, 8 | ) 9 | from .oauth import ZaifTokenApi 10 | 11 | _MAX_COUNT = 1000 12 | _MIN_WAIT_TIME_SEC = 1 13 | 14 | __version__ = "1.7.0" 15 | 16 | __all__ = [ 17 | "__version__", 18 | "ZaifTradeApi", 19 | "ZaifPublicApi", 20 | "ZaifTokenTradeApi", 21 | "ZaifTokenApi", 22 | "ZaifPublicStreamApi", 23 | "ZaifLeverageTradeApi", 24 | "ZaifFuturesPublicApi", 25 | ] 26 | -------------------------------------------------------------------------------- /zaifapi/api_common/__init__.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from abc import ABCMeta 3 | from .response import get_response # NOQA 4 | from .url import ApiUrl, get_api_url # NOQA 5 | from .validator import ZaifApiValidator, FuturesPublicApiValidator # NOQA 6 | 7 | 8 | def method_name() -> str: 9 | return inspect.stack()[1][3] 10 | 11 | 12 | class ZaifApi(metaclass=ABCMeta): 13 | def __init__(self, url: ApiUrl): 14 | self._url = url 15 | -------------------------------------------------------------------------------- /zaifapi/api_common/response.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Any, Dict, Optional 3 | import requests 4 | from zaifapi.api_error import ZaifServerException 5 | 6 | 7 | def get_response( 8 | url, params: Optional[Dict[Any, Any]] = None, headers: Optional[Dict[Any, Any]] = None 9 | ) -> Any: 10 | response = requests.post(url, data=params, headers=headers) 11 | if response.status_code != 200: 12 | raise ZaifServerException("return status code is {}".format(response.status_code)) 13 | return json.loads(response.text) 14 | -------------------------------------------------------------------------------- /zaifapi/api_common/url.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | from typing import List, Optional 3 | from urllib.parse import urlencode 4 | 5 | 6 | class ApiUrl: 7 | _skeleton_url = "{}://{}{}" 8 | 9 | def __init__( 10 | self, 11 | api_name, 12 | protocol="https", 13 | host="api.zaif.jp", 14 | version=None, 15 | port=None, 16 | dirs=None, 17 | params=None, 18 | ): 19 | 20 | self._protocol = protocol 21 | self._host = host 22 | self._api_name = api_name 23 | self._port = port 24 | self._q_params = QueryParam(params) 25 | self._dirs = dirs or [] 26 | self._version = version 27 | 28 | def get_base_url(self) -> str: 29 | base = self._skeleton_url.format(self._protocol, self._host, self._get_port()) 30 | if self._api_name: 31 | base += "/" + str(self._api_name) 32 | 33 | if self._version: 34 | base += "/" + str(self._version) 35 | return base 36 | 37 | def get_absolute_url(self, *, with_params: bool = False) -> str: 38 | absolute_url = self.get_base_url() + self.get_pathname() 39 | if with_params is True: 40 | absolute_url += self._q_params.get_str_params() 41 | return absolute_url 42 | 43 | def get_pathname(self) -> str: 44 | path_name = "" 45 | for dir_ in self._dirs: 46 | path_name += "/" + str(dir_) 47 | return path_name 48 | 49 | def _get_port(self) -> str: 50 | if self._port: 51 | return ":{}".format(self._port) 52 | return "" 53 | 54 | def add_dirs(self, dir_, *dirs) -> None: 55 | for dir_ in itertools.chain((dir_,), dirs): 56 | if dir_ is None: 57 | return 58 | self._dirs.append(str(dir_)) 59 | 60 | def refresh_dirs(self) -> None: 61 | self._dirs = [] 62 | 63 | def add_q_params(self, dict_) -> None: 64 | for key, value in dict_.items(): 65 | self._q_params.add_param(key, value) 66 | 67 | def refresh_q_params(self) -> None: 68 | self._q_params.delete_all() 69 | 70 | 71 | class QueryParam: 72 | def __init__(self, params=None): 73 | self._params = params or {} 74 | 75 | def _encode(self) -> str: 76 | return urlencode(self._params) 77 | 78 | def get_str_params(self) -> str: 79 | if len(self._params) == 0: 80 | return "" 81 | return "?" + self._encode() 82 | 83 | def __str__(self): 84 | return self._encode() 85 | 86 | def add_param(self, k, v) -> None: 87 | self._params[k] = v 88 | 89 | def add_params(self, dictionary) -> None: 90 | for k, v in dictionary.items(): 91 | self._params[k] = v 92 | 93 | def delete_all(self) -> None: 94 | self._params = {} 95 | 96 | def __len__(self): 97 | return len(self._params) 98 | 99 | def __dict__(self): 100 | return self._params 101 | 102 | 103 | def get_api_url( 104 | arg_api_url: Optional[ApiUrl], 105 | api_name: Optional[str], 106 | protocol: str = "https", 107 | host: str = "api.zaif.jp", 108 | version: Optional[str] = None, 109 | dirs: Optional[List[str]] = None, 110 | port: Optional[int] = None, 111 | ) -> ApiUrl: 112 | if arg_api_url is not None: 113 | return arg_api_url 114 | return ApiUrl(api_name, protocol=protocol, host=host, version=version, dirs=dirs, port=port) 115 | -------------------------------------------------------------------------------- /zaifapi/api_common/validator.py: -------------------------------------------------------------------------------- 1 | import json 2 | from decimal import Decimal 3 | from typing import Any, Dict 4 | import cerberus 5 | from zaifapi.api_error import ZaifApiValidationError 6 | 7 | 8 | class ZaifApiValidator: 9 | def __init__(self): 10 | self._schema = _ZaifValidationSchema() 11 | 12 | def params_pre_processing(self, schema_keys, params): 13 | self._validate(schema_keys, params) 14 | return self._edit_params(params) 15 | 16 | @classmethod 17 | def _edit_params(cls, params): 18 | if "from_num" in params: 19 | params["from"] = params["from_num"] 20 | del params["from_num"] 21 | return params 22 | 23 | def _validate(self, schema_keys, params): 24 | required_schema = self._schema.select(schema_keys) 25 | v = _UnitValidator(required_schema) 26 | if v.validate(params): 27 | return 28 | raise ZaifApiValidationError(json.dumps(v.errors)) 29 | 30 | 31 | class FuturesPublicApiValidator(ZaifApiValidator): 32 | def __init__(self): 33 | super().__init__() 34 | self._schema.updates({"currency_pair": {"type": "string", "nullable": True}}) 35 | 36 | 37 | class _UnitValidator(cerberus.Validator): 38 | @staticmethod 39 | def _validate_type_decimal(value): 40 | if isinstance(value, Decimal): 41 | return True 42 | 43 | def validate(self, params): 44 | return super().validate(params) 45 | 46 | 47 | class _ZaifValidationSchema: 48 | def __init__(self): 49 | self._schema = DEFAULT_SCHEMA 50 | 51 | def all(self) -> Any: 52 | return self._schema 53 | 54 | def select(self, keys) -> Dict[Any, Any]: 55 | return dict(filter(lambda item: item[0] in keys, self._schema.items())) 56 | 57 | def update(self, k, v) -> None: 58 | self._schema[k] = v 59 | 60 | def updates(self, dictionary) -> None: 61 | for k, v, in dictionary.items(): 62 | self.update(k, v) 63 | 64 | 65 | DEFAULT_SCHEMA: Dict[str, Dict[str, Any]] = { 66 | "from_num": {"type": "integer"}, 67 | "count": {"type": "integer"}, 68 | "from_id": {"type": "integer"}, 69 | "end_id": {"type": ["string", "integer"]}, 70 | "order": {"type": "string", "allowed": ["ASC", "DESC"]}, 71 | "since": {"type": "integer"}, 72 | "end": {"type": ["string", "integer"]}, 73 | "currency_pair": {"type": "string"}, 74 | "currency": {"required": True, "type": "string"}, 75 | "address": {"required": True, "type": "string"}, 76 | "message": {"type": "string"}, 77 | "amount": {"required": True, "type": ["number", "decimal"]}, 78 | "opt_fee": {"type": "number"}, 79 | "order_id": {"required": True, "type": "integer"}, 80 | "action": {"required": True, "type": "string", "allowed": ["bid", "ask"]}, 81 | "price": {"required": True, "type": ["number", "decimal"]}, 82 | "limit": {"type": ["number", "decimal"]}, 83 | "is_token": {"type": "boolean"}, 84 | "is_token_both": {"type": "boolean"}, 85 | "comment": {"type": "string"}, 86 | "group_id": {"type": ["string", "integer"]}, 87 | "page": {"type": "integer"}, 88 | "type": {"type": "string", "required": True, "allowed": ["margin", "futures"]}, 89 | "leverage": {"required": True, "type": ["number", "decimal"]}, 90 | "leverage_id": {"type": "integer", "required": True}, 91 | "stop": {"type": ["number", "decimal"]}, 92 | } 93 | -------------------------------------------------------------------------------- /zaifapi/api_error.py: -------------------------------------------------------------------------------- 1 | class ZaifApiError(Exception): 2 | def __init__(self, message: str): 3 | self._message = message 4 | 5 | def __str__(self): 6 | return self._message 7 | 8 | 9 | class ZaifApiNonceError(ZaifApiError): 10 | pass 11 | 12 | 13 | class ZaifApiValidationError(ZaifApiError): 14 | pass 15 | 16 | 17 | class ZaifServerException(ZaifApiError): 18 | pass 19 | -------------------------------------------------------------------------------- /zaifapi/exchange_api/__init__.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | from zaifapi.api_common.validator import ZaifApiValidator 3 | from zaifapi.api_common import ZaifApi 4 | 5 | 6 | class ZaifExchangeApi(ZaifApi, metaclass=ABCMeta): 7 | def __init__(self, url, validator=None): 8 | super().__init__(url) 9 | self._validator = validator or ZaifApiValidator() 10 | 11 | @abstractmethod 12 | def _params_pre_processing(self, *args, **kwargs): 13 | raise NotImplementedError 14 | 15 | 16 | from .public import ZaifPublicApi, ZaifFuturesPublicApi, ZaifPublicStreamApi # NOQA 17 | from .trade import ZaifTokenTradeApi, ZaifTradeApi, ZaifLeverageTradeApi # NOQA 18 | 19 | 20 | __all__ = [ 21 | "ZaifLeverageTradeApi", 22 | "ZaifTradeApi", 23 | "ZaifTokenTradeApi", 24 | "ZaifFuturesPublicApi", 25 | "ZaifPublicApi", 26 | "ZaifPublicStreamApi", 27 | ] 28 | -------------------------------------------------------------------------------- /zaifapi/exchange_api/public.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | from abc import ABCMeta 4 | from typing import Optional 5 | 6 | from zaifapi.api_error import ZaifApiError 7 | from websocket import create_connection 8 | from zaifapi.api_common import ApiUrl, method_name, get_api_url, FuturesPublicApiValidator 9 | from . import ZaifExchangeApi 10 | 11 | 12 | class _ZaifPublicApiBase(ZaifExchangeApi, metaclass=ABCMeta): 13 | def _execute_api(self, func_name, schema_keys=None, q_params=None, **kwargs): 14 | schema_keys = schema_keys or [] 15 | q_params = q_params or {} 16 | params = self._params_pre_processing(schema_keys, kwargs) 17 | self._url.add_dirs(func_name, *params.values()) 18 | response = requests.get(self._url.get_absolute_url(), params=q_params) 19 | self._url.refresh_dirs() 20 | if response.status_code != 200: 21 | raise ZaifApiError("return status code is {}".format(response.status_code)) 22 | return json.loads(response.text) 23 | 24 | def _params_pre_processing(self, keys, params): 25 | return self._validator.params_pre_processing(keys, params) 26 | 27 | 28 | class ZaifPublicApi(_ZaifPublicApiBase): 29 | def __init__(self, api_url: Optional[ApiUrl] = None): 30 | super().__init__(get_api_url(api_url, "api", version="1")) 31 | 32 | def last_price(self, currency_pair): 33 | schema_keys = ["currency_pair"] 34 | return self._execute_api(method_name(), schema_keys, currency_pair=currency_pair) 35 | 36 | def ticker(self, currency_pair): 37 | schema_keys = ["currency_pair"] 38 | return self._execute_api(method_name(), schema_keys, currency_pair=currency_pair) 39 | 40 | def trades(self, currency_pair): 41 | schema_keys = ["currency_pair"] 42 | return self._execute_api(method_name(), schema_keys, currency_pair=currency_pair) 43 | 44 | def depth(self, currency_pair): 45 | schema_keys = ["currency_pair"] 46 | return self._execute_api(method_name(), schema_keys, currency_pair=currency_pair) 47 | 48 | def currency_pairs(self, currency_pair): 49 | schema_keys = ["currency_pair"] 50 | return self._execute_api(method_name(), schema_keys, currency_pair=currency_pair) 51 | 52 | def currencies(self, currency): 53 | schema_keys = ["currency"] 54 | return self._execute_api(method_name(), schema_keys, currency=currency) 55 | 56 | 57 | class ZaifFuturesPublicApi(_ZaifPublicApiBase): 58 | def __init__(self, api_url=None): 59 | api_url = get_api_url(api_url, "fapi", version=1) 60 | super().__init__(api_url, FuturesPublicApiValidator()) 61 | 62 | # Want to delete this method 63 | def _execute_api(self, func_name, schema_keys=None, q_params=None, **kwargs): 64 | schema_keys = schema_keys or [] 65 | q_params = q_params or {} 66 | params = self._params_pre_processing(schema_keys, kwargs) 67 | if params.get("page", None): 68 | self._url.add_dirs( 69 | func_name, params.get("group_id"), params.get("currency_pair"), params.get("page") 70 | ) 71 | else: 72 | self._url.add_dirs(func_name, params.get("group_id"), params.get("currency_pair")) 73 | response = requests.get(self._url.get_absolute_url(), params=q_params) 74 | self._url.refresh_dirs() 75 | if response.status_code != 200: 76 | raise ZaifApiError("return status code is {}".format(response.status_code)) 77 | return json.loads(response.text) 78 | 79 | def last_price(self, group_id, currency_pair=None): 80 | schema_keys = ["currency_pair", "group_id"] 81 | return self._execute_api( 82 | method_name(), schema_keys, group_id=group_id, currency_pair=currency_pair 83 | ) 84 | 85 | def ticker(self, group_id, currency_pair): 86 | schema_keys = ["currency_pair", "group_id"] 87 | return self._execute_api( 88 | method_name(), schema_keys, group_id=group_id, currency_pair=currency_pair 89 | ) 90 | 91 | def trades(self, group_id, currency_pair): 92 | schema_keys = ["currency_pair", "group_id"] 93 | return self._execute_api( 94 | method_name(), schema_keys, group_id=group_id, currency_pair=currency_pair 95 | ) 96 | 97 | def depth(self, group_id, currency_pair): 98 | schema_keys = ["currency_pair", "group_id"] 99 | return self._execute_api( 100 | method_name(), schema_keys, group_id=group_id, currency_pair=currency_pair 101 | ) 102 | 103 | def groups(self, group_id): 104 | schema_keys = ["group_id"] 105 | return self._execute_api(method_name(), schema_keys, group_id=group_id) 106 | 107 | def swap_history(self, group_id, currency_pair, page=None): 108 | if not page: 109 | schema_keys = ["currency_pair", "group_id"] 110 | return self._execute_api( 111 | method_name(), schema_keys, group_id=group_id, currency_pair=currency_pair 112 | ) 113 | schema_keys = ["currency_pair", "group_id", "page"] 114 | return self._execute_api( 115 | method_name(), schema_keys, group_id=group_id, currency_pair=currency_pair, page=page 116 | ) 117 | 118 | 119 | class ZaifPublicStreamApi(_ZaifPublicApiBase): 120 | def __init__(self, api_url=None): 121 | api_url = get_api_url(api_url, "stream", protocol="wss", host="ws.zaif.jp", port=8888) 122 | super().__init__(api_url) 123 | self._continue = True 124 | 125 | def stop(self): 126 | self._continue = False 127 | 128 | def execute(self, currency_pair): 129 | params = {"currency_pair": currency_pair} 130 | params = self._params_pre_processing(["currency_pair"], params=params) 131 | self._url.add_q_params(params) 132 | ws = create_connection(self._url.get_absolute_url(with_params=True)) 133 | while self._continue: 134 | result = ws.recv() 135 | yield json.loads(result) 136 | ws.close() 137 | -------------------------------------------------------------------------------- /zaifapi/exchange_api/trade.py: -------------------------------------------------------------------------------- 1 | import time 2 | import hmac 3 | import hashlib 4 | from decimal import Decimal 5 | from datetime import datetime 6 | from abc import ABCMeta, abstractmethod 7 | from typing import Optional 8 | from urllib.parse import urlencode 9 | from zaifapi.api_common import ApiUrl, get_response, get_api_url, method_name 10 | from zaifapi.api_error import ZaifApiError, ZaifApiNonceError 11 | from . import ZaifExchangeApi 12 | 13 | 14 | class _ZaifTradeApiBase(ZaifExchangeApi, metaclass=ABCMeta): 15 | @abstractmethod 16 | def _get_header(self, params): 17 | raise NotImplementedError() 18 | 19 | @staticmethod 20 | def _get_nonce(): 21 | now = datetime.now() 22 | nonce = str(int(time.mktime(now.timetuple()))) 23 | microseconds = "{0:06d}".format(now.microsecond) 24 | return Decimal(nonce + "." + microseconds) 25 | 26 | def _execute_api(self, func_name, schema_keys=None, params=None): 27 | schema_keys = schema_keys or [] 28 | params = params or {} 29 | 30 | params = self._params_pre_processing(schema_keys, params, func_name) 31 | header = self._get_header(params) 32 | url = self._url.get_absolute_url() 33 | 34 | res = get_response(url, params, header) 35 | if res["success"] == 0: 36 | if res["error"].startswith("nonce"): 37 | raise ZaifApiNonceError(res["error"]) 38 | raise ZaifApiError(res["error"]) 39 | return res["return"] 40 | 41 | def _params_pre_processing(self, keys, params, func_name): 42 | params = self._validator.params_pre_processing(keys, params) 43 | params["method"] = func_name 44 | params["nonce"] = self._get_nonce() 45 | return urlencode(params) 46 | 47 | 48 | def _make_signature(key, secret, params): 49 | signature = hmac.new(bytearray(secret.encode("utf-8")), digestmod=hashlib.sha512) 50 | signature.update(params.encode("utf-8")) 51 | return {"key": key, "sign": signature.hexdigest()} 52 | 53 | 54 | class ZaifTradeApi(_ZaifTradeApiBase): 55 | def __init__(self, key, secret, api_url=None): 56 | super().__init__(get_api_url(api_url, "tapi")) 57 | self._key = key 58 | self._secret = secret 59 | 60 | def _get_header(self, params): 61 | return _make_signature(self._key, self._secret, params) 62 | 63 | def get_info(self): 64 | return self._execute_api(method_name()) 65 | 66 | def get_info2(self): 67 | return self._execute_api(method_name()) 68 | 69 | def get_personal_info(self): 70 | return self._execute_api(method_name()) 71 | 72 | def get_id_info(self): 73 | return self._execute_api(method_name()) 74 | 75 | def trade_history(self, **kwargs): 76 | schema_keys = [ 77 | "from_num", 78 | "count", 79 | "from_id", 80 | "end_id", 81 | "order", 82 | "since", 83 | "end", 84 | "currency_pair", 85 | "is_token", 86 | ] 87 | return self._execute_api(method_name(), schema_keys, kwargs) 88 | 89 | def active_orders(self, **kwargs): 90 | schema_keys = ["currency_pair", "is_token", "is_token_both"] 91 | return self._execute_api(method_name(), schema_keys, kwargs) 92 | 93 | def _inner_history_api(self, func_name, kwargs): 94 | schema_keys = [ 95 | "currency", 96 | "from_num", 97 | "count", 98 | "from_id", 99 | "end_id", 100 | "order", 101 | "since", 102 | "end", 103 | "is_token", 104 | ] 105 | return self._execute_api(func_name, schema_keys, kwargs) 106 | 107 | def withdraw_history(self, **kwargs): 108 | return self._inner_history_api(method_name(), kwargs) 109 | 110 | def deposit_history(self, **kwargs): 111 | return self._inner_history_api(method_name(), kwargs) 112 | 113 | def withdraw(self, **kwargs): 114 | schema_keys = ["currency", "address", "message", "amount", "opt_fee"] 115 | return self._execute_api(method_name(), schema_keys, kwargs) 116 | 117 | def cancel_order(self, **kwargs): 118 | schema_keys = ["order_id", "is_token", "currency_pair"] 119 | return self._execute_api(method_name(), schema_keys, kwargs) 120 | 121 | def trade(self, **kwargs): 122 | schema_keys = ["currency_pair", "action", "price", "amount", "limit", "comment"] 123 | return self._execute_api(method_name(), schema_keys, kwargs) 124 | 125 | 126 | class ZaifLeverageTradeApi(_ZaifTradeApiBase): 127 | def __init__(self, key, secret, api_url=None): 128 | api_url = get_api_url(api_url, "tlapi") 129 | super().__init__(api_url) 130 | self._key = key 131 | self._secret = secret 132 | 133 | def _get_header(self, params): 134 | return _make_signature(self._key, self._secret, params) 135 | 136 | def get_positions(self, **kwargs): 137 | schema_keys = [ 138 | "type", 139 | "group_id", 140 | "from_num", 141 | "count", 142 | "from_id", 143 | "end_id", 144 | "order", 145 | "since", 146 | "end", 147 | "currency_pair", 148 | ] 149 | 150 | return self._execute_api(method_name(), schema_keys, kwargs) 151 | 152 | def position_history(self, **kwargs): 153 | schema_keys = ["type", "group_id", "leverage_id"] 154 | return self._execute_api(method_name(), schema_keys, kwargs) 155 | 156 | def active_positions(self, **kwargs): 157 | schema_keys = ["type", "group_id", "currency_pair"] 158 | return self._execute_api(method_name(), schema_keys, kwargs) 159 | 160 | def create_position(self, **kwargs): 161 | schema_keys = [ 162 | "type", 163 | "group_id", 164 | "currency_pair", 165 | "action", 166 | "price", 167 | "amount", 168 | "leverage", 169 | "limit", 170 | "stop", 171 | ] 172 | return self._execute_api(method_name(), schema_keys, kwargs) 173 | 174 | def change_position(self, **kwargs): 175 | schema_keys = ["type", "group_id", "leverage_id", "price", "limit", "stop"] 176 | return self._execute_api(method_name(), schema_keys, kwargs) 177 | 178 | def cancel_position(self, **kwargs): 179 | schema_keys = ["type", "group_id", "leverage_id"] 180 | return self._execute_api(method_name(), schema_keys, kwargs) 181 | 182 | 183 | class ZaifTokenTradeApi(ZaifTradeApi): 184 | def __init__(self, token: str, api_url: Optional[ApiUrl] = None): 185 | self._token = token 186 | super().__init__(None, None, api_url) 187 | 188 | def get_header(self, params): 189 | return {"token": self._token} 190 | -------------------------------------------------------------------------------- /zaifapi/impl.py: -------------------------------------------------------------------------------- 1 | # Importing from this module is deprecated 2 | # This module will be removed in the future 3 | 4 | from zaifapi.exchange_api.public import ( 5 | ZaifPublicStreamApi, 6 | ZaifPublicApi, 7 | ZaifFuturesPublicApi, 8 | ) 9 | from zaifapi.exchange_api.trade import ( 10 | ZaifTradeApi, 11 | ZaifLeverageTradeApi, 12 | ZaifTokenTradeApi, 13 | ) 14 | 15 | from zaifapi.oauth import ZaifTokenApi 16 | 17 | _MAX_COUNT = 1000 18 | _MIN_WAIT_TIME_SEC = 1 19 | 20 | __all__ = [ 21 | "ZaifTradeApi", 22 | "ZaifLeverageTradeApi", 23 | "ZaifTokenTradeApi", 24 | "ZaifPublicStreamApi", 25 | "ZaifPublicApi", 26 | "ZaifFuturesPublicApi", 27 | "ZaifTokenApi", 28 | ] 29 | -------------------------------------------------------------------------------- /zaifapi/oauth.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from zaifapi.api_common import get_response, ZaifApi 3 | from zaifapi.api_common import ApiUrl, get_api_url 4 | 5 | 6 | class ZaifTokenApi(ZaifApi): 7 | def __init__(self, client_id: str, client_secret: str, api_url: Optional[ApiUrl] = None): 8 | setup_api_url = get_api_url( 9 | api_url, None, host="oauth.zaif.jp", version="v1", dirs=["token"] 10 | ) 11 | super().__init__(setup_api_url) 12 | self._client_id = client_id 13 | self._client_secret = client_secret 14 | 15 | def get_token(self, code: str, redirect_uri: Optional[str] = None): 16 | params = { 17 | "code": code, 18 | "client_id": self._client_id, 19 | "client_secret": self._client_secret, 20 | "grant_type": "authorization_code", 21 | } 22 | if redirect_uri: 23 | params["redirect_uri"] = redirect_uri 24 | return get_response(self._url.get_absolute_url(), params) 25 | 26 | def refresh_token(self, refresh_token: str): 27 | params = { 28 | "refresh_token": refresh_token, 29 | "client_id": self._client_id, 30 | "client_secret": self._client_secret, 31 | "grant_type": "refresh_token", 32 | } 33 | return get_response(self._url.get_absolute_url(), params) 34 | --------------------------------------------------------------------------------