chore: 移除 grvt-pysdk 项目中的所有文件,清理代码库

This commit is contained in:
discountry
2025-10-03 02:12:45 +08:00
parent 42061017f6
commit 599b4ec3f3
47 changed files with 104 additions and 14319 deletions
-57
View File
@@ -1,57 +0,0 @@
name: ci
on:
push:
branches:
- main
env:
UV_VERSION: "0.4.0"
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
build:
env:
UV_CACHE_DIR: /tmp/.uv-cache
runs-on: ubuntu-24.04
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Set up python
id: setup-python
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"
- name: Set up uv
run: curl -LsSf https://astral.sh/uv/${{ env.UV_VERSION }}/install.sh | sh
- name: Restore uv cache
uses: actions/cache@v4
with:
path: /tmp/.uv-cache
key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
restore-keys: |
uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
uv-${{ runner.os }}
- name: Install dependencies
run: uv sync --all-extras --dev --frozen
- name: Test with pytest
run: uv run pytest tests --cov=src
- name: Minimize uv cache
run: uv cache prune --ci
build-image:
runs-on: ubuntu-24.04
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build docker image
uses: docker/build-push-action@v6
with:
context: .
push: false
-39
View File
@@ -1,39 +0,0 @@
# name: "codeql"
# on:
# push:
# branches: ["main"]
# pull_request:
# branches: ["main"]
# jobs:
# analyze:
# name: Analyze
# runs-on: ubuntu-24.04
# permissions:
# actions: read
# contents: read
# security-events: write
# strategy:
# fail-fast: false
# matrix:
# language: [python]
# steps:
# - name: Checkout
# uses: actions/checkout@v4
# - name: Initialize CodeQL
# uses: github/codeql-action/init@v3
# with:
# languages: ${{ matrix.language }}
# queries: +security-and-quality
# - name: Autobuild
# uses: github/codeql-action/autobuild@v3
# - name: Perform CodeQL Analysis
# uses: github/codeql-action/analyze@v3
# with:
# category: "/language:${{ matrix.language }}"
-61
View File
@@ -1,61 +0,0 @@
name: pr
on:
pull_request:
branches:
- "**"
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
build:
env:
UV_CACHE_DIR: /tmp/.uv-cache
runs-on: ubuntu-24.04
strategy:
matrix:
python-version: ["3.10.15", "3.10.x"]
uv-version: ["0.3.5", "0.4.0"]
fail-fast: false
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Set up python
id: setup-python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
architecture: x64
- name: Set up uv
run: curl -LsSf https://astral.sh/uv/${{ matrix.uv-version }}/install.sh | sh
- name: Restore uv cache
uses: actions/cache@v4
with:
path: /tmp/.uv-cache
key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
restore-keys: |
uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
uv-${{ runner.os }}
- name: Install dependencies
run: uv sync --all-extras --dev --frozen
- name: Test with pytest
run: uv run pytest tests --cov=src
- name: Minimize uv cache
run: uv cache prune --ci
build-image:
runs-on: ubuntu-24.04
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build docker image
uses: docker/build-push-action@v6
with:
context: .
push: false
-287
View File
@@ -1,287 +0,0 @@
# Created by https://www.toptal.com/developers/gitignore/api/python,pycharm+all,visualstudiocode
# Edit at https://www.toptal.com/developers/gitignore?templates=python,pycharm+all,visualstudiocode
### PyCharm+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### PyCharm+all Patch ###
# Ignore everything but code style settings and run configurations
# that are supposed to be shared within teams.
.idea/*
!.idea/codeStyles
!.idea/runConfigurations
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
### Python Patch ###
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
poetry.toml
# ruff
.ruff_cache/
# LSP config files
pyrightconfig.json
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
# Tests
test_results.csv
test_results_sync.csv
# End of https://www.toptal.com/developers/gitignore/api/python,pycharm+all,visualstudiocode
@@ -1,79 +0,0 @@
ci:
skip: [pytest]
default_language_version:
python: python3.10
repos:
# general checks (see here: https://pre-commit.com/hooks.html)
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: check-yaml
args: [--allow-multiple-documents]
- id: end-of-file-fixer
- id: trailing-whitespace
# ruff - linting + formatting
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.6.3"
hooks:
- id: ruff
name: ruff
- id: ruff-format
name: ruff-format
# mypy - lint-like type checking
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.11.2
hooks:
- id: mypy
name: mypy
# docformatter - formats docstrings to follow PEP 257
- repo: https://github.com/pycqa/docformatter
rev: v1.7.5
hooks:
- id: docformatter
name: docformatter
args:
[
-r,
-i,
--pre-summary-newline,
--make-summary-multi-line,
--wrap-summaries,
"90",
--wrap-descriptions,
"90",
src,
tests,
]
exclude: ^(artifacts/.*)$
# bandit - find common security issues
- repo: https://github.com/pycqa/bandit
rev: 1.7.9
hooks:
- id: bandit
name: bandit
exclude: ^tests/
args:
- -r
- src
- repo: local
hooks:
- id: pytest
name: pytest
entry: uv run pytest tests --cov=src
language: system
types: [python]
pass_filenames: false
# prettier - formatting JS, CSS, JSON, Markdown, ...
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.1.0
hooks:
- id: prettier
exclude: ^(uv.lock)$
@@ -1 +0,0 @@
3.10
@@ -1,11 +0,0 @@
{
"extends": [
"config:base",
"schedule:daily",
"group:all",
":prConcurrentLimitNone",
":prHourlyLimitNone",
":prImmediately"
],
"labels": ["dependencies"]
}
-78
View File
@@ -1,78 +0,0 @@
# Changelog
## Version [0.2.1] - 2025-07-24
### Changes in 0.2.1
- New methods in ccxt-compatible code for Strategy (Vault) managers to view investment history and current redemption queue of a Vault.
- Classes `GrvtCcxt` and `GrvtCcxtPro`
- Methods `fetch_vault_manager_investor_history` and `fetch_vault_redemption_queue`
## Version [0.2.0] - 2025-07-16
### Changes in 0.2.0
- additional fixes for removal of explicit enums in `grvt_raw_types.py`
- Note: this update would `break existing integrations` for `non-create-order flows` (e.g., transfer/withdrawal history, transfer, get open orders, get-instrument filters) to handle currency strings directly instead of enums.
- users will be losing the currency enum when they upgrade, so they would need to call the currencies endpoin (<https://api-docs.grvt.io/market_data_api/#get-currency> ), for which the support has been added in PySDK (`get_currency_v1()` in grvt_raw_async.py and grvt_raw_sync.py), for the metadata attached to the currency strings.
## Version [0.1.32] - 2025-07-15
### Changes in 0.1.32
- removed explicit enums in `grvt_raw_types.py` (no need to update SDK for when new coins are listed)
- added vault-related functionality
## Version [0.1.31] - 2025-07-08
### Changes in 0.1.31
- added AVAX in the raw code (testing purposes)
## Version [0.1.30] - 2025-07-08
### Changes in 0.1.30
- added `H` in the **raw** code
## Version [0.1.29] - 2025-06-30
### Changes in 0.1.29
- Added new currencies: `HYPE, UNI, MOODENG, LAUNCHCOIN` in the **raw** code
- Improvements and type fixes in `fetch_funding_rate_history()` methods of GrvtCcxt and GrvtCcxtPro.
## Version [0.1.28] - 2025-06-02
### Changes in 0.1.28
- Renamed currency name `AI_16_Z` into `AI16Z` in the **raw** code to match the exchange currency name.
## Version [0.1.27] - 2025-05-19
### Changes in 0.1.27
- `GrvtCcxt` and `GrvtCcxtPro` classes:
- renamed method `fetch_balances()` to `fetch_balance()` as defined in ccxt.
## Version [0.1.26] - 2025-05-15
### Added in 0.1.26
- `GrvtCcxt` and `GrvtCcxtPro` classes:
- new method `describe()` - returns a list of public method names
- new method `fetch_balances()` - returns dict with balances in ccxt format.
- constructor parameter `order_book_ccxt_format: bool = False` . If = True then order book snapshots from `fetch_order_book()` are in ccxt format.
### Fixed in 0.1.26
- Issues with typing and lynting errors
## Version [0.1.25] - 2025-04-25
### Fixed in 0.1.25
- Issues with typing and lynting errors
- Fixed bug in test_grvt_ccxt.py
-201
View File
@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-59
View File
@@ -1,59 +0,0 @@
# A Self-Documenting Makefile: http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
.PHONY: help
help: ## Show this help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sed -e 's/^Makefile://' | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
.PHONY: run
run: ## Run the project
uv run python -m pysdk.main
.PHONY: test
test: ## Run the tests
uv run pytest tests --cov=src
.PHONY: precommit
precommit: ## Run the pre-commit hooks
bash .git/hooks/pre-commit
.PHONY: lint
lint: ## Run the linter
uv run ruff check .
.PHONY: lint-fix
lint-fix: ## Run the linter and fix the issues
uv run ruff check --fix --unsafe-fixes .
.PHONY: format
format: ## Run the formatter
uv run ruff format .
.PHONY: typecheck
typecheck: ## Run the type checker
uv run mypy .
.PHONY: security
security: ## Run the security checker
uv run bandit .
.PHONY: clean
clean: ## Clean the project
uv run ruff clean .
.PHONY: install
install: ## Install the project
uv sync --all-extras --dev --frozen
.PHONY: build
build: ## Build the project
uv build
.PHONY: publish
publish: ## Publish the project
python3 build_readme.py
make build
uv run twine upload --skip-existing dist/*
# ==============================================================================
# always make sure the following is the last line on this file
.DEFAULT_GOAL := help
-300
View File
@@ -1,300 +0,0 @@
<p align="center">
<a href="https://github.com/astral-sh/uv" target="blank"><img src="https://github.com/astral-sh/uv/blob/8674968a17e5f2ee0dda01d17aaf609f162939ca/docs/assets/logo-letter.svg" height="100" alt="uv logo" /></a>
<a href="https://pre-commit.com/" target="blank"><img src="https://pre-commit.com/logo.svg" height="100" alt="pre-commit logo" /></a>
<a href="https://github.com/astral-sh/ruff" target="blank"><img src="https://raw.githubusercontent.com/astral-sh/ruff/8c20f14e62ddaf7b6d62674f300f5d19cbdc5acb/docs/assets/bolt.svg" height="100" alt="ruff logo" style="background-color: #ef5552" /></a>
<a href="https://bandit.readthedocs.io/" target="blank"><img src="https://raw.githubusercontent.com/pycqa/bandit/main/logo/logo.svg" height="100" alt="bandit logo" /></a>
<a href="https://docs.pytest.org/" target="blank"><img src="https://raw.githubusercontent.com/pytest-dev/pytest/main/doc/en/img/pytest_logo_curves.svg" height="100" alt="pytest logo" /></a>
</p>
<p align="center">
<a href="https://docs.docker.com/" target="blank"><img src="https://www.docker.com/wp-content/uploads/2022/03/Moby-logo.png" height="60" alt="Docker logo" /></a>
<a href="https://github.com/features/actions" target="blank"><img src="https://avatars.githubusercontent.com/u/44036562" height="60" alt="GitHub Actions logo" /></a>
</p>
# GRVT Python SDK
[![CodeQL](https://github.com/smarlhens/python-boilerplate/workflows/codeql/badge.svg)](https://github.com/smarlhens/python-boilerplate/actions/workflows/codeql.yml)
[![GitHub CI](https://github.com/smarlhens/python-boilerplate/workflows/ci/badge.svg)](https://github.com/smarlhens/python-boilerplate/actions/workflows/ci.yml)
[![GitHub license](https://img.shields.io/github/license/smarlhens/python-boilerplate)](https://github.com/smarlhens/python-boilerplate)
---
## Table of Contents
- [GRVT Python SDK](#grvt-python-sdk)
- [Table of Contents](#table-of-contents)
- [What's in the library](#whats-in-the-library)
- [Environments](#environments)
- [Files](#files)
- [Installation via pip](#installation-via-pip)
- [Configuration](#configuration)
- [Usage](#usage)
- [Contributor's guide](#contributors-guide)
- [Prerequisites](#prerequisites)
- [Installation of a source code](#installation-of-a-source-code)
- [Manually run example files](#manually-run-example-files)
- [What's in the box ?](#whats-in-the-box-)
- [uv](#uv)
- [pre-commit](#pre-commit)
- [ruff](#ruff)
- [mypy](#mypy)
- [bandit](#bandit)
- [docformatter](#docformatter)
- [Testing](#testing)
- [Makefile](#makefile)
---
## What's in the library
GRVT Python SDK library provides Python classes and utility methods for easy access to GRVT API endpoints across all environments.
### Environments
- `prod` - Production environment.
- `testnet` - Testnet environment.
- `staging` - Development Integration environment.
- `dev` - Development environment.
SDK Library provides two types of classes:
1. **Raw** - thin wrapper classes around Rest API.
2. **Ccxt-compatible** - classes with ccxt-like methods. Provide access to both Rest API and WebSockets.
### Files
Raw access:
- `grvt_raw_base.py` - base classes for Rest API access.
- `grvt_raw_env.py` - definitions of environments for raw access.
- `grvt_raw_signing.py` - utility methods for signing orders.
- `grvt_raw_sync.py` - class for raw synchronous calls to Rest API.
- `grvt_raw_async.py` - class for raw asynchronous calls to Rest API.
CCXT-compatible access:
- `grvt_ccxt_utils.py` - utility methods for signing orders.
- `grvt_ccxt_env.py` - definitions of environments for ccxt-like access.
- `grvt_ccxt_base.py` - base class for Rest API access.
- `grvt_ccxt.py` - class for synchronous calls to Rest API.
- `grvt_ccxt_pro.py` - class for asynchronous calls to Rest API.
- `grvt_ccxt_ws.py` - class for WebSocket calls.
## Installation via pip
```bash
pip install grvt-pysdk
```
### Configuration
Setup these environment variables:
```bash
export GRVT_PRIVATE_KEY="`Secret Private Key` in API setup"
export GRVT_API_KEY="`API Key` in API setup"
export GRVT_TRADING_ACCOUNT_ID=<`Trading account ID` in API>
export GRVT_ENV="testnet"
export GRVT_END_POINT_VERSION="v1"
export GRVT_WS_STREAM_VERSION="v1"
```
## Usage
**Examples of how to use various methods to connect to GRVT API:**
- [GRVT CCXT](https://github.com/gravity-technologies/grvt-pysdk/blob/main/tests/pysdk/test_grvt_ccxt.py) - Example of usage of CCXT-compatible Python class `GrvtCcxt` with `synchronous` Rest API calls.
- [GRVT CCXT Pro](https://github.com/gravity-technologies/grvt-pysdk/blob/main/tests/pysdk/test_grvt_ccxt_pro.py) - Example of usage of CCXT.PRO-compatible Python class `GrvtCcxtPro` with `asynchronous` Rest API calls.
- [GRVT CCXT WS](https://github.com/gravity-technologies/grvt-pysdk/blob/main/tests/pysdk/test_grvt_ccxt_ws.py) - Example of usage of CCXT.PRO-compatible Python class `GrvtCcxtWS` with `asynchronous` Rest API calls + Web Socket subscriptions + JSON RPC calls over Web Sockets.
- [GRVT API Sync](https://github.com/gravity-technologies/grvt-pysdk/blob/main/tests/pysdk/test_grvt_raw_sync.py) - Synchronous API client for GRVT
- [GRVT API Async](https://github.com/gravity-technologies/grvt-pysdk/blob/main/tests/pysdk/test_grvt_raw_async.py) - Asynchronous API client for GRVT
## Contributor's guide
### Prerequisites
- [Python](https://www.python.org/downloads/) **>=3.10.0 < 3.13** (_tested with 3.10.15_)
- [pre-commit](https://pre-commit.com/#install)
- [uv](https://docs.astral.sh/uv/getting-started/installation/) **>=0.3.3** (_tested with 0.4.0_)
- [docker](https://docs.docker.com/get-docker/) (_optional_)
### Installation of a source code
1. Clone the git repository
```bash
git clone https://github.com/grvt-technologies/grvt-pysdk.git
```
2. Go into the project directory
```bash
cd grvt-pysdk/
```
3. Checkout working branch
```bash
git checkout <branch>
```
4. Install dependencies
```bash
make install
```
5. Enable pre-commit hooks
```bash
pre-commit install
```
---
### Manually run example files
Example files run extensive testing of the SDK API classes and log details about details of using GRVT API.
1. Change to tests folder
```bash
cd tests/pysdk
```
2. Run example of using synchronous CCXT-compatible calls to `Rest API` via `grvt_ccxt.py`, class `GrvtCcxt`
```bash
uv run python3 test_grvt_ccxt.py
```
3. Run example of using asynchronous CCXT-compatible calls to `Rest API` via `grvt_ccxt_pro.py`, class `GrvtCcxtPro`
```bash
uv run python3 test_grvt_ccxt_pro.py
```
4. Run example of using WebSockets subscriptions and JSON RPC calls via `grvt_ccxt_ws.py`, class `GrvtCcxtWS`
```bash
uv run python3 test_grvt_ccxt_ws.py
```
### What's in the box ?
#### uv
[uv](https://github.com/astral-sh/uv) is an extremely fast Python package and project manager, written in Rust.
**pyproject.toml file** ([`pyproject.toml`](pyproject.toml)): orchestrate your project and its dependencies
**uv.lock file** ([`uv.lock`](uv.lock)): ensure that the package versions are consistent for everyone
working on your project
For more configuration options and details, see the [configuration docs](https://docs.astral.sh/uv/).
#### pre-commit
[pre-commit](https://pre-commit.com/) is a framework for managing and maintaining multi-language pre-commit hooks.
**.pre-commit-config.yaml file** ([`.pre-commit-config.yaml`](.pre-commit-config.yaml)): describes what repositories and
hooks are installed
For more configuration options and details, see the [configuration docs](https://pre-commit.com/).
#### ruff
[ruff](https://github.com/astral-sh/ruff) is an extremely fast Python linter, written in Rust.
Rules are defined in the [`pyproject.toml`](pyproject.toml).
For more configuration options and details, see the [configuration docs](https://github.com/astral-sh/ruff#configuration).
#### mypy
[mypy](http://mypy-lang.org/) is an optional static type checker for Python that aims to combine the benefits of
dynamic (or "duck") typing and static typing.
Rules are defined in the [`pyproject.toml`](pyproject.toml).
For more configuration options and details, see the [configuration docs](https://mypy.readthedocs.io/).
#### bandit
[bandit](https://bandit.readthedocs.io/) is a tool designed to find common security issues in Python code.
Rules are defined in the [`pyproject.toml`](pyproject.toml).
For more configuration options and details, see the [configuration docs](https://bandit.readthedocs.io/).
#### docformatter
[docformatter](https://github.com/PyCQA/docformatter) is a tool designed to format docstrings to
follow [PEP 257](https://peps.python.org/pep-0257/).
Options are defined in the [`.pre-commit-config.yaml`](.pre-commit-config.yaml).
---
#### Testing
We are using [pytest](https://docs.pytest.org/) & [pytest-cov](https://github.com/pytest-dev/pytest-cov) to write tests.
To run tests with coverage:
```bash
make test
```
```text
Name Stmts Miss Cover
-------------------------------------------------------------
src/pysdk/__init__.py 0 0 100%
src/pysdk/grvt_ccxt.py 238 21 91%
src/pysdk/grvt_ccxt_base.py 196 57 71%
src/pysdk/grvt_ccxt_env.py 48 12 75%
src/pysdk/grvt_ccxt_logging_selector.py 15 7 53%
src/pysdk/grvt_ccxt_pro.py 245 69 72%
src/pysdk/grvt_ccxt_test_utils.py 30 4 87%
src/pysdk/grvt_ccxt_types.py 41 0 100%
src/pysdk/grvt_ccxt_utils.py 237 89 62%
src/pysdk/grvt_ccxt_ws.py 275 238 13%
src/pysdk/grvt_raw_async.py 149 109 27%
src/pysdk/grvt_raw_base.py 154 69 55%
src/pysdk/grvt_raw_env.py 27 5 81%
src/pysdk/grvt_raw_signing.py 33 17 48%
src/pysdk/grvt_raw_sync.py 149 109 27%
src/pysdk/grvt_raw_types.py 1031 0 100%
-------------------------------------------------------------
TOTAL 2868 806 72%
=================================================================================================== 8 passed in 58.20s ====================================================================================================
```
#### Makefile
We are using [Makefile](https://www.gnu.org/software/make/manual/make.html) to manage the project.
To see the list of commands:
```bash
make
```
```bash
➜ grvt-pysdk git:(main) ✗ make
help Show this help
run Run the project
test Run the tests
precommit Run the pre-commit hooks
lint Run the linter
format Run the formatter
typecheck Run the type checker
security Run the security checker
clean Clean the project
install Install the project
build Build the project
publish Publish the project
```
---
-382
View File
@@ -1,382 +0,0 @@
<p align="center">
<a href="https://github.com/astral-sh/uv" target="blank"><img src="https://github.com/astral-sh/uv/blob/8674968a17e5f2ee0dda01d17aaf609f162939ca/docs/assets/logo-letter.svg" height="100" alt="uv logo" /></a>
<a href="https://pre-commit.com/" target="blank"><img src="https://pre-commit.com/logo.svg" height="100" alt="pre-commit logo" /></a>
<a href="https://github.com/astral-sh/ruff" target="blank"><img src="https://raw.githubusercontent.com/astral-sh/ruff/8c20f14e62ddaf7b6d62674f300f5d19cbdc5acb/docs/assets/bolt.svg" height="100" alt="ruff logo" style="background-color: #ef5552" /></a>
<a href="https://bandit.readthedocs.io/" target="blank"><img src="https://raw.githubusercontent.com/pycqa/bandit/main/logo/logo.svg" height="100" alt="bandit logo" /></a>
<a href="https://docs.pytest.org/" target="blank"><img src="https://raw.githubusercontent.com/pytest-dev/pytest/main/doc/en/img/pytest_logo_curves.svg" height="100" alt="pytest logo" /></a>
</p>
<p align="center">
<a href="https://docs.docker.com/" target="blank"><img src="https://www.docker.com/wp-content/uploads/2022/03/Moby-logo.png" height="60" alt="Docker logo" /></a>
<a href="https://github.com/features/actions" target="blank"><img src="https://avatars.githubusercontent.com/u/44036562" height="60" alt="GitHub Actions logo" /></a>
</p>
# GRVT Python SDK
[![CodeQL](https://github.com/smarlhens/python-boilerplate/workflows/codeql/badge.svg)](https://github.com/smarlhens/python-boilerplate/actions/workflows/codeql.yml)
[![GitHub CI](https://github.com/smarlhens/python-boilerplate/workflows/ci/badge.svg)](https://github.com/smarlhens/python-boilerplate/actions/workflows/ci.yml)
[![GitHub license](https://img.shields.io/github/license/smarlhens/python-boilerplate)](https://github.com/smarlhens/python-boilerplate)
---
## Table of Contents
- [GRVT Python SDK](#grvt-python-sdk)
- [Table of Contents](#table-of-contents)
- [What's in the library](#whats-in-the-library)
- [Environments](#environments)
- [Files](#files)
- [Installation via pip](#installation-via-pip)
- [Configuration](#configuration)
- [Usage](#usage)
- [Contributor's guide](#contributors-guide)
- [Prerequisites](#prerequisites)
- [Installation of a source code](#installation-of-a-source-code)
- [Manually run example files](#manually-run-example-files)
- [What's in the box ?](#whats-in-the-box-)
- [uv](#uv)
- [pre-commit](#pre-commit)
- [ruff](#ruff)
- [mypy](#mypy)
- [bandit](#bandit)
- [docformatter](#docformatter)
- [Testing](#testing)
- [Makefile](#makefile)
---
## What's in the library
GRVT Python SDK library provides Python classes and utility methods for easy access to GRVT API endpoints across all environments.
### Environments
- `prod` - Production environment.
- `testnet` - Testnet environment.
- `staging` - Development Integration environment.
- `dev` - Development environment.
SDK Library provides two types of classes:
1. **Raw** - thin wrapper classes around Rest API.
2. **Ccxt-compatible** - classes with ccxt-like methods. Provide access to both Rest API and WebSockets.
### Files
Raw access:
- `grvt_raw_base.py` - base classes for Rest API access.
- `grvt_raw_env.py` - definitions of environments for raw access.
- `grvt_raw_signing.py` - utility methods for signing orders.
- `grvt_raw_sync.py` - class for raw synchronous calls to Rest API.
- `grvt_raw_async.py` - class for raw asynchronous calls to Rest API.
CCXT-compatible access:
- `grvt_ccxt_utils.py` - utility methods for signing orders.
- `grvt_ccxt_env.py` - definitions of environments for ccxt-like access.
- `grvt_ccxt_base.py` - base class for Rest API access.
- `grvt_ccxt.py` - class for synchronous calls to Rest API.
- `grvt_ccxt_pro.py` - class for asynchronous calls to Rest API.
- `grvt_ccxt_ws.py` - class for WebSocket calls.
## Installation via pip
```bash
pip install grvt-pysdk
```
### Configuration
Setup these environment variables:
```bash
export GRVT_PRIVATE_KEY="`Secret Private Key` in API setup"
export GRVT_API_KEY="`API Key` in API setup"
export GRVT_TRADING_ACCOUNT_ID=<`Trading account ID` in API>
export GRVT_ENV="testnet"
export GRVT_END_POINT_VERSION="v1"
export GRVT_WS_STREAM_VERSION="v1"
```
## Usage
**Examples of how to use various methods to connect to GRVT API:**
- [GRVT CCXT](https://github.com/gravity-technologies/grvt-pysdk/blob/main/tests/pysdk/test_grvt_ccxt.py) - Example of usage of CCXT-compatible Python class `GrvtCcxt` with `synchronous` Rest API calls.
- [GRVT CCXT Pro](https://github.com/gravity-technologies/grvt-pysdk/blob/main/tests/pysdk/test_grvt_ccxt_pro.py) - Example of usage of CCXT.PRO-compatible Python class `GrvtCcxtPro` with `asynchronous` Rest API calls.
- [GRVT CCXT WS](https://github.com/gravity-technologies/grvt-pysdk/blob/main/tests/pysdk/test_grvt_ccxt_ws.py) - Example of usage of CCXT.PRO-compatible Python class `GrvtCcxtWS` with `asynchronous` Rest API calls + Web Socket subscriptions + JSON RPC calls over Web Sockets.
- [GRVT API Sync](https://github.com/gravity-technologies/grvt-pysdk/blob/main/tests/pysdk/test_grvt_raw_sync.py) - Synchronous API client for GRVT
- [GRVT API Async](https://github.com/gravity-technologies/grvt-pysdk/blob/main/tests/pysdk/test_grvt_raw_async.py) - Asynchronous API client for GRVT
## Contributor's guide
### Prerequisites
- [Python](https://www.python.org/downloads/) **>=3.10.0 < 3.13** (_tested with 3.10.15_)
- [pre-commit](https://pre-commit.com/#install)
- [uv](https://docs.astral.sh/uv/getting-started/installation/) **>=0.3.3** (_tested with 0.4.0_)
- [docker](https://docs.docker.com/get-docker/) (_optional_)
### Installation of a source code
1. Clone the git repository
```bash
git clone https://github.com/grvt-technologies/grvt-pysdk.git
```
2. Go into the project directory
```bash
cd grvt-pysdk/
```
3. Checkout working branch
```bash
git checkout <branch>
```
4. Install dependencies
```bash
make install
```
5. Enable pre-commit hooks
```bash
pre-commit install
```
---
### Manually run example files
Example files run extensive testing of the SDK API classes and log details about details of using GRVT API.
1. Change to tests folder
```bash
cd tests/pysdk
```
2. Run example of using synchronous CCXT-compatible calls to `Rest API` via `grvt_ccxt.py`, class `GrvtCcxt`
```bash
uv run python3 test_grvt_ccxt.py
```
3. Run example of using asynchronous CCXT-compatible calls to `Rest API` via `grvt_ccxt_pro.py`, class `GrvtCcxtPro`
```bash
uv run python3 test_grvt_ccxt_pro.py
```
4. Run example of using WebSockets subscriptions and JSON RPC calls via `grvt_ccxt_ws.py`, class `GrvtCcxtWS`
```bash
uv run python3 test_grvt_ccxt_ws.py
```
### What's in the box ?
#### uv
[uv](https://github.com/astral-sh/uv) is an extremely fast Python package and project manager, written in Rust.
**pyproject.toml file** ([`pyproject.toml`](pyproject.toml)): orchestrate your project and its dependencies
**uv.lock file** ([`uv.lock`](uv.lock)): ensure that the package versions are consistent for everyone
working on your project
For more configuration options and details, see the [configuration docs](https://docs.astral.sh/uv/).
#### pre-commit
[pre-commit](https://pre-commit.com/) is a framework for managing and maintaining multi-language pre-commit hooks.
**.pre-commit-config.yaml file** ([`.pre-commit-config.yaml`](.pre-commit-config.yaml)): describes what repositories and
hooks are installed
For more configuration options and details, see the [configuration docs](https://pre-commit.com/).
#### ruff
[ruff](https://github.com/astral-sh/ruff) is an extremely fast Python linter, written in Rust.
Rules are defined in the [`pyproject.toml`](pyproject.toml).
For more configuration options and details, see the [configuration docs](https://github.com/astral-sh/ruff#configuration).
#### mypy
[mypy](http://mypy-lang.org/) is an optional static type checker for Python that aims to combine the benefits of
dynamic (or "duck") typing and static typing.
Rules are defined in the [`pyproject.toml`](pyproject.toml).
For more configuration options and details, see the [configuration docs](https://mypy.readthedocs.io/).
#### bandit
[bandit](https://bandit.readthedocs.io/) is a tool designed to find common security issues in Python code.
Rules are defined in the [`pyproject.toml`](pyproject.toml).
For more configuration options and details, see the [configuration docs](https://bandit.readthedocs.io/).
#### docformatter
[docformatter](https://github.com/PyCQA/docformatter) is a tool designed to format docstrings to
follow [PEP 257](https://peps.python.org/pep-0257/).
Options are defined in the [`.pre-commit-config.yaml`](.pre-commit-config.yaml).
---
#### Testing
We are using [pytest](https://docs.pytest.org/) & [pytest-cov](https://github.com/pytest-dev/pytest-cov) to write tests.
To run tests with coverage:
```bash
make test
```
```text
Name Stmts Miss Cover
-------------------------------------------------------------
src/pysdk/__init__.py 0 0 100%
src/pysdk/grvt_ccxt.py 238 21 91%
src/pysdk/grvt_ccxt_base.py 196 57 71%
src/pysdk/grvt_ccxt_env.py 48 12 75%
src/pysdk/grvt_ccxt_logging_selector.py 15 7 53%
src/pysdk/grvt_ccxt_pro.py 245 69 72%
src/pysdk/grvt_ccxt_test_utils.py 30 4 87%
src/pysdk/grvt_ccxt_types.py 41 0 100%
src/pysdk/grvt_ccxt_utils.py 237 89 62%
src/pysdk/grvt_ccxt_ws.py 275 238 13%
src/pysdk/grvt_raw_async.py 149 109 27%
src/pysdk/grvt_raw_base.py 154 69 55%
src/pysdk/grvt_raw_env.py 27 5 81%
src/pysdk/grvt_raw_signing.py 33 17 48%
src/pysdk/grvt_raw_sync.py 149 109 27%
src/pysdk/grvt_raw_types.py 1031 0 100%
-------------------------------------------------------------
TOTAL 2868 806 72%
=================================================================================================== 8 passed in 58.20s ====================================================================================================
```
#### Makefile
We are using [Makefile](https://www.gnu.org/software/make/manual/make.html) to manage the project.
To see the list of commands:
```bash
make
```
```bash
➜ grvt-pysdk git:(main) ✗ make
help Show this help
run Run the project
test Run the tests
precommit Run the pre-commit hooks
lint Run the linter
format Run the formatter
typecheck Run the type checker
security Run the security checker
clean Clean the project
install Install the project
build Build the project
publish Publish the project
```
---
## Changelog
# Changelog
## Version [0.2.1] - 2025-07-24
### Changes in 0.2.1
- New methods in ccxt-compatible code for Strategy (Vault) managers to view investment history and current redemption queue of a Vault.
- Classes `GrvtCcxt` and `GrvtCcxtPro`
- Methods `fetch_vault_manager_investor_history` and `fetch_vault_redemption_queue`
## Version [0.2.0] - 2025-07-16
### Changes in 0.2.0
- additional fixes for removal of explicit enums in `grvt_raw_types.py`
- Note: this update would `break existing integrations` for `non-create-order flows` (e.g., transfer/withdrawal history, transfer, get open orders, get-instrument filters) to handle currency strings directly instead of enums.
- users will be losing the currency enum when they upgrade, so they would need to call the currencies endpoin (<https://api-docs.grvt.io/market_data_api/#get-currency> ), for which the support has been added in PySDK (`get_currency_v1()` in grvt_raw_async.py and grvt_raw_sync.py), for the metadata attached to the currency strings.
## Version [0.1.32] - 2025-07-15
### Changes in 0.1.32
- removed explicit enums in `grvt_raw_types.py` (no need to update SDK for when new coins are listed)
- added vault-related functionality
## Version [0.1.31] - 2025-07-08
### Changes in 0.1.31
- added AVAX in the raw code (testing purposes)
## Version [0.1.30] - 2025-07-08
### Changes in 0.1.30
- added `H` in the **raw** code
## Version [0.1.29] - 2025-06-30
### Changes in 0.1.29
- Added new currencies: `HYPE, UNI, MOODENG, LAUNCHCOIN` in the **raw** code
- Improvements and type fixes in `fetch_funding_rate_history()` methods of GrvtCcxt and GrvtCcxtPro.
## Version [0.1.28] - 2025-06-02
### Changes in 0.1.28
- Renamed currency name `AI_16_Z` into `AI16Z` in the **raw** code to match the exchange currency name.
## Version [0.1.27] - 2025-05-19
### Changes in 0.1.27
- `GrvtCcxt` and `GrvtCcxtPro` classes:
- renamed method `fetch_balances()` to `fetch_balance()` as defined in ccxt.
## Version [0.1.26] - 2025-05-15
### Added in 0.1.26
- `GrvtCcxt` and `GrvtCcxtPro` classes:
- new method `describe()` - returns a list of public method names
- new method `fetch_balances()` - returns dict with balances in ccxt format.
- constructor parameter `order_book_ccxt_format: bool = False` . If = True then order book snapshots from `fetch_order_book()` are in ccxt format.
### Fixed in 0.1.26
- Issues with typing and lynting errors
## Version [0.1.25] - 2025-04-25
### Fixed in 0.1.25
- Issues with typing and lynting errors
- Fixed bug in test_grvt_ccxt.py
@@ -1,9 +0,0 @@
from pathlib import Path
readme = Path("README.md").read_text()
changelog = Path("CHANGELOG.md").read_text()
combined = f"{readme}\n\n## Changelog\n\n{changelog}"
Path("README_PYPI.md").write_text(combined)
print("✅ Combined README.md and CHANGELOG.md into README_PYPI.md")
-106
View File
@@ -1,106 +0,0 @@
[project]
name = "grvt-pysdk"
version = "0.2.1"
description = "GRVT Python SDK"
requires-python = ">=3.10"
license = { file = "LICENSE" }
authors = [
{ name = "GRVT", email = "contact@grvt.io" },
]
readme = { file = "README_PYPI.md", content-type = "text/markdown" }
dependencies = [
"aiohttp>=3.10.11",
"backports-weakref>=1.0.post1",
"dacite>=1.8.1",
"dataclasses-json>=0.6.7",
"eth-account>=0.13.4",
"inflection>=0.5.1",
"requests>=2.32.3",
"websockets==13.1",
]
[project.urls]
homepage = "https://github.com/gravity-technologies/grvt-pysdk"
repository = "https://github.com/gravity-technologies/grvt-pysdk"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/pysdk"]
[tool.uv]
dev-dependencies = [
"pytest>=8.3.2",
"pytest-cov>=5.0.0",
"mypy>=1.11.2",
"bandit>=1.7.9",
"docformatter>=1.7.5",
"ruff>=0.6.2",
"twine>=5.1.1",
]
[tool.pytest.ini_options]
addopts = "-vvv"
testpaths = "tests"
[tool.ruff]
extend-exclude = [
"__pycache__",
"build",
"dist",
]
target-version = "py312"
line-length = 90
src = ["src", "tests"]
[tool.ruff.lint]
extend-select = [
"C4",
"D200",
"D201",
"D204",
"D205",
"D206",
"D210",
"D211",
"D213",
"D300",
"D400",
"D402",
"D403",
"D404",
"D419",
"E",
"F",
"G010",
"I001",
"INP001",
"N805",
"PERF101",
"PERF102",
"PERF401",
"PERF402",
"PGH004",
"PGH005",
"PIE794",
"PIE796",
"PIE807",
"PIE810",
"RET502",
"RET503",
"RET504",
"RET505",
"RUF015",
"RUF100",
"S101",
"T20",
"UP",
"W",
]
[tool.mypy]
files = ["src", "tests"]
strict = "true"
@@ -1,812 +0,0 @@
# ruff: noqa: D200
# ruff: noqa: D204
# ruff: noqa: D205
# ruff: noqa: D404
# ruff: noqa: W291
# ruff: noqa: D400
# ruff: noqa: E501
import json
import logging
from typing import Any, Literal
import requests
from .grvt_ccxt_base import GrvtCcxtBase
from .grvt_ccxt_env import GrvtEnv, get_grvt_endpoint
from .grvt_ccxt_types import (
Amount,
GrvtInstrumentKind,
GrvtInvalidOrder,
GrvtOrderSide,
GrvtOrderType,
Num,
)
from .grvt_ccxt_utils import (
EnumEncoder,
GrvtOrder,
get_cookie_with_expiration,
get_grvt_order,
get_order_payload,
)
class GrvtCcxt(GrvtCcxtBase):
"""
GrvtCcxt class to interact with Grvt Rest API in synchronous mode.
Args:
env: GrvtEnv (DEV, TESTNET, PROD)
logger: logging.Logger
parameters: dict with trading_account_id, private_key, api_key etc
Examples:
>>> from grvt_api import GrvtCcxt
>>> from grvt_env import GrvtEnv
>>> grvt = GrvtCcxt(env=GrvtEnv.TESTNET)
>>> grvt.fetch_markets()
"""
def __init__(
self,
env: GrvtEnv,
logger: logging.Logger | None = None,
parameters: dict = {},
order_book_ccxt_format: bool = False,
):
"""Initialize the GrvtCcxt instance."""
super().__init__(env, logger, parameters, order_book_ccxt_format)
self._clsname: str = type(self).__name__
self._session: requests.Session = requests.Session()
self._session.headers.update({"Content-Type": "application/json"})
self.refresh_cookie()
# Assign markets here
self.markets: dict[str, dict] = self.load_markets()
def refresh_cookie(self) -> dict | None:
"""Refresh the session cookie."""
if not self.should_refresh_cookie():
return self._cookie
path = get_grvt_endpoint(self.env, "AUTH")
self._cookie = get_cookie_with_expiration(path, self._api_key)
self._path_return_value_map[path] = self._cookie
if self._cookie:
self._session.cookies.update({"gravity": self._cookie["gravity"]})
if self._cookie["X-Grvt-Account-Id"]:
self._session.headers.update(
{"X-Grvt-Account-Id": self._cookie["X-Grvt-Account-Id"]}
)
self.logger.info(
f"refresh_cookie {self._cookie=} {self._session.cookies=} {self._session.headers=}"
)
return self._cookie
# PRIVATE API CALLS
def _auth_and_post(self, path: str, payload: dict) -> dict:
FN = f"_auth_and_post {path=}"
MAX_LEN_TO_LOG = 1280
response: dict = {}
if not path:
self.logger.warning(f"{FN} Invalid path {path=} {payload=}")
raise GrvtInvalidOrder(f"{FN} Invalid path {path=} {payload=}")
# Always see if need to referesh cookie before sending a request
self.refresh_cookie()
payload_json = json.dumps(payload, cls=EnumEncoder)
self.logger.info(f"{FN} {payload=}\n{payload_json=}")
return_value = self._session.post(path, data=payload_json, timeout=5)
return_text: str = ""
try:
return_text = return_value.text
response = return_value.json()
except Exception as err:
self.logger.warning(f"{FN} Unable to parse {return_value=} as json. {err=}")
if not return_value.ok:
self.logger.warning(f"{FN} ERROR {payload_json=}\n{return_value=}\n{response=}")
else:
if len(return_text) > MAX_LEN_TO_LOG:
self.logger.debug(f"{FN} OK {return_value=} {response=}")
self.logger.info(f"{FN} OK {return_value=} response=**TOO LONG**")
else:
self.logger.info(f"{FN} OK {return_value=} {response=}")
self._path_return_value_map[path] = response
return response
def _create_grvt_order(self, order: GrvtOrder) -> dict:
"""
Send a GrvtOrder object to the exchange.
:param order: The GrvtOrder object.
Return: dictionary representing the order response.
"""
FN = f"{self._clsname} _create_grvt_order cloid:{order.metadata.client_order_id}"
order_payload = get_order_payload(
order,
private_key=self._private_key,
env=self.env,
instruments=self.markets,
)
path = get_grvt_endpoint(self.env, "CREATE_ORDER")
self.logger.info(f"{FN} {path=} {order_payload=}")
response: dict = self._auth_and_post(path, payload=order_payload)
if response.get("result") is None:
self.logger.error(f"{FN} Error: {response}")
return {}
self.logger.info(
f"{FN} Order created:"
f"{response.get('result', {}).get('metadata', {}).get('client_order_id')}"
)
return response.get("result", {})
def create_order(
self,
symbol: str,
order_type: GrvtOrderType,
side: GrvtOrderSide,
amount: Amount,
price: Num = None,
params={},
) -> dict:
"""Ccxt compliant signature."""
self._check_account_auth()
self._check_valid_symbol(symbol)
# Validate order fields
self._check_order_arguments(order_type, side, amount, price)
# create GrvtOrder object
order_duration_secs = params.get("order_duration_secs", 24 * 60 * 60)
order = get_grvt_order(
sub_account_id=self.get_trading_account_id(),
symbol=symbol,
order_type=order_type,
side=side,
amount=amount,
limit_price=price,
order_duration_secs=order_duration_secs,
params=params,
)
return self._create_grvt_order(order)
def create_limit_order(
self,
symbol: str,
side: GrvtOrderSide,
amount: Amount,
price: Num = None,
params={},
) -> dict:
return self.create_order(symbol, "limit", side, amount, price, params)
def cancel_all_orders(
self,
params: dict = {},
) -> bool:
"""
Ccxt compliant signature BUT lacks symbol
Cancel all orders for a sub-account.
params: dictionary with parameters. Valid keys:<br>
`kind` (str): instrument kind. Valid values: 'PERPETUAL'.<br>
`base` (str): base currency. If missing/empty then fetch
orders for all base currencies.<br>
`quote` (str): quote currency. Defaults to all.<br>
"""
self._check_account_auth()
FN = f"{self._clsname} cancel_all_orders"
payload: dict = self._get_payload_cancel_all_orders(params)
path = get_grvt_endpoint(self.env, "CANCEL_ALL_ORDERS")
response: dict = self._auth_and_post(path, payload)
cancel_ack = response.get("result", {}).get("ack")
if not cancel_ack:
self.logger.warning(f"{FN} failed to cancel orders: {response=}")
return False
self.logger.info(f"{FN} Cancelled {response=}")
return True
def cancel_order(
self,
id: str | None = None,
symbol: str | None = None,
params: dict = {},
) -> bool:
"""
Ccxt compliant signature
Cancel specific order for the account.<br>
Private call requires authorization.<br>
See [Cancel order](https://api-docs.grvt.io/trading_api/#cancel-order)
for details.<br>.
Args:
id (str): exchange assigned order ID<br>
symbol (str): trading symbol<br>
params:
* client_order_id (str): client assigned order ID<br>
* time_to_live_ms (str): lifetime of cancel requiest in millisecs<br>
Returns:
True if cancel request was acked by exchange. False otherwise.<br>
"""
FN = f"{self._clsname} cancel_order"
self._check_account_auth()
payload: dict = {
"sub_account_id": self.get_trading_account_id(),
}
if id:
payload["order_id"] = str(id)
elif "client_order_id" in params:
payload["client_order_id"] = str(params["client_order_id"])
else:
raise GrvtInvalidOrder(f"{FN} requires either order_id or client_order_id")
if "time_to_live_ms" in params:
payload["time_to_live_ms"] = str(params["time_to_live_ms"])
path = get_grvt_endpoint(self.env, "CANCEL_ORDER")
self.logger.info(
f"{FN} Send cancel {payload=} for trading_account_id={self.get_trading_account_id()}"
)
response: dict = self._auth_and_post(path, payload)
cancel_ack = response.get("result", {}).get("ack")
if not cancel_ack:
self.logger.warning(f"{FN} failed to cancel order: {response=}")
return False
self.logger.info(f"{FN} Cancelled {response=}")
return True
def set_derisk_mm_ratio(self, ratio: str) -> bool:
"""
Ccxt compliant signature
Set the Derisk to Maintenance marginb ratio for the account.
Private call requires authorization.
See [Set Derisk M M ratio](https://api-docs.grvt.io/trading_api/#set-derisk-m-m-ratio)
for details.
Args:
ratio (Amount): The new derisking market making ratio.
Returns:
True if the request was acknowledged by the exchange. False otherwise.
"""
FN = f"{self._clsname} set_derisk_mm_ratio"
self._check_account_auth()
payload: dict[str, str | dict] = self._get_set_derisk_mm_ratio_payload(str(ratio))
path = get_grvt_endpoint(self.env, "SET_DERISK_MM_RATIO")
self.logger.info(
f"{FN} Send {payload=} for trading_account_id={self.get_trading_account_id()}"
)
response: dict = self._auth_and_post(path, payload)
# set_ack = response.get("result", {}).get("ack")
# if not set_ack:
# self.logger.warning(f"{FN} failed to set derisk_mm_ratio: {response=}")
# return False
self.logger.info(f"{FN} Set derisk_mm_ratio {response=}")
return True
def fetch_open_orders(
self,
symbol: str | None = None,
since: int | None = None,
limit: int | None = None,
params: dict = {},
) -> list[dict]:
"""
Ccxt compliant signature
Fetch open orders for the account.<br>
Private call requires authorization.<br>
See [Open orders](https://api-docs.grvt.io/trading_api/#open-orders)
for details.<br>.
Args:
symbol: (str) get orders for this symbol only.<br>
since: ccxt-compliant argument, NOT SUPPORTED.<br>
limit: ccxt-compliant argument, NOT SUPPORTED.<br>
params: dictionary with parameters. Valid keys:<br>
`kind` (str): instrument kind. Valid values: 'PERPETUAL'.<br>
`base` (str): base currency. If missing/empty then fetch
orders for all base currencies.<br>
`quote` (str): quote currency. Defaults to all.<br>
Returns:
a list of dictionaries, each dict represent an order.<br>
"""
self._check_account_auth()
# Prepare request payload
payload = self._get_payload_fetch_open_orders(symbol, params)
# Post payload and parse the response
path = get_grvt_endpoint(self.env, "GET_OPEN_ORDERS")
response: dict = self._auth_and_post(path, payload)
open_orders: list = response.get("result", [])
if symbol:
open_orders = [
o for o in open_orders if o.get("legs") and o["legs"][0].get("instrument") == symbol
]
return open_orders
def fetch_order(
self,
id: str | None = None,
params: dict = {},
) -> dict:
"""
Ccxt compliant signature
Get Order status by order_id or client_order_id
Private call requires authorization.<br>
See [Open orders](https://api-docs.grvt.io/trading_api/#open-orders)
for details.<br>
Args:
id: (str) order_id to fetch.<br>
params: dictionary with parameters. Valid keys:<br>
`client_order_id` (int): client assigned order ID.<br>
Return: dict with order's details or {} if order was NOT found.
"""
self._check_account_auth()
payload = {
"sub_account_id": self.get_trading_account_id(),
}
if id:
payload["order_id"] = id
elif "client_order_id" in params:
payload["client_order_id"] = str(params["client_order_id"])
else:
raise GrvtInvalidOrder(
f"{self._clsname} fetch_order() requires order_id or params['client_order_id']"
)
path = get_grvt_endpoint(self.env, "GET_ORDER")
response: dict = self._auth_and_post(path, payload)
return response
def fetch_order_history(self, params: dict = {}) -> dict:
"""
Ccxt compliant signature, HISTORICAL data.<br>
Get Order status by order_id or client_order_id
Private call requires authorization.<br>
See [Order History](https://api-docs.grvt.io/trading_api/#order-history)
for details.<br>
Args:
params: dictionary with parameters. Valid keys:<br>
`kind`: (str) - The kind filter to apply. Defaults to all kinds.<br>
`base`: (str) - The base currency filter. Defaults to all base currencies.<br>
`quote`: (str) - The quote currency filter. Defaults to all quote currencies.<br>
`expiration`: (int) The expiration time in nanoseconds. Defaults to all.<br>
`strike_price`: (str) The strike price to apply. Defaults to all strike prices.<br>
`limit`: (int) The limit to query for. Defaults to 500; Max 1000.<br>
`cursor`: (str) The cursor to use for pagination. If nil, return the first page.<br>
Return: a dictionary with keys:
`total` : total number of account history snapshots.<br>
`next` : cursor for the next page.<br>
`result` : a list of dictionaries, each dict represent an order state.<br>.
"""
self._check_account_auth()
payload = self._get_payload_fetch_order_history(params)
path = get_grvt_endpoint(self.env, "GET_ORDER_HISTORY")
response: dict = self._auth_and_post(path, payload)
return response
def get_account_summary(
self, type: Literal["sub-account", "funding", "aggregated"] = "sub-account"
) -> dict:
"""
Return: The account summary.
Private call requires authorization.<br>
See [Account Summary](https://api-docs.grvt.io/trading_api/#sub-account_summary)
for details.<br>
Returns: dictionary with account data.<br>.
"""
FN = f"{self._clsname} get_account_summary {type=}"
self._check_account_auth()
payload = {}
if type == "sub-account":
path = get_grvt_endpoint(self.env, "GET_ACCOUNT_SUMMARY")
payload = {"sub_account_id": self.get_trading_account_id()}
elif type == "funding":
path = get_grvt_endpoint(self.env, "GET_FUNDING_ACCOUNT_SUMMARY")
elif type == "aggregated":
path = get_grvt_endpoint(self.env, "GET_AGGREGATED_ACCOUNT_SUMMARY")
else:
raise GrvtInvalidOrder(f"{FN} Invalid account summary type {type}")
response: dict = self._auth_and_post(path, payload=payload)
sub_account: dict = response.get("result", {})
if not sub_account:
self.logger.info(f"{FN} No account summary for {path=} {payload=}")
return sub_account
def fetch_balance(
self, type: Literal["sub-account", "funding", "aggregated"] = "sub-account"
) -> dict:
"""
Ccxt compliant signature
Fetch balances for the account.<br>
Private call requires authorization.<br>
See [Account Summary](https://api-docs.grvt.io/trading_api/#sub-account_summary)
for details.<br>.
Args:
type: (str) - The type of account to fetch balances for. Defaults to 'sub-account'.
Valid values: 'sub-account', 'funding', 'aggregated'.
Returns: dictionary with ccxt-compliant balance data https://docs.ccxt.com/#/README?id=account-balance.<br>.
"""
account_summary: dict = self.get_account_summary(type)
return self._get_balances_from_account_summary(account_summary)
def fetch_account_history(self, params: dict = {}, limit: int = 500) -> dict:
"""
HISTORICAL data.<br>
Get account history.<br>
Private call requires authorization.<br>
See [Account History](https://api-docs.grvt.io/trading_api/#account-history)
for details.<br>.
Args:
limit: maximum number of account snapshots per page to fetch.<br>
params: dictionary with parameters. Valid keys:<br>
`start_time` (int): fetch orders since this timestamp in nanoseconds.<br>
`end_time` (int): fetch orders until this timestamp in nanoseconds.<br>
`cursor` (str): cursor for the pagination. If cursor is present then we ignore
`start_time` and `end_time`.<br>
Returns:
a dictionary with keys:
`total` : total number of account history snapshots.<br>
`next` : cursor for the next page.<br>
`result` : list of account history snapshots.<br>
"""
self._check_account_auth()
# Prepare request payload
payload = self._get_payload_fetch_account_history(limit, params)
# Post payload and parse the response
path = get_grvt_endpoint(self.env, "GET_ACCOUNT_HISTORY")
response: dict = self._auth_and_post(path, payload=payload)
return response
def fetch_positions(self, symbols: list[str] = [], params={}):
"""
Ccxt compliant signature
Fetch positions for the account.<br>
Private call requires authorization.<br>
See [Positions](https://api-docs.grvt.io/trading_api/#positions)
for details.<br>.
Args:
symbols: list(str) get positions for these symbols only.<br>
Returns: list of dictionaries, each dict represent a position.<br>
"""
self._check_account_auth()
# Prepare request payload
payload = self._get_payload_fetch_positions(symbols, params)
# Post payload and parse the response
path = get_grvt_endpoint(self.env, "GET_POSITIONS")
response: dict = self._auth_and_post(path, payload)
positions: list = response.get("result", [])
if symbols:
self.logger.info(f"fetch_positions filter positions by {symbols=}")
positions = [p for p in positions if p.get("instrument") in symbols]
return positions
def fetch_my_trades(
self,
symbol: str | None = None,
since: int | None = None,
limit: int | None = None,
params: dict = {},
) -> dict:
"""
Ccxt compliant signature, HISTORICAL data.<br>
Fetch past trades for the account.<br>
Private call requires authorization.<br>
See [Private Trade History](https://api-docs.grvt.io/trading_api/#private-trade-history)
for details.<br>.
Args:
symbol: get trades for this symbol only.<br>
since: fetch trades since this timestamp in nanoseconds.<br>
limit: maximum number of trades to fetch.<br>
params: dictionary with parameters. Valid keys:<br>
`cursor` (str): cursor for the pagination.
If cursor is present then we ignore other filters.<br>
`kind` (str): instrument kind. Valid values: 'PERPETUAL'.<br>
`base` (str): base currency. If missing/empty then fetch
orders for all base currencies.<br>
`quote` (str): quote currency. Default: 'USDT'.<br>
Returns:
a dictionary with keys:
`total` : total number of account history snapshots.<br>
`next` : cursor for the next page.<br>
`result` : a list of dictionaries, each dict represent a trade.<br>
"""
self._check_account_auth()
# Prepare request payload
payload = self._get_payload_fetch_my_trades(symbol, since, limit, params)
# Post payload and parse the response
path = get_grvt_endpoint(self.env, "GET_FILL_HISTORY")
response: dict = self._auth_and_post(path, payload=payload)
if symbol:
# filter result by symbol
trades: list = response.get("result", [])
trades = [t for t in trades if t.get("instrument") == symbol]
response["result"] = trades
return response
# **************** PUBLIC API CALLS
def load_markets(self) -> dict[str, dict]:
self.logger.info("load_markets START")
instruments = self.fetch_markets(
params={
"kind": GrvtInstrumentKind.PERPETUAL,
# "base": "BTC",
# "quote": "USDT",
}
)
if instruments:
self.markets = {
str(i.get("instrument", "")): i for i in instruments if i.get("instrument")
}
self.logger.info(f"load_markets: loaded {len(self.markets)} markets.")
else:
self.logger.warning("load_markets: No markets found.")
return self.markets
def fetch_markets(
self,
params: dict = {},
) -> list[dict]:
"""
Ccxt compliant signature
Retrieve the list of all instruments of matching kind, base and quote
supported by the exchange.
Params: dict with keys:<br>
`is_active` (bool) - defaults to True.<br>
`limit` (int) - defaiults to 20.<br>
`kind` (str): instrument kind. Valid values: 'PERPETUAL'.<br>
`base` (str): base currency. If missing/empty then fetch
orders for all base currencies.<br>
`quote` (str): quote currency. Default: 'USDT'.<br>
Returns: list of dictionaries per instrument with keys:<br>
`instrument`: symbol e.g. 'BTC_USDT_Perp'.<br>
`instrument_hash`: hashed symbol for order signing e.g. '0x030501'.<br>
`base`: base currency e.g. 'BTC'.<br>
`quote`: quote currency e.g. 'USDT'.<br>
`kind`: kind of instrument 'PERPETUAL'/'FUTURE'.<br>
'base_decimals': size multiplier for order signing.<br>
`tick_size`: price tick size.<br>
`min_size`: minimum order size.<br>
"""
# Prepare request payload
payload = self._get_payload_fetch_markets(params)
# Make the POST request to get all instruments
path = get_grvt_endpoint(self.env, "GET_INSTRUMENTS")
response: dict = self._auth_and_post(path, payload)
return response.get("result", [])
def fetch_all_markets(
self,
is_active: bool | None = True,
) -> list[dict]:
"""
Retrieve the list of all instruments supported by the exchange.<br>
Params:<br>
`is_active` (bool) - defaults to True.<br>.
Returns: list of dictionaries per instrument. See fetch_markets().<br>
"""
# Prepare request payload
payload = {"is_active": is_active}
# Make the POST request to get all instruments
path = get_grvt_endpoint(self.env, "GET_ALL_INSTRUMENTS")
response: dict = self._auth_and_post(path, payload)
return response.get("result", [])
def fetch_market(self, symbol: str) -> dict:
"""
Retrieve the instrument object for a given symbol.
:param symbol: The symbol of the instrument.
"""
# Make the POST request to get all instruments
path = get_grvt_endpoint(self.env, "GET_INSTRUMENT")
response: dict = self._auth_and_post(path, payload={"instrument": symbol})
return response.get("result", [])
def fetch_ticker(self, symbol: str, params: dict = {}) -> dict:
"""
Ccxt compliant signature
Retrieve the ticker of a given symbol.
:param symbol: The instrument name.
:return: The ticker dictionary of the instrument.
"""
# {'event_time': '1724252426000000000', 'instrument': 'BTC_USDT_Perp',
# 'mark_price': '59373870996065', 'index_price': '59395287961367',
# 'last_price': '99000000000000', 'last_size': '9917000000', 'mid_price': '59569850000000',
# 'best_bid_price': '59866000000000', 'best_bid_size': '23705000000', 'best_ask_price':
# '592737000', 'best_ask_size': '21678', 'funding_rate_curr': 2544, 'funding_rate_avg': 0,
# 'interest_rate': 0, 'forward_price': '0', 'buy_volume_u': '401930000000',
# 'sell_volume_u': '1218289000000', 'buy_volume_q': '34637817515500',
# 'sell_volume_q': '68764000329900', 'high_price': '3435450000', 'low_price': '100000',
# 'open_price': '32554000000000', 'open_interest': '8174350000000',
# 'long_short_ratio': 1.0948905}
path = get_grvt_endpoint(self.env, "GET_TICKER")
response: dict = self._auth_and_post(path, payload={"instrument": symbol})
return response.get("result", [])
def fetch_mini_ticker(self, symbol: str) -> dict:
"""
Retrieve the mini-ticker of a given symbol.
:param symbol: The instrument name.
:return: The mini-ticker dictionary of the instrument.
"""
# {'event_time': '1724252426000000000', 'instrument': 'BTC_USDT_Perp',
# 'mark_price': '59373870996065', 'index_price': '59395287961367',
# 'last_price': '99000000000000', 'last_size': '9917000000', 'mid_price': '59569850000000',
# 'best_bid_price': '59866000000000', 'best_bid_size': '23705000000', 'best_ask_price':
# '59273700000000', 'best_ask_size': '21678000000'}
path = get_grvt_endpoint(self.env, "GET_MINI_TICKER")
response: dict = self._auth_and_post(path, payload={"instrument": symbol})
return response.get("result", [])
def fetch_order_book(self, symbol: str, limit: int = 10, params={}) -> dict:
"""
Ccxt compliant signature
Retrieve the order book of a given symbol.
:param symbol: The instrument name.
:return: The order book dictionary of the instrument.
"""
# {'event_time': '0', 'instrument': 'BTC_USDT_Perp',
# 'bids': [{'price': '100000000', 'size': '86353000000', 'num_orders': 4},...]
# 'asks': [{'price': '59273700000000', 'size': '21678000000', 'num_orders': 1}, ...]
payload = {"instrument": symbol, "aggregate": 1}
if limit:
payload["depth"] = limit
path = get_grvt_endpoint(self.env, "GET_ORDER_BOOK")
response: dict = self._auth_and_post(path, payload=payload)
if self.is_order_book_ccxt_format():
# Convert to ccxt format
return self.convert_grvt_ob_to_ccxt(response.get("result", {}))
return response.get("result", {})
def fetch_recent_trades(
self,
symbol: str,
limit: int | None = None,
) -> list:
"""
Retrieve recent trades of a given instrument.
:param symbol: The instrument name.
:return: The list of trades for the instrument.
"""
# List of {'event_time': '1724248876870635916', 'instrument': 'ETH_USDT_Perp',
# 'is_taker_buyer': True, 'size': '24000000000', 'price': '2600000000000',
# 'mark_price': '2591055564869', 'index_price': '2592459142472', 'interest_rate': 0,
# 'forward_price': '0', 'trade_id': '729726', 'venue': 'ORDERBOOK'}
payload: dict[str, str | int] = {"instrument": symbol}
if limit:
payload["limit"] = limit
path = get_grvt_endpoint(self.env, "GET_TRADES")
response: dict = self._auth_and_post(path, payload=payload)
return response.get("result", [])
def fetch_trades(
self,
symbol: str,
since: int | None = None,
limit: int = 10,
params: dict = {},
) -> dict:
"""
Ccxt compliant signature, HISTORICAL data.<br>
Retrieve trade history of a given instrument.
:param symbol: The instrument name.
:return: dict with field 'result' containing a list of trades.
"""
# List of {'event_time': '1724248876870635916', 'instrument': 'ETH_USDT_Perp',
# 'is_taker_buyer': True, 'size': '24000000000', 'price': '2600000000000',
# 'mark_price': '2591055564869', 'index_price': '2592459142472', 'interest_rate': 0,
# 'forward_price': '0', 'trade_id': '729726', 'venue': 'ORDERBOOK'}
payload: dict = self._get_payload_fetch_trades(
symbol,
since=since,
limit=limit,
params=params,
)
path: str = get_grvt_endpoint(self.env, "GET_TRADE_HISTORY")
response: dict = self._auth_and_post(path, payload=payload)
return response
def fetch_funding_rate_history(
self,
symbol: str,
since: int = 0,
limit: int = 1_000,
params: dict = {},
) -> dict:
"""
Ccxt compliant signature, HISTORICAL data.<br>
Retrieve the funding rates history of a given instrument.<br>
Args:
symbol (str): The instrument name.<br>
since (int): fetch trades since this timestamp in nanoseconds.<br>
limit: int - maximum number of trades to fetch.<br>
params: dictionary with parameters. Valid keys:<br>
`cursor` (str): cursor for the pagination.
If cursor is present then we ignore other filters.<br>
`end_time` (int): end time in nanoseconds.<br>
Returns:
dict with field 'result' containing list of dictionaries repesenting funding rate
at a point in time with fields:<br>
`instrument` (str): instrument name.<br>
'funding_rate' (float): funding rate.<br>
'funding_time' (int): funding time in nanoseconds.<br>
'mark_price' (float): mark price.<br>.
"""
payload: dict[str, str | int] = {"instrument": symbol}
if params.get("cursor"):
payload["cursor"] = params["cursor"]
else:
if since:
payload["start_time"] = str(since)
if params.get("end_time"):
payload["end_time"] = str(params["end_time"])
if limit:
payload["limit"] = int(limit)
path = get_grvt_endpoint(self.env, "GET_FUNDING")
response: dict = self._auth_and_post(path, payload=payload)
return response
def fetch_ohlcv(
self,
symbol: str,
timeframe="1m",
since: int = 0,
limit: int = 10,
params={},
) -> dict:
"""
Ccxt compliant signature, HISTORICAL data.
Retrieve the ohlc history of a given instrument.
Args:
symbol: The instrument name.
timeframe: The timeframe of the ohlc. See `ccxt_interval_to_grvt_candlestick_interval`.
since: fetch ohlc since this timestamp in nanoseconds.
limit: maximum number of ohlc to fetch.
params: dictionary with parameters. Valid keys:
`cursor` (str): cursor for the pagination.
If cursor is present then we ignore other filters.
`end_time` (int): end time in nanoseconds.
`candle_type` (str): candle type. Valid values: 'TRADE', 'MARK', 'INDEX'.
Returns:
dict with field 'result' containing a list of dictionaries, each dict representing a candlestick with fields:<br>
`instrument` - instrument name.<br>
`open_time` - start of interval in nanoseconds.<br>
`close_time` - end of interval in nanoseconds.<br>
`open` - opening price.<br>
`close` - closing price.<br>
`high` - highest price.<br>
`low` - lowest price.<br>
`volume_u` - volume in units.<br>
`volume_q` - volume in quote(USDT).<br>
`trades` - number of trades.<br>
"""
FN = f"{self._clsname} fetch_ohlcv"
payload: dict[str, Any] = self._get_payload_fetch_ohlcv(
symbol, timeframe, since, limit, params
)
self.logger.info(f"{FN} {payload=}")
path = get_grvt_endpoint(self.env, "GET_CANDLESTICK")
return self._auth_and_post(path, payload=payload)
# Vault Management APIs
def fetch_vault_manager_investor_history(self, only_own_investments: bool = False) -> dict:
payload: dict = self._get_fetch_vault_manager_investor_history_payload(
vault_id=self.get_trading_account_id(),
only_own_investments=only_own_investments, # Default to False to fetch all investments
)
path: str = get_grvt_endpoint(self.env, "GET_VAULT_MANAGER_INVESTOR_HISTORY")
# path = "https://trades.grvt.io/full/v1/vault_manager_investor_history"
return self._auth_and_post(path, payload=payload)
def fetch_vault_redemption_queue(self):
payload: dict = self._get_fetch_vault_redemption_queue_payload(
vault_id=self.get_trading_account_id()
)
path: str = get_grvt_endpoint(self.env, "GET_VAULT_REDEMPTION_QUEUE")
# path = "https://trades.grvt.io/full/v1/vault_view_redemption_queue"
return self._auth_and_post(path, payload=payload)
@@ -1,586 +0,0 @@
# ruff: noqa: D200
# ruff: noqa: D204
# ruff: noqa: D205
# ruff: noqa: D404
# ruff: noqa: W291
# ruff: noqa: D400
# ruff: noqa: E501
import logging
import time
from datetime import datetime
from decimal import Decimal
from typing import Any, get_args
from .grvt_ccxt_env import GrvtEnv
from .grvt_ccxt_types import (
CandlestickInterval,
CandlestickType,
GrvtInvalidOrder,
GrvtOrderSide,
GrvtOrderType,
Num,
ccxt_interval_to_grvt_candlestick_interval,
)
from .grvt_ccxt_utils import get_kuq_from_symbol, sign_derisk_mm_ratio_request
# COOKIE_REFRESH_INTERVAL_SECS = 60 * 60 # 30 minutes
class GrvtCcxtBase:
"""
GrvtCcxtBase is an abstract class for other Grvt Rest
and WebSocket connectivity classes.
Args:
env: GrvtCcxtBase (DEV, TESTNET, PROD)
logger (logging.Logger, optional). Defaults to None.
parameters: (dict, optional). Dict with trading_account_id, private_key, api_key etc
defaults to empty.
"""
def __init__(
self,
env: GrvtEnv,
logger: logging.Logger | None = None,
parameters: dict = {},
order_book_ccxt_format: bool = False,
):
"""Initialize the GrvtCcxtBase part."""
self.name: str = "GRVT"
self.logger = logger or logging.getLogger(__name__)
self.env: GrvtEnv = env
self._trading_account_id: str | None = parameters.get("trading_account_id")
self._private_key: str = str(parameters.get("private_key", ""))
self._api_key: str = str(parameters.get("api_key", ""))
self._order_book_ccxt_format: bool = order_book_ccxt_format
self._path_return_value_map: dict = {}
self._cookie: dict | None = None
self.markets: dict = {}
self._clsname: str = type(self).__name__
self.logger.info(f"GrvtCcxtBase: {self.env=}, {self._trading_account_id=}")
def describe(self) -> list[str]:
"""Returns the description of the class methods."""
return [
"create_order",
"create_limit_order",
"cancel_all_orders",
"cancel_order",
"fetch_balance",
"fetch_open_orders",
"fetch_order",
"fetch_order_history",
"get_account_summary",
"fetch_account_history",
"fetch_positions",
"fetch_my_trades",
"load_markets",
"fetch_markets",
"fetch_all_markets",
"fetch_market",
"fetch_ticker",
"fetch_mini_ticker",
"fetch_order_book",
"fetch_recent_trades",
"fetch_trades",
"fetch_funding_rate_history",
"fetch_ohlcv",
]
def get_trading_account_id(self) -> str:
"""Returns the trading account id."""
return self._trading_account_id or ""
def is_order_book_ccxt_format(self) -> bool:
"""Returns True if order book should be returned in CCXT format."""
return self._order_book_ccxt_format
def should_refresh_cookie(self) -> bool:
"""
Retuns:
True if this object has API key and the session cookie should be refreshed.
False - otherwise.
"""
if not self._api_key:
return False
time_till_expiration = None
if self._cookie and "expires" in self._cookie:
time_till_expiration = self._cookie["expires"] - time.time()
is_cookie_fresh = time_till_expiration is not None and time_till_expiration > 5
if not is_cookie_fresh:
self.logger.info(
f"cookie should be refreshed {self._cookie=} now={time.time()}"
f" {time_till_expiration=} secs"
)
return not is_cookie_fresh
def get_path_return_value_map(self) -> dict:
"""Returns the path return value map."""
return self._path_return_value_map
def get_endpoint_return_value(self, endpoint: str) -> dict:
"""Returns the return value for the endpoint."""
return self._path_return_value_map.get(endpoint, {})
def was_path_called(self, path: str) -> bool:
"""Returns True if the path was called."""
return path in self._path_return_value_map
# PRIVATE API CALLS
def _check_order_arguments(
self, order_type: GrvtOrderType, side: GrvtOrderSide, amount: Num, price: Num
) -> None:
FN = f"{self._clsname} _check_order_arguments"
if order_type not in get_args(GrvtOrderType):
raise GrvtInvalidOrder(f"{FN}: order_type should be one of {get_args(GrvtOrderType)}")
if side not in get_args(GrvtOrderSide):
raise GrvtInvalidOrder(f"{FN}: side should be one of {get_args(GrvtOrderSide)}")
if order_type == "limit":
if price is None or Decimal(price) <= Decimal("0"):
raise GrvtInvalidOrder(f"{FN}: requires a price argument for a limit order")
elif order_type == "market":
if price:
raise GrvtInvalidOrder(
f"{FN}: should not have a positive price argument for a market order"
)
if not amount or Decimal(amount) < Decimal("0"):
raise GrvtInvalidOrder(f"{FN}: amount should be above 0")
def _check_account_auth(self) -> bool:
if not self.get_trading_account_id():
raise GrvtInvalidOrder(f"{self._clsname}: this action requires a trading_account_id")
return True
def _check_valid_symbol(self, symbol: str) -> bool:
if not self.markets:
raise GrvtInvalidOrder(f"{self._clsname}: markets not loaded")
market = self.markets.get(symbol)
if not market:
raise GrvtInvalidOrder(f"{self._clsname}: {symbol=} not found")
return True
def _get_payload_cancel_all_orders(
self,
params: dict = {},
) -> dict:
"""
Prepares payload for fetch_order_history() method.<br>.
Args:
params: (dict) with possible keys as:.<br>
`kind`: (str) - The kind filter to apply. Defaults to all kinds.<br>
`base`: (str) - The base currency filter. Defaults to all base currencies.<br>
`quote`: (str) - The quote currency filter. Defaults to all quote currencies.<br>
Returns: a dictionary with a payload for Rest API call to cancel all orders.<br>
"""
payload: dict[str, str | int | bool | list] = {
"sub_account_id": str(self.get_trading_account_id())
}
if "kind" in params:
payload["kind"] = [params["kind"]]
if "base" in params:
payload["base"] = [params["base"]]
if "quote" in params:
payload["quote"] = [params["quote"]]
return payload
def _get_payload_fetch_markets(self, params: dict) -> dict:
payload: dict[str, str | int | bool | list] = {}
if params.get("kind"):
payload["kind"] = [params.get("kind")]
if params.get("base"):
payload["base"] = [params.get("base")]
if params.get("quote"):
payload["quote"] = [params.get("quote")]
payload["limit"] = int(params.get("limit", 1_000))
payload["is_active"] = bool(params.get("is_active", True))
return payload
def _get_payload_fetch_my_trades(
self,
symbol: str | None = None,
since: int | None = None,
limit: int | None = None,
params: dict = {},
) -> dict:
"""
Prepares payload for fetch_my_trades() method.<br>.
Args:
symbol: get trades for this symbol only.<br>
since: fetch trades since this timestamp in nanoseconds.<br>
limit: maximum number of trades to fetch.<br>
params: dictionary with parameters. Valid keys:<br>
`cursor` (str): cursor for the pagination.
If cursor is present then we ignore other filters.<br>
`kind` (str): instrument kind. Valid values: 'PERPETUAL'.<br>
`base` (str): base currency. If missing/empty then fetch
orders for all base currencies.<br>
`quote` (str): quote currency. Default: 'USDT'.<br>
`end_time` (int): fetch trades until this timestamp in nanoseconds.<br>
Returns:
a dictionary with a payload for Rest API call to fetch trades.<br>
"""
payload: dict[str, str | int | list] = {
"sub_account_id": str(self.get_trading_account_id())
}
if params.get("cursor"):
payload["cursor"] = params["cursor"]
else:
if symbol:
payload["instrument"] = symbol
else:
if "kind" in params:
payload["kind"] = [params["kind"]]
if "base" in params:
payload["base"] = [params["base"]]
if "quote" in params:
payload["quote"] = [params["quote"]]
if since:
payload["start_time"] = str(since)
if params.get("end_time"):
payload["end_time"] = str(params["end_time"])
if limit:
payload["limit"] = int(limit)
return payload
def _get_payload_fetch_trades(
self,
symbol: str,
since: int | None = None,
limit: int = 1_000,
params: dict = {},
) -> dict:
"""
Prepares payload for fetch_trades() method.<br>.
Args:
symbol: get trades for this symbol only.<br>
since: fetch trades since this timestamp in nanoseconds.<br>
limit: maximum number of trades to fetch.<br>
params: dictionary with parameters. Valid keys:<br>
`cursor` (str): cursor for the pagination.
If cursor is present then we ignore other filters.<br>
`kind` (str): instrument kind. Valid values: 'PERPETUAL'.<br>
`base` (str): base currency. If missing/empty then fetch
orders for all base currencies.<br>
`quote` (str): quote currency. Default: 'USDT'.<br>
Returns:
a dictionary with a payload for Rest API call to fetch trades.<br>
"""
payload: dict[str, str | int] = {
"sub_account_id": str(self.get_trading_account_id()),
"instrument": symbol,
}
if params.get("cursor"):
payload["cursor"] = params["cursor"]
else:
if since:
payload["start_time"] = str(since)
if params.get("end_time"):
payload["end_time"] = str(params["end_time"])
payload["limit"] = limit
return payload
def _get_payload_fetch_account_history(
self,
# since: int | None = None,
limit: int = 500,
params: dict = {},
) -> dict:
"""
Prepares payload for fetch_account_history() method.<br>.
Args:
limit: maximum number of trades to fetch.<br>
params: dictionary with parameters. Valid keys:<br>
`start_time` (int): fetch orders since this timestamp in nanoseconds.<br>
`end_time` (int): fetch orders until this timestamp in nanoseconds.<br>
`cursor` (int):cursor for the pagination. If cursor is present then we ignore
`start_time` and `end_time`.<br>
Returns:
a dictionary with a payload for Rest API call to fetch account history.<br>
"""
payload: dict[str, str | int] = {"sub_account_id": str(self.get_trading_account_id())}
if params.get("cursor"):
payload["cursor"] = params["cursor"]
else:
start_time = params.get("start_time")
end_time = params.get("end_time")
if start_time:
payload["start_time"] = str(start_time)
if end_time:
payload["end_time"] = str(end_time)
payload["limit"] = limit | 500
return payload
def _get_payload_fetch_positions(self, symbols: list[str] = [], params={}) -> dict:
"""
Prepares payload for fetch_positions() method.<br>.
Args:
symbols: list(str) get positions for these symbols only.<br>
Returns: a dictionary with a payload for Rest API call to fetch positions.<br>
"""
payload: dict[str, str | int | bool | list] = {
"sub_account_id": str(self.get_trading_account_id())
}
if symbols:
ks, us, qs = [], [], []
for symbol in symbols:
try:
k, u, q = get_kuq_from_symbol(symbol)
ks.append(k)
us.append(u)
qs.append(q)
except Exception as e:
raise GrvtInvalidOrder(f"Invalid symbol {symbol} in fetch_positions {e}")
payload["kind"] = list(set(ks))
payload["base"] = list(set(us))
payload["quote"] = list(set(qs))
else:
if "kind" in params:
payload["kind"] = [params["kind"]]
if "base" in params:
payload["base"] = [params["base"]]
if "quote" in params:
payload["quote"] = [params["quote"]]
return payload
def _get_payload_fetch_order_history(
self,
params: dict,
) -> dict:
"""
Prepares payload for fetch_order_history() method.<br>.
Args:
params: (dict) with possible keys as:.<br>
`kind`: (str) - The kind filter to apply. Defaults to all kinds.<br>
`base`: (str) - The base currency filter. Defaults to all base currencies.<br>
`quote`: (str) - The quote currency filter. Defaults to all quote currencies.<br>
`expiration`: (int) The expiration time in nanoseconds. Defaults to all.<br>
`strike_price`: (str) The strike price to apply. Defaults to all strike prices.<br>
`limit`: (int) The limit to query for. Defaults to 500; Max 1000.<br>
`cursor`: (str) The cursor to use for pagination. If nil, return the first page.<br>
Returns: a dictionary with a payload for Rest API call to fetch order history.<br>
"""
payload: dict[str, str | int | bool | list] = {
"sub_account_id": str(self.get_trading_account_id())
}
if "limit" in params:
payload["limit"] = params["limit"]
if params.get("cursor"):
payload["cursor"] = params["cursor"]
else:
if "kind" in params:
payload["kind"] = [params["kind"]]
if "base" in params:
payload["base"] = [params["base"]]
if "quote" in params:
payload["quote"] = [params["quote"]]
if "expiration" in params:
payload["expiration"] = [params["expiration"]]
if "strike_price" in params:
payload["strike_price"] = [params["strike_price"]]
return payload
def _get_payload_fetch_open_orders(
self,
symbol: str | None = None,
params: dict = {},
) -> dict:
"""
Prepares payload for fetch_order_history() method.<br>.
Args:
params: (dict) with possible keys as:.<br>
`kind`: (str) - The kind filter to apply. Defaults to all kinds.<br>
`base`: (str) - The base currency filter. Defaults to all base currencies.<br>
`quote`: (str) - The quote currency filter. Defaults to all quote currencies.<br>
Returns: a dictionary with a payload for Rest API call to fetch order history.<br>
"""
payload: dict[str, str | int | bool | list] = {
"sub_account_id": str(self.get_trading_account_id())
}
if symbol:
try:
k, u, q = get_kuq_from_symbol(symbol)
payload["kind"] = [k]
payload["base"] = [u]
payload["quote"] = [q]
except Exception as e:
raise GrvtInvalidOrder(f"Invalid symbol {symbol} in fetch_open_orders {e}")
else:
if "kind" in params:
payload["kind"] = [params["kind"]]
if "base" in params:
payload["base"] = [params["base"]]
if "quote" in params:
payload["quote"] = [params["quote"]]
return payload
def _get_payload_fetch_ohlcv(
self,
symbol: str,
timeframe: str,
since: int,
limit: int,
params={},
) -> dict:
"""
Prepares payload for fetch_ohlcv() method.<br>.
Args:
symbol: The instrument name.<br>
timeframe: The timeframe of the ohlc.
See `ccxt_interval_to_grvt_candlestick_interval`.<br>
since: fetch ohlc since this timestamp in nanoseconds.<br>
limit: maximum number of ohlc to fetch.<br>
params: dictionary with parameters. Valid keys:<br>
`cursor` (str): cursor for the pagination.
If cursor is present then we ignore other filters.<br>
`end_time` (int): end time in nanoseconds.<br>
`candle_type` (str): candle type. Valid values: 'TRADE', 'MARK', 'INDEX'.<br>
Returns: a dictionary with a payload for Rest API call to fetch_ohlcv.<br>
See [Candlestick] (https://api-docs.grvt.io/market_data_api/#candlestick_1)
for more details.<br>
"""
if timeframe not in ccxt_interval_to_grvt_candlestick_interval:
raise ValueError(f"Invalid timeframe {timeframe}")
interval: CandlestickInterval = ccxt_interval_to_grvt_candlestick_interval[timeframe]
payload: dict[str, str | int | bool | list] = {"instrument": symbol}
if params.get("cursor"):
payload["cursor"] = params["cursor"]
else:
if interval:
payload["interval"] = interval.value
candle_type = CandlestickType.TRADE
if "candle_type" in params:
candle_type = CandlestickType[params["candle_type"]]
payload["type"] = candle_type.value
if since:
payload["start_time"] = str(since)
if "end_time" in params:
payload["end_time"] = str(params["end_time"])
if limit:
payload["limit"] = int(limit)
return payload
def _get_balances_from_account_summary(self, account_summary: dict) -> dict:
balances: dict = {}
balances["info"] = account_summary.get("spot_balances", [])
balances["timestamp"] = int(int(account_summary.get("event_time", 0)) / 1_000_000)
balances["datetime"] = (
datetime.fromtimestamp(balances["timestamp"] / 1_000).strftime("%Y-%m-%dT%H:%M:%S.%f")[
:-3
]
+ "Z"
)
balances["total"] = {}
balances["free"] = {}
balances["used"] = {}
for currency_balance in account_summary.get("spot_balances", []):
if not currency_balance or not isinstance(currency_balance, dict):
continue
currency: str = currency_balance.get("currency", "")
if not currency:
continue
balances[currency] = {"total": currency_balance.get("balance", "0.0")}
balances["total"][currency] = balances[currency]["total"]
if currency == "USDT":
balances[currency]["free"] = account_summary.get("available_balance", "0.0")
balances[currency]["used"] = str(
Decimal(balances[currency]["total"]) - Decimal(balances[currency]["free"])
)
else:
balances[currency]["free"] = balances[currency]["total"]
balances[currency]["used"] = "0.0"
balances["free"][currency] = balances[currency]["free"]
balances["used"][currency] = balances[currency]["used"]
return balances
def _get_set_derisk_mm_ratio_payload(
self,
ratio: str,
) -> dict[str, str | dict]:
"""
Returns a payload for setting the derisking market making ratio.
"""
payload: dict[str, str | dict] = {
"sub_account_id": self.get_trading_account_id(),
"ratio": str(ratio),
}
signature: dict = sign_derisk_mm_ratio_request(
self.env, int(self.get_trading_account_id()), str(ratio), self._private_key
)
payload["signature"] = signature
return payload
def convert_grvt_ob_to_ccxt(self, order_book: dict) -> dict:
"""
Converts GRVT-specific order book format to CCXT format.
"""
ob_time_ms: int = int(order_book["event_time"]) // 1_000_000
ccxt_ob = {
"symbol": order_book["instrument"],
"bids": [],
"asks": [],
"timestamp": ob_time_ms,
"datetime": datetime.fromtimestamp(ob_time_ms / 1_000).strftime("%Y-%m-%dT%H:%M:%S.%f")[
:-3
]
+ "Z",
"nonce": int(order_book["event_time"]),
}
ccxt_ob["bids"] = [[bid["price"], bid["size"]] for bid in order_book["bids"]]
ccxt_ob["asks"] = [[ask["price"], ask["size"]] for ask in order_book["asks"]]
return ccxt_ob
# Vault Management APIs
def _get_fetch_vault_manager_investor_history_payload(
self,
vault_id: str,
only_own_investments: bool = False,
) -> dict:
"""
Prepares payload for fetch_vault_manager_investor_history() method.<br>.
Args:
vault_id: The vault id to fetch history for.<br>
only_own_investments: If True, fetch only investments by the manager.<br>
Returns:
A dictionary with a payload for Rest API call to fetch vault investor history.
"""
payload: dict[str, str | bool] = {
"vault_id": vault_id,
"only_own_investments": only_own_investments,
}
return payload
def _get_fetch_vault_redemption_queue_payload(
self,
vault_id: str,
) -> dict:
"""
Prepares payload for fetch_vault_redemption_queue() method.<br>.
Args:
vault_id: The vault id to fetch redemption queue for.<br>
Returns:
A dictionary with a payload for Rest API call to fetch vault redemption queue.
"""
return {"vault_id": vault_id}
@@ -1,195 +0,0 @@
# ruff: noqa: D200
# ruff: noqa: D204
# ruff: noqa: D205
# ruff: noqa: D404
# ruff: noqa: W291
# ruff: noqa: D400
# ruff: noqa: E501
import os
from enum import Enum
class GrvtEnv(str, Enum):
PROD = "prod"
TESTNET = "testnet"
STAGING = "staging"
DEV = "dev"
# GrvtEndpointType defines the root path for a family of endpoints
class GrvtEndpointType(str, Enum):
EDGE = "edge"
TRADE_DATA = "tdg"
MARKET_DATA = "mdg"
class GrvtWSEndpointType(str, Enum):
TRADE_DATA = "tdg"
MARKET_DATA = "mdg"
TRADE_DATA_RPC_FULL = "tdg_rpc_full"
MARKET_DATA_RPC_FULL = "mdg_rpc_full"
END_POINT_VERSION = os.getenv("GRVT_END_POINT_VERSION", "v1")
def get_grvt_endpoint_domains(env_name: str) -> dict[GrvtEndpointType, str]:
if env_name == GrvtEnv.PROD.value:
return {
GrvtEndpointType.EDGE: "https://edge.grvt.io",
GrvtEndpointType.TRADE_DATA: "https://trades.grvt.io",
GrvtEndpointType.MARKET_DATA: "https://market-data.grvt.io",
}
if env_name == GrvtEnv.TESTNET.value:
return {
GrvtEndpointType.EDGE: f"https://edge.{env_name}.grvt.io",
GrvtEndpointType.TRADE_DATA: f"https://trades.{env_name}.grvt.io",
GrvtEndpointType.MARKET_DATA: f"https://market-data.{env_name}.grvt.io",
}
if env_name == GrvtEnv.STAGING.value:
return {
GrvtEndpointType.EDGE: f"https://edge.{env_name}.gravitymarkets.io",
GrvtEndpointType.TRADE_DATA: f"https://trades.{env_name}.gravitymarkets.io",
GrvtEndpointType.MARKET_DATA: f"https://market-data.{env_name}.gravitymarkets.io",
}
if env_name == GrvtEnv.DEV.value:
return {
GrvtEndpointType.EDGE: f"https://edge.{env_name}.gravitymarkets.io",
GrvtEndpointType.TRADE_DATA: f"https://trades.{env_name}.gravitymarkets.io",
GrvtEndpointType.MARKET_DATA: f"https://market-data.{env_name}.gravitymarkets.io",
}
return {}
def get_grvt_ws_endpoint(
env: str,
endpoint_type: GrvtWSEndpointType,
) -> str:
"""Returns string pointing to WS endpoint for given environment and endpoint type."""
if env == GrvtEnv.PROD.value:
return {
GrvtWSEndpointType.TRADE_DATA: "wss://trades.grvt.io/ws",
GrvtWSEndpointType.MARKET_DATA: "wss://market-data.grvt.io/ws",
GrvtWSEndpointType.TRADE_DATA_RPC_FULL: "wss://trades.grvt.io/ws/full",
GrvtWSEndpointType.MARKET_DATA_RPC_FULL: "wss://market-data.grvt.io/ws/full",
}.get(endpoint_type, "")
if env == GrvtEnv.TESTNET.value:
return {
GrvtWSEndpointType.TRADE_DATA: f"wss://trades.{env}.grvt.io/ws",
GrvtWSEndpointType.MARKET_DATA: f"wss://market-data.{env}.grvt.io/ws",
GrvtWSEndpointType.TRADE_DATA_RPC_FULL: f"wss://trades.{env}.grvt.io/ws/full",
GrvtWSEndpointType.MARKET_DATA_RPC_FULL: f"wss://market-data.{env}.grvt.io/ws/full",
}.get(endpoint_type, "")
if env == GrvtEnv.STAGING.value:
return {
GrvtWSEndpointType.TRADE_DATA: f"wss://trades.{env}.gravitymarkets.io/ws",
GrvtWSEndpointType.MARKET_DATA: f"wss://market-data.{env}.gravitymarkets.io/ws",
GrvtWSEndpointType.TRADE_DATA_RPC_FULL: f"wss://trades.{env}.gravitymarkets.io/ws/full",
GrvtWSEndpointType.MARKET_DATA_RPC_FULL: f"wss://market-data.{env}.gravitymarkets.io/ws/full",
}.get(endpoint_type, "")
if env == GrvtEnv.DEV.value:
return {
GrvtWSEndpointType.TRADE_DATA: f"wss://trades.{env}.gravitymarkets.io/ws",
GrvtWSEndpointType.MARKET_DATA: f"wss://market-data.{env}.gravitymarkets.io/ws",
GrvtWSEndpointType.TRADE_DATA_RPC_FULL: f"wss://trades.{env}.gravitymarkets.io/ws/full",
GrvtWSEndpointType.MARKET_DATA_RPC_FULL: f"wss://market-data.{env}.gravitymarkets.io/ws/full",
}.get(endpoint_type, "")
return ""
# Mapping of WS stream names to DEFAULT endpoint types
GRVT_WS_STREAMS = {
# ******* Market Data ********
"mini.s": GrvtWSEndpointType.MARKET_DATA,
"mini.d": GrvtWSEndpointType.MARKET_DATA,
"ticker.s": GrvtWSEndpointType.MARKET_DATA,
"ticker.d": GrvtWSEndpointType.MARKET_DATA,
"book.s": GrvtWSEndpointType.MARKET_DATA,
"book.d": GrvtWSEndpointType.MARKET_DATA,
"trade": GrvtWSEndpointType.MARKET_DATA,
"candle": GrvtWSEndpointType.MARKET_DATA,
# ******* Trade Data ********
"order": GrvtWSEndpointType.TRADE_DATA,
"state": GrvtWSEndpointType.TRADE_DATA,
"cancel": GrvtWSEndpointType.TRADE_DATA,
"position": GrvtWSEndpointType.TRADE_DATA,
"fill": GrvtWSEndpointType.TRADE_DATA,
"transfer": GrvtWSEndpointType.TRADE_DATA,
"deposit": GrvtWSEndpointType.TRADE_DATA,
"withdrawal": GrvtWSEndpointType.TRADE_DATA,
}
def is_trading_ws_endpoint(end_point_type: GrvtWSEndpointType) -> bool:
return end_point_type in [
GrvtWSEndpointType.TRADE_DATA,
GrvtWSEndpointType.TRADE_DATA_RPC_FULL,
]
# "wss://market-data.testnet.grvt.io/ws"
# wss://trades.testnet.grvt.io/ws
# GRVT_ENDPOINTS defines the endpoint paths grouped by endpoint type
GRVT_ENDPOINTS = {
GrvtEndpointType.EDGE: {
"GRAPHQL": "query",
"AUTH": "auth/api_key/login",
},
GrvtEndpointType.TRADE_DATA: {
"CREATE_ORDER": f"full/{END_POINT_VERSION}/create_order",
"CANCEL_ALL_ORDERS": f"full/{END_POINT_VERSION}/cancel_all_orders",
"CANCEL_ORDER": f"full/{END_POINT_VERSION}/cancel_order",
"GET_OPEN_ORDERS": f"full/{END_POINT_VERSION}/open_orders",
"GET_ACCOUNT_SUMMARY": f"full/{END_POINT_VERSION}/account_summary",
"GET_FUNDING_ACCOUNT_SUMMARY": f"full/{END_POINT_VERSION}/funding_account_summary",
"GET_AGGREGATED_ACCOUNT_SUMMARY": f"full/{END_POINT_VERSION}/aggregated_account_summary",
"GET_ACCOUNT_HISTORY": f"full/{END_POINT_VERSION}/account_history",
"GET_POSITIONS": f"full/{END_POINT_VERSION}/positions",
"GET_ORDER": f"full/{END_POINT_VERSION}/order",
"GET_ORDER_HISTORY": f"full/{END_POINT_VERSION}/order_history",
"GET_FILL_HISTORY": f"full/{END_POINT_VERSION}/fill_history",
"SET_DERISK_MM_RATIO": f"full/{END_POINT_VERSION}/set_derisk_mm_ratio",
"GET_VAULT_MANAGER_INVESTOR_HISTORY": f"full/{END_POINT_VERSION}/vault_manager_investor_history",
"GET_VAULT_REDEMPTION_QUEUE": f"full/{END_POINT_VERSION}/vault_view_redemption_queue",
},
GrvtEndpointType.MARKET_DATA: {
"GET_ALL_INSTRUMENTS": f"full/{END_POINT_VERSION}/all_instruments",
"GET_INSTRUMENTS": f"full/{END_POINT_VERSION}/instruments",
"GET_INSTRUMENT": f"full/{END_POINT_VERSION}/instrument",
"GET_TICKER": f"full/{END_POINT_VERSION}/ticker",
"GET_MINI_TICKER": f"full/{END_POINT_VERSION}/mini",
"GET_ORDER_BOOK": f"full/{END_POINT_VERSION}/book",
"GET_TRADES": f"full/{END_POINT_VERSION}/trade",
"GET_TRADE_HISTORY": f"full/{END_POINT_VERSION}/trade_history",
"GET_FUNDING": f"full/{END_POINT_VERSION}/funding",
"GET_CANDLESTICK": f"full/{END_POINT_VERSION}/kline",
},
}
def get_grvt_endpoint(environment: GrvtEnv, end_point: str) -> str:
# if end_point == "GET_ALL_INSTRUMENTS":
# return "https://market-data.testnet.grvt.io/full/v1/instruments"
endpoint_domains = get_grvt_endpoint_domains(environment.value)
for endpoints_type, endpoints in GRVT_ENDPOINTS.items():
if end_point in endpoints:
return f"{endpoint_domains[endpoints_type]}/{endpoints[end_point]}"
return ""
def get_all_grvt_endpoints(environment: GrvtEnv) -> dict[str, str]:
endpoint_domains = get_grvt_endpoint_domains(environment.value)
endpoints = {}
for endpoints_type, endpoints_map in GRVT_ENDPOINTS.items():
for endpoint, path in endpoints_map.items():
endpoints[endpoint] = f"{endpoint_domains[endpoints_type]}/{path}"
return endpoints
CHAIN_IDS = {
GrvtEnv.DEV.value: 327,
GrvtEnv.STAGING.value: 327,
GrvtEnv.TESTNET.value: 326,
GrvtEnv.PROD.value: 325,
}
########################################################
@@ -1,32 +0,0 @@
import logging
import os
import sys
from datetime import datetime
LOG_FILE = os.getenv("LOG_FILE", "FALSE").upper()
GRVT_ENV = os.getenv("GRVT_ENV")
if LOG_FILE == "TRUE":
LOG_TIMESTAMP = datetime.now().strftime("%Y_%m_%d_%H_%M_%S")
fn = sys.argv[0].split("/")[-1]
fn_base = fn.split(".")[0]
if GRVT_ENV:
filename = f"logs/{fn_base}_{GRVT_ENV}_{LOG_TIMESTAMP}.log"
else:
filename = f"logs/{fn_base}_{LOG_TIMESTAMP}.log"
os.makedirs("logs", exist_ok=True)
logging.basicConfig(
filename=filename,
level=os.getenv("LOGGING_LEVEL", "INFO"),
format="%(asctime)s.%(msecs)03d | %(levelname)s | %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
logger = logging.getLogger(__name__)
logger.info(f"Using FILE logger {LOG_FILE=}")
else:
logging.basicConfig(
level=os.getenv("LOGGING_LEVEL", "INFO"),
format="%(asctime)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger(__name__)
logger.info(f"Using CONSOLE logger {LOG_FILE=}")
@@ -1,841 +0,0 @@
# ruff: noqa: D200
# ruff: noqa: D204
# ruff: noqa: D205
# ruff: noqa: D404
# ruff: noqa: W291
# ruff: noqa: D400
# ruff: noqa: E501
import asyncio
import json
import logging
from typing import Literal
import aiohttp
from .grvt_ccxt_base import GrvtCcxtBase
# import requests
# from env import ENDPOINTS
from .grvt_ccxt_env import GrvtEnv, get_grvt_endpoint
from .grvt_ccxt_types import (
Amount,
GrvtInstrumentKind,
GrvtInvalidOrder,
GrvtOrderSide,
GrvtOrderType,
Num,
)
from .grvt_ccxt_utils import (
EnumEncoder,
GrvtOrder,
get_cookie_with_expiration,
get_cookie_with_expiration_async,
get_grvt_order,
get_order_payload,
)
class GrvtCcxtPro(GrvtCcxtBase):
"""
GrvtCcxtPro class to interact with Grvt Rest API and WebSockets in asynchronous mode.
Args:
env: GrvtCcxtPro (DEV, TESTNET, PROD)
parameters: dict with trading_account_id, private_key, api_key etc
Examples:
>>> from grvt_api_pro import GrvtCcxtPro
>>> from grvt_env import GrvtEnv
>>> grvt = GrvtCcxtPro(env=GrvtEnv.TESTNET)
>>> await grvt.fetch_markets()
"""
def __init__(
self,
env: GrvtEnv,
logger: logging.Logger | None = None,
parameters: dict = {},
order_book_ccxt_format: bool = False,
):
"""Initialize the GrvtCcxt instance."""
super().__init__(env, logger, parameters, order_book_ccxt_format)
self._clsname: str = type(self).__name__
self._session = aiohttp.ClientSession(headers={"Content-Type": "application/json"})
# Force sync call to get cookie here
self._cookie = get_cookie_with_expiration(
get_grvt_endpoint(self.env, "AUTH"), self._api_key
)
self.update_session_with_cookie()
def __del__(self):
"""Close the aiohttp session when the instance is deleted."""
self.logger.info(f"{self._clsname} __del__() called")
if self._session:
self.logger.info(f"{self._clsname} closing session")
asyncio.get_running_loop().create_task(self._session.close())
def update_session_with_cookie(self) -> None:
if self._cookie:
self._session.cookie_jar.update_cookies({"gravity": self._cookie["gravity"]})
if self._cookie["X-Grvt-Account-Id"]:
self._session.headers.update(
{"X-Grvt-Account-Id": self._cookie["X-Grvt-Account-Id"]}
)
self.logger.info(
f"update_session_with_cookie {self._cookie=} {self._session.cookie_jar=}"
f" {self._session.headers=}"
)
async def refresh_cookie(self) -> dict | None:
"""Refresh the session cookie."""
if not self.should_refresh_cookie():
return self._cookie
path: str = get_grvt_endpoint(self.env, "AUTH")
self._cookie = await get_cookie_with_expiration_async(path, self._api_key)
self._path_return_value_map[path] = self._cookie
self.update_session_with_cookie()
return self._cookie
# PRIVATE API CALLS
async def _auth_and_post(self, path: str, payload: dict) -> dict:
FN = f"{self._clsname} _auth_and_post {path=}"
MAX_LEN_TO_LOG = 1280
response: dict = {}
if not path:
self.logger.warning(f"{FN} Invalid path {path=} {payload=}")
raise GrvtInvalidOrder(f"{FN} Invalid path {path=} {payload=}")
# Always see if need to referesh cookie before sending a request
await self.refresh_cookie()
payload_json = json.dumps(payload, cls=EnumEncoder)
self.logger.info(f"{FN} {payload=}\n{payload_json=}")
return_text: str = ""
async with self._session.post(
url=path,
data=payload_json,
headers={"Content-Type": "application/json"},
timeout=5,
) as return_value:
return_text: str = ""
try:
return_text = await return_value.text()
response = await return_value.json(content_type="application/json")
except Exception as err:
self.logger.warning(
f"{FN} Unable to parse {return_value=} as "
f" json(content_type='application/json'). {err=}"
)
if not return_value.ok:
self.logger.warning(f"{FN} {payload_json=}\n{return_value=}\n{response=}")
else:
if len(return_text) > MAX_LEN_TO_LOG:
self.logger.debug(f"{FN} OK {return_value=} response={response}")
self.logger.info(f"{FN} OK {return_value=} response=**TOO LONG**")
else:
self.logger.info(f"{FN} OK {return_value=} response={response}")
self._path_return_value_map[path] = response
return response or {}
async def _create_grvt_order(self, order: GrvtOrder) -> dict:
"""
Send a GrvtOrder object to the exchange.
:param order: The GrvtOrder object.
Return: dictionary representing the order response.
"""
FN = f"{self._clsname} _create_grvt_order cloid:{order.metadata.client_order_id}"
order_payload = get_order_payload(
order,
private_key=self._private_key,
env=self.env,
instruments=self.markets,
)
path = get_grvt_endpoint(self.env, "CREATE_ORDER")
self.logger.info(f"{FN} {path=} {order_payload=}")
response: dict = await self._auth_and_post(path, payload=order_payload)
if response.get("result") is None:
self.logger.error(f"Error creating order, {response}")
return {}
self.logger.info(
f"{FN} Order created:"
f"{response.get('result', {}).get('metadata', {}).get('client_order_id')}"
)
return response.get("result", {})
def _get_order_with_validations(
self,
symbol: str,
order_type: GrvtOrderType,
side: GrvtOrderSide,
amount: Amount,
price: Num = None,
params: dict = {},
) -> GrvtOrder:
self._check_account_auth()
self._check_valid_symbol(symbol)
# Validate order fields
self._check_order_arguments(order_type, side, amount, price)
# create GrvtOrder object
order_duration_secs = params.get("order_duration_secs", 24 * 60 * 60)
return get_grvt_order(
sub_account_id=self.get_trading_account_id(),
symbol=symbol,
order_type=order_type,
side=side,
amount=amount,
limit_price=price,
order_duration_secs=order_duration_secs,
params=params,
)
async def create_order(
self,
symbol: str,
order_type: GrvtOrderType,
side: GrvtOrderSide,
amount: Amount,
price: Num = None,
params={},
) -> dict:
"""Ccxt compliant signature."""
order = self._get_order_with_validations(symbol, order_type, side, amount, price, params)
return await self._create_grvt_order(order)
async def create_limit_order(
self,
symbol: str,
side: GrvtOrderSide,
amount: Amount,
price: Num = None,
params={},
) -> dict:
return await self.create_order(symbol, "limit", side, amount, price, params)
async def cancel_all_orders(
self,
params: dict = {},
) -> bool:
"""
Ccxt compliant signature BUT lacks symbol
Cancel all orders for a sub-account.
params: dictionary with parameters. Valid keys:<br>
`kind` (str): instrument kind. Valid values: 'PERPETUAL'.<br>
`base` (str): base currency. If missing/empty then fetch
orders for all base currencies.<br>
`quote` (str): quote currency. Defaults to all.<br>
"""
self._check_account_auth()
FN = f"{self._clsname} cancel_all_orders"
payload: dict = self._get_payload_cancel_all_orders(params)
path = get_grvt_endpoint(self.env, "CANCEL_ALL_ORDERS")
response: dict = await self._auth_and_post(path, payload=payload)
cancel_ack = response.get("result", {}).get("ack")
if not cancel_ack:
self.logger.warning(f"{FN} failed to cancel orders: {response=}")
return False
self.logger.info(f"{FN} Cancelled {response=}")
return True
async def cancel_order(
self,
id: str | None = None,
symbol: str | None = None,
params: dict = {},
) -> bool:
"""
Ccxt compliant signature
Cancel specific order for the account.<br>
Private call requires authorization.<br>
See [Cancel order](https://api-docs.grvt.io/trading_api/#cancel-order)
for details.<br>.
Args:
id (str): exchange assigned order ID<br>
symbol (str): trading symbol<br>
params:
* client_order_id (str): client assigned order ID<br>
* time_to_live_ms (str): lifetime of cancel requiest in millisecs<br>
Returns:
True if cancel request was acked by exchange. False otherwise.<br>
"""
FN = f"{self._clsname} cancel_order"
self._check_account_auth()
# Prepare payload
payload: dict = {
"sub_account_id": str(self._trading_account_id),
}
if id:
payload["order_id"] = str(id)
elif "client_order_id" in params:
payload["client_order_id"] = str(params["client_order_id"])
else:
raise GrvtInvalidOrder(f"{FN} requires either order_id or client_order_id")
if "time_to_live_ms" in params:
payload["time_to_live_ms"] = str(params["time_to_live_ms"])
# Send cancel request
path = get_grvt_endpoint(self.env, "CANCEL_ORDER")
self.logger.info(
f"{FN} Send cancel {payload=} for trading_account_id={self._trading_account_id}"
)
response: dict = await self._auth_and_post(path, payload)
cancel_ack = response.get("result", {}).get("ack")
if not cancel_ack:
self.logger.warning(f"{FN} failed to cancel order: {response=}")
return False
self.logger.info(f"{FN} Cancelled {response=}")
return True
async def set_derisk_mm_ratio(self, ratio: str) -> bool:
"""
Set the Derisk to Maintenance marginb ratio for the account.
Private call requires authorization.
See [Set Derisk M M ratio](https://api-docs.grvt.io/trading_api/#set-derisk-m-m-ratio)
for details.
Args:
ratio (Amount): The new derisking market making ratio.
Returns:
True if the request was acknowledged by the exchange. False otherwise.
"""
FN = f"{self._clsname} set_derisk_mm_ratio"
self._check_account_auth()
payload: dict[str, str | dict] = self._get_set_derisk_mm_ratio_payload(str(ratio))
path = get_grvt_endpoint(self.env, "SET_DERISK_MM_RATIO")
self.logger.info(
f"{FN} Send {payload=} for trading_account_id={self.get_trading_account_id()}"
)
response: dict = await self._auth_and_post(path, payload)
self.logger.info(f"{FN} Set derisk_mm_ratio {response=}")
return True
async def fetch_open_orders(
self,
symbol: str | None = None,
since: int | None = None,
limit: int | None = None,
params: dict = {},
) -> list[dict]:
"""
Ccxt compliant signature
Fetch open orders for the account.<br>
Private call requires authorization.<br>
See [Open orders](https://api-docs.grvt.io/trading_api/#open-orders)
for details.<br>.
Args:
symbol: get orders for this symbol only.<br>
since: ccxt-compliant argument, NOT SUPPORTED.<br>
limit: ccxt-compliant argument, NOT SUPPORTED.<br>
params: dictionary with parameters. Valid keys:<br>
`kind` (str): instrument kind. Valid values are 'PERPETUAL'.<br>
`base` (str): base currency. If missing/empty then fetch orders
for all base currencies.<br>
`quote` (str): quote currency. Defaults to all.<br>
Returns:
a list of dictionaries, each dict represent an order.<br>
"""
self._check_account_auth()
# Prepare request payload
payload = self._get_payload_fetch_open_orders(symbol, params)
# Post payload and parse the response
path = get_grvt_endpoint(self.env, "GET_OPEN_ORDERS")
response: dict = await self._auth_and_post(path, payload)
open_orders: list = response.get("result", [])
if symbol:
open_orders = [
o for o in open_orders if o.get("legs") and o["legs"][0].get("instrument") == symbol
]
return open_orders
async def fetch_order(
self,
id: str | None = None,
params: dict = {},
) -> dict:
"""
Ccxt compliant signature
Private call requires authorization.<br>
See [Get Order](https://api-docs.grvt.io/trading_api/#get-order)
for details.<br>.
Get Order status by either order_id or client_order_id
Args:
id: (str) order_id to fetch.<br>
symbol: (str) NOT SUPPRTED.<br>
params: dictionary with parameters. Valid keys:<br>
`client_order_id` (int): client assigned order ID.<br>
Returns:
dict with order details or {} if order was NOT found.<br>
"""
FN = f"{self._clsname} fetch_order"
self._check_account_auth()
payload = {
"sub_account_id": str(self._trading_account_id),
}
if id:
payload["order_id"] = id
elif "client_order_id" in params:
payload["client_order_id"] = str(params["client_order_id"])
else:
raise GrvtInvalidOrder(f"{FN} requires either order_id or params['client_order_id']")
path = get_grvt_endpoint(self.env, "GET_ORDER")
response: dict = await self._auth_and_post(path, payload)
return response
async def fetch_order_history(self, params: dict = {}) -> dict:
"""
Ccxt compliant signature, HISTORICAL data.<br>
Get Order history of orders by kind/base/quote.<br>
Private call requires authorization.<br>
See [Order History](https://api-docs.grvt.io/trading_api/#order-history)
for details.<br>
Args:
params: dictionary with parameters. Valid keys:<br>
`kind`: (str) - The kind filter to apply. Defaults to all kinds.<br>
`base`: (str) - The base currency filter. Defaults to all base currencies.<br>
`quote`: (str) - The quote currency filter. Defaults to all quote currencies.<br>
`expiration`: (int) The expiration time in nanoseconds. Defaults to all.<br>
`strike_price`: (str) The strike price to apply. Defaults to all strike prices.<br>
`limit`: (int) The limit to query for. Defaults to 500; Max 1000.<br>
`cursor`: (str) The cursor to use for pagination. If nil, return the first page.<br>
Return: a dictionary with keys:
`total` : total number of account history snapshots.<br>
`next` : cursor for the next page.<br>
`result` : a list of dictionaries, each dict represent an order state.<br>.
"""
self._check_account_auth()
payload = self._get_payload_fetch_order_history(params)
path = get_grvt_endpoint(self.env, "GET_ORDER_HISTORY")
response: dict = await self._auth_and_post(path, payload)
return response
async def get_account_summary(
self, type: Literal["sub-account", "funding", "aggregated"] = "sub-account"
) -> dict:
"""
Return: The account summary.
Private call requires authorization.<br>
See [Account Summary](https://api-docs.grvt.io/trading_api/#account_summary)
for details.<br>
Returns: dictionary with account data.<br>.
"""
FN = f"{self._clsname} get_account_summary {type=}"
self._check_account_auth()
payload = {}
if type == "sub-account":
path = get_grvt_endpoint(self.env, "GET_ACCOUNT_SUMMARY")
payload = {"sub_account_id": str(self._trading_account_id)}
elif type == "funding":
path = get_grvt_endpoint(self.env, "GET_FUNDING_ACCOUNT_SUMMARY")
elif type == "aggregated":
path = get_grvt_endpoint(self.env, "GET_AGGREGATED_ACCOUNT_SUMMARY")
else:
raise GrvtInvalidOrder(f"{FN} Invalid account summary type {type}")
response: dict = await self._auth_and_post(path, payload=payload)
sub_account: dict = response.get("result", {})
if not sub_account:
self.logger.info(f"{FN} No account summary for {path=} {payload=}")
return sub_account
async def fetch_balance(
self, type: Literal["sub-account", "funding", "aggregated"] = "sub-account"
) -> dict:
"""
Ccxt compliant signature
Fetch balances for the account.<br>
Private call requires authorization.<br>
See [Account Summary](https://api-docs.grvt.io/trading_api/#sub-account_summary)
for details.<br>.
Args:
type: (str) - The type of account to fetch balances for. Defaults to 'sub-account'.
Valid values: 'sub-account', 'funding', 'aggregated'.
Returns: dictionary with ccxt-compliant balance data https://docs.ccxt.com/#/README?id=account-balance.<br>.
"""
account_summary: dict = await self.get_account_summary(type)
return self._get_balances_from_account_summary(account_summary)
async def fetch_account_history(self, params: dict = {}, limit: int = 500) -> dict:
"""
HISTORICAL data.<br>
Get account history.<br>
Private call requires authorization.<br>
See [Account History](https://api-docs.grvt.io/trading_api/#account-history)
for details.<br>.
Args:
limit: maximum number of account snapshots per page to fetch.<br>
params: dictionary with parameters. Valid keys:<br>
`start_time` (int): fetch orders since this timestamp in nanoseconds.<br>
`end_time` (int): fetch orders until this timestamp in nanoseconds.<br>
`cursor` (str): cursor for the pagination. If cursor is present then we ignore
`start_time` and `end_time`.<br>
Returns:
a dictionary with keys:
`total` : total number of account history snapshots.<br>
`next` : cursor for the next page.<br>
`result` : list of account history snapshots.<br>
"""
self._check_account_auth()
# Prepare request payload
payload = self._get_payload_fetch_account_history(limit, params)
# Post payload and parse the response
path = get_grvt_endpoint(self.env, "GET_ACCOUNT_HISTORY")
response: dict = await self._auth_and_post(path, payload=payload)
return response
async def fetch_positions(self, symbols: list[str] = [], params={}) -> list[dict]:
"""
Ccxt compliant signature
Fetch positions for the account.<br>
Private call requires authorization.<br>
See [Positions](https://api-docs.grvt.io/trading_api/#positions)
for details.<br>.
Args:
symbols: list(str) get positions for these symbols only.<br>
Returns: list of dictionaries, each dict represent a position.<br>
"""
self._check_account_auth()
# Prepare request payload
payload = self._get_payload_fetch_positions(symbols, params)
# Post payload and parse the response
path = get_grvt_endpoint(self.env, "GET_POSITIONS")
response: dict = await self._auth_and_post(path, payload)
positions: list = response.get("result", [])
if symbols:
self.logger.info(f"fetch_positions filter positions by {symbols=}")
positions = [p for p in positions if p.get("instrument") in symbols]
return positions
async def fetch_my_trades(
self,
symbol: str | None = None,
since: int | None = None,
limit: int | None = None,
params: dict = {},
) -> dict:
"""
Ccxt compliant signature, HISTORICAL data.<br>
Fetch past trades for the account.<br>
Private call requires authorization.<br>
See [Private Trade History](https://api-docs.grvt.io/trading_api/#private-trade-history)
for details.<br>.
Args:
symbol: get trades for this symbol only.<br>
since: fetch trades since this timestamp in nanoseconds.<br>
limit: maximum number of trades to fetch.<br>
params: dictionary with parameters. Valid keys:<br>
`cursor` (str): cursor for the pagination.
If cursor is present then we ignore other filters.<br>
`kind` (str): instrument kind. Valid values: 'PERPETUAL'.<br>
`base` (str): base currency. If missing/empty then fetch
orders for all base currencies.<br>
`quote` (str): quote currency. Default: 'USDT'.<br>
Returns:
a dictionary with keys:
`total` : total number of account history snapshots.<br>
`next` : cursor for the next page.<br>
`result` : a list of dictionaries, each dict represent a trade.<br>
"""
self._check_account_auth()
# Prepare request payload
payload = self._get_payload_fetch_my_trades(symbol, since, limit, params)
# Post payload and parse the response
path = get_grvt_endpoint(self.env, "GET_FILL_HISTORY")
response: dict = await self._auth_and_post(path, payload=payload)
if symbol:
# filter result by symbol
trades: list = response.get("result", [])
trades = [t for t in trades if t.get("instrument") == symbol]
response["result"] = trades
return response
# **************** PUBLIC API CALLS
async def load_markets(self) -> dict | None:
self.logger.info("load_markets START")
instruments = await self.fetch_markets(
params={
"kind": GrvtInstrumentKind.PERPETUAL,
}
)
if instruments:
self.markets = {i.get("instrument"): i for i in instruments}
self.logger.info(f"load_markets: loaded {len(self.markets)} markets.")
else:
self.logger.warning("load_markets: No markets found.")
return self.markets
async def fetch_markets(
self,
params: dict = {},
) -> list[dict]:
"""
ccxt-compliant signature
Retrieve the list of all instruments of matching kind, base and quote
supported by the exchange.
Params: dict with keys:<br>
`is_active` (bool) - defaults to True.<br>
`limit` (int) - defaiults to 20.<br>
`kind` (str): instrument kind. Valid values: 'PERPETUAL'.<br>
`base` (str): base currency. If missing/empty then fetch
orders for all base currencies.<br>
`quote` (str): quote currency. Default: 'USDT'.<br>
Returns: list of dictionaries per instrument with keys:<br>
`instrument`: symbol e.g. 'BTC_USDT_Perp'.<br>
`instrument_hash`: hashed symbol for order signing e.g. '0x030501'.<br>
`base`: base currency e.g. 'BTC'.<br>
`quote`: quote currency e.g. 'USDT'.<br>
`kind`: kind of instrument 'PERPETUAL'/'FUTURE'.<br>
'base_decimals': size multiplier for order signing.<br>
`tick_size`: price tick size.<br>
`min_size`: minimum order size.<br>
"""
# Prepare payload
payload = self._get_payload_fetch_markets(params)
# Make the POST request to get all instruments
path = get_grvt_endpoint(self.env, "GET_INSTRUMENTS")
response: dict = await self._auth_and_post(path, payload=payload)
return response.get("result", [])
async def fetch_all_markets(
self,
is_active: bool | None = True,
) -> list[dict]:
"""
Retrieve the list of all instruments supported by the exchange.<br>
Params:<br>
`is_active` (bool) - defaults to True.<br>.
Returns: list of dictionaries per instrument. See fetch_markets().<br>
"""
# Prepare payload
payload = {"is_active": is_active}
# Make the POST request to get all instruments
path = get_grvt_endpoint(self.env, "GET_ALL_INSTRUMENTS")
response: dict = await self._auth_and_post(path, payload=payload)
# Extract and return the list of instruments
return response.get("result", [])
async def fetch_market(self, symbol: str) -> dict:
"""
Retrieve the instrument object for a given symbol.
:param symbol: The symbol of the instrument.
"""
# Make the POST request to get all instruments
path = get_grvt_endpoint(self.env, "GET_INSTRUMENT")
response: dict = await self._auth_and_post(path, payload={"instrument": symbol})
return response.get("result", [])
async def fetch_ticker(self, symbol: str, params: dict = {}) -> dict:
"""
ccxt-compliant signature
Retrieve the ticker of a given symbol.
:param symbol: The instrument name.
:return: The ticker dictionary of the instrument.
"""
# {'event_time': '1724252426000000000', 'instrument': 'BTC_USDT_Perp',
# 'mark_price': '59373870996065', 'index_price': '59395287961367',
# 'last_price': '99000000000000', 'last_size': '9917000000', 'mid_price': '59569000',
# 'best_bid_price': '59866000000000', 'best_bid_size': '23705000000', 'best_ask_price':
# '59273700', 'best_ask_size': '21670', 'funding_rate_curr': 2544, 'funding_rate_avg': 0,
# 'interest_rate': 0, 'forward_price': '0', 'buy_volume_u': '401930000000',
# 'sell_volume_u': '1218289000000', 'buy_volume_q': '34637817515500',
# 'sell_volume_q': '687640900', 'high_price': '343545000', 'low_price': '100000',
# 'open_price': '32554000000000', 'open_interest': '8174350000000',
# 'long_short_ratio': 1.0948905}
path = get_grvt_endpoint(self.env, "GET_TICKER")
response: dict = await self._auth_and_post(path, payload={"instrument": symbol})
return response.get("result", {})
async def fetch_mini_ticker(self, symbol: str) -> dict:
"""
Retrieve the mini-ticker of a given symbol.
:param symbol: The instrument name.
:return: The mini-ticker dictionary of the instrument.
"""
# {'event_time': '1724252426000000000', 'instrument': 'BTC_USDT_Perp',
# 'mark_price': '59373870996065', 'index_price': '59395287961367',
# 'last_price': '99000000000000', 'last_size': '9917000000', 'mid_price': '59569000',
# 'best_bid_price': '59866000000000', 'best_bid_size': '23705000000', 'best_ask_price':
# '59273700000000', 'best_ask_size': '21678000000'}
path = get_grvt_endpoint(self.env, "GET_MINI_TICKER")
response: dict = await self._auth_and_post(path, payload={"instrument": symbol})
return response.get("result", {})
async def fetch_order_book(self, symbol: str, limit: int = 10, params={}) -> dict:
"""
ccxt-compliant signature
Retrieve the order book of a given symbol.
:param symbol: The instrument name.
:return: The order book dictionary of the instrument.
"""
# {'event_time': '0', 'instrument': 'BTC_USDT_Perp',
# 'bids': [{'price': '100000000', 'size': '86353000000', 'num_orders': 4},...]
# 'asks': [{'price': '59273700000000', 'size': '21678000000', 'num_orders': 1}, ...]
payload = {"instrument": symbol, "aggregate": 1}
if limit:
payload["depth"] = limit
path = get_grvt_endpoint(self.env, "GET_ORDER_BOOK")
response: dict = await self._auth_and_post(path, payload=payload)
if self.is_order_book_ccxt_format():
# Convert to ccxt format
return self.convert_grvt_ob_to_ccxt(response.get("result", {}))
return response.get("result", {})
async def fetch_recent_trades(
self,
symbol: str,
limit: int | None = None,
) -> list:
"""
Retrieve the recent trades a given instrument.<br>
:param instrument: The instrument name.
:return: The order book dictionary of the instrument.
"""
# List of {'event_time': '1724248876870635916', 'instrument': 'ETH_USDT_Perp',
# 'is_taker_buyer': True, 'size': '24000000000', 'price': '2600000000000',
# 'mark_price': '2591055564869', 'index_price': '2592459142472', 'interest_rate': 0,
# 'forward_price': '0', 'trade_id': '729726', 'venue': 'ORDERBOOK'}
payload: dict[str, str | int] = {"instrument": symbol}
if limit:
payload["limit"] = limit
path = get_grvt_endpoint(self.env, "GET_TRADES")
response: dict = await self._auth_and_post(path, payload=payload)
return response.get("result", [])
async def fetch_trades(
self,
symbol: str,
since: int | None = None,
limit: int = 10,
params: dict = {},
) -> dict:
"""
Ccxt-compliant signature, HISTORICAL data.<br>
Retrieve trade history of a given instrument.
:param symbol: The instrument name.
:return: dict with field 'result' containing a list of trades.
"""
# List of {'event_time': '1724248876870635916', 'instrument': 'ETH_USDT_Perp',
# 'is_taker_buyer': True, 'size': '24000000000', 'price': '2600000000000',
# 'mark_price': '2591055564869', 'index_price': '2592459142472', 'interest_rate': 0,
# 'forward_price': '0', 'trade_id': '729726', 'venue': 'ORDERBOOK'}
payload: dict = self._get_payload_fetch_trades(
symbol,
since=since,
limit=limit,
params=params,
)
path: str = get_grvt_endpoint(self.env, "GET_TRADE_HISTORY")
response: dict = await self._auth_and_post(path, payload=payload)
return response
async def fetch_funding_rate_history(
self,
symbol: str,
since: int = 0,
limit: int = 1_000,
params: dict = {},
) -> dict:
"""
ccxt-compliant signature, HISTORICAL data.<br>
Retrieve the funding rates history of a given instrument.<br>
Args:
symbol (str): The instrument name.<br>
since (int): fetch trades since this timestamp in nanoseconds.<br>
limit: int - maximum number of trades to fetch.<br>
params: dictionary with parameters. Valid keys:<br>
`cursor` (str): cursor for the pagination.
If cursor is present then we ignore other filters.<br>
`end_time` (int): end time in nanoseconds.<br>
Returns:
dict with field 'result' containing list of dictionaries repesenting funding rate
at a point in time with fields:<br>
`instrument` (str): instrument name.<br>
'funding_rate' (float): funding rate.<br>
'funding_time' (int): funding time in nanoseconds.<br>
'mark_price' (float): mark price.<br>.
"""
payload: dict[str, str | int] = {"instrument": symbol}
if params.get("cursor"):
payload["cursor"] = params["cursor"]
else:
if since:
payload["start_time"] = str(since)
if params.get("end_time"):
payload["end_time"] = str(params["end_time"])
if limit:
payload["limit"] = int(limit)
path: str = get_grvt_endpoint(self.env, "GET_FUNDING")
response: dict = await self._auth_and_post(path, payload=payload)
return response
async def fetch_ohlcv(
self,
symbol: str,
timeframe: str = "1m",
since: int = 0,
limit: int = 10,
params={},
) -> dict:
"""
ccxt-compliant signature, HISTORICAL data.<br>
Retrieve the ohlc history of a given instrument.<br>
Args:
symbol: The instrument name.<br>
timeframe: The timeframe of the ohlc.
See `ccxt_interval_to_grvt_candlestick_interval`.<br>
since: fetch ohlc since this timestamp in nanoseconds.<br>
limit: maximum number of ohlc to fetch.<br>
params: dictionary with parameters. Valid keys:<br>
`cursor` (str): cursor for the pagination.
If cursor is present then we ignore other filters.<br>
`end_time` (int): end time in nanoseconds.<br>
`candle_type` (str): candle type. Valid values: 'TRADE', 'MARK', 'INDEX'.<br>
Returns:
dict with field 'result' containing a list of dictionaries, each dict representing a candlestick with fields:<br>
`instrument` - instrument name.<br>
`open_time` - start of interval in nanoseconds.<br>
`close_time` - end of interval in nanoseconds.<br>
`open` - opening price.<br>
`close` - closing price.<br>
`high` - highest price.<br>
`low` - lowest price.<br>
`volume_u` - volume in units.<br>
`volume_q` - volume in quote(USDT).<br>
`trades` - number of trades.<br>.
"""
FN: str = f"{self._clsname} fetch_ohlcv"
payload: dict = self._get_payload_fetch_ohlcv(symbol, timeframe, since, limit, params)
self.logger.info(f"{FN} {payload=}")
path: str = get_grvt_endpoint(self.env, "GET_CANDLESTICK")
response: dict = await self._auth_and_post(path, payload=payload)
return response
# Vault Management APIs
async def fetch_vault_manager_investor_history(self, only_own_investments: bool = False) -> dict:
payload: dict = self._get_fetch_vault_manager_investor_history_payload(
vault_id=self.get_trading_account_id(),
only_own_investments=only_own_investments, # Default to False to fetch all investments
)
path: str = get_grvt_endpoint(self.env, "GET_VAULT_MANAGER_INVESTOR_HISTORY")
return await self._auth_and_post(path, payload=payload)
async def fetch_vault_redemption_queue(self):
payload: dict = self._get_fetch_vault_redemption_queue_payload(
vault_id=self.get_trading_account_id()
)
path: str = get_grvt_endpoint(self.env, "GET_VAULT_REDEMPTION_QUEUE")
return await self._auth_and_post(path, payload=payload)
@@ -1,75 +0,0 @@
import logging
from collections.abc import Callable
from .grvt_ccxt import GrvtCcxt
from .grvt_ccxt_env import get_all_grvt_endpoints
from .grvt_ccxt_pro import GrvtCcxtPro
def default_check(return_value: dict) -> str:
if not isinstance(return_value, list | dict):
return "return_value is not a list or dict"
if not return_value:
return "return_value is empty"
return "OK"
def validate_return_values(api: GrvtCcxt | GrvtCcxtPro, result_filename: str) -> None:
logging.info("validate_return_values: START")
endpoint_check_map: dict[str, Callable] = {
"GRAPHQL": default_check,
"AUTH": default_check,
"CREATE_ORDER": default_check,
"CANCEL_ALL_ORDERS": default_check,
"CANCEL_ORDER": default_check,
"GET_OPEN_ORDERS": default_check,
"GET_ACCOUNT_SUMMARY": default_check,
"GET_FUNDING_ACCOUNT_SUMMARY": default_check,
"GET_AGGREGATED_ACCOUNT_SUMMARY": default_check,
"GET_ACCOUNT_HISTORY": default_check,
"GET_POSITIONS": default_check,
"GET_ORDER": default_check,
"GET_ORDER_HISTORY": default_check,
"GET_FILL_HISTORY": default_check,
"GET_ALL_INSTRUMENTS": default_check,
"GET_INSTRUMENTS": default_check,
"GET_INSTRUMENT": default_check,
"GET_TICKER": default_check,
"GET_MINI_TICKER": default_check,
"GET_ORDER_BOOK": default_check,
"GET_TRADES": default_check,
"GET_TRADE_HISTORY": default_check,
"GET_FUNDING": default_check,
"GET_CANDLESTICK": default_check,
}
all_endpoints = get_all_grvt_endpoints(api.env)
end_point_status = {}
for short_name, endpoint in all_endpoints.items():
if not api.was_path_called(endpoint):
logging.info(f"validate_return_values: {short_name=}, {endpoint=}, not called")
end_point_status[short_name] = [endpoint, "not called"]
else:
return_value = api.get_endpoint_return_value(endpoint)
if short_name in endpoint_check_map:
check_function = endpoint_check_map.get(short_name)
if not check_function or not callable(check_function):
logging.error(
f"validate_return_values: {short_name=} "
f"not found in {endpoint_check_map.keys()=}"
)
continue
check_result = check_function(return_value)
logging.info(
f"validate_return_values: {short_name=}, {endpoint=}, {check_result=}"
)
end_point_status[short_name] = [endpoint, check_result]
else:
logging.error(
f"validate_return_values: NO {short_name=} in {endpoint_check_map.keys()=}"
)
end_point_status[short_name] = [endpoint, "no check"]
with open(result_filename, "w") as file_handle:
file_handle.write("NAME, URL, STATUS\n")
for short_name, status in end_point_status.items():
file_handle.write(f"{short_name}, {status[0]}, {status[1]}\n")
logging.info("validate_return_values: END")
@@ -1,81 +0,0 @@
# ruff: noqa: D200
# ruff: noqa: D204
# ruff: noqa: D205
# ruff: noqa: D404
# ruff: noqa: W291
# ruff: noqa: D400
# ruff: noqa: E501
from decimal import Decimal
from enum import Enum
from typing import Literal
Num = None | str | float | int | Decimal
Amount = Decimal | int | float | str
GrvtOrderSide = Literal["buy", "sell"]
GrvtOrderType = Literal["limit", "market"]
DURATION_SECOND_IN_NSEC = 1_000_000_000
PRICE_MULTIPLIER = 1_000_000_000
BTC_ETH_SIZE_MULTIPLIER = 1_000_000_000
class GrvtInvalidOrder(Exception):
pass
class CandlestickInterval(Enum):
CI_1_M = "CI_1_M"
CI_3_M = "CI_3_M"
CI_5_M = "CI_5_M"
CI_15_M = "CI_15_M"
CI_30_M = "CI_30_M"
CI_1_H = "CI_1_H"
CI_2_H = "CI_2_H"
CI_4_H = "CI_4_H"
CI_6_H = "CI_6_H"
CI_8_H = "CI_8_H"
CI_12_H = "CI_12_H"
CI_1_D = "CI_1_D"
CI_3_D = "CI_3_D"
CI_5_D = "CI_5_D"
CI_1_W = "CI_1_W"
CI_2_W = "CI_2_W"
CI_3_W = "CI_3_W"
CI_4_W = "CI_4_W"
ccxt_interval_to_grvt_candlestick_interval = {
"1m": CandlestickInterval.CI_1_M,
"3m": CandlestickInterval.CI_3_M,
"5m": CandlestickInterval.CI_5_M,
"15m": CandlestickInterval.CI_15_M,
"30m": CandlestickInterval.CI_30_M,
"1h": CandlestickInterval.CI_1_H,
"2h": CandlestickInterval.CI_2_H,
"4h": CandlestickInterval.CI_4_H,
"6h": CandlestickInterval.CI_6_H,
"8h": CandlestickInterval.CI_8_H,
"12h": CandlestickInterval.CI_12_H,
"1d": CandlestickInterval.CI_1_D,
"3d": CandlestickInterval.CI_3_D,
"5d": CandlestickInterval.CI_5_D,
"1w": CandlestickInterval.CI_1_W,
"2w": CandlestickInterval.CI_2_W,
"3w": CandlestickInterval.CI_3_W,
"4w": CandlestickInterval.CI_4_W,
}
class CandlestickType(Enum):
TRADE = "TRADE"
MARK = "MARK"
INDEX = "INDEX"
MID = "MID"
class GrvtInstrumentKind(Enum):
PERPETUAL = "PERPETUAL"
FUTURE = "FUTURE"
CALL = "CALL"
PUT = "PUT"
@@ -1,544 +0,0 @@
# ruff: noqa: D200
# ruff: noqa: D204
# ruff: noqa: D205
# ruff: noqa: D404
# ruff: noqa: W291
# ruff: noqa: D400
# ruff: noqa: E501
import json
import logging
import random
import time
from dataclasses import dataclass
from datetime import datetime
from decimal import Decimal
from enum import Enum
from http.cookies import SimpleCookie
from typing import Any
import aiohttp
import requests
from eth_account import Account
from eth_account.messages import encode_typed_data, SignableMessage
from .grvt_ccxt_env import CHAIN_IDS, GrvtEnv
from .grvt_ccxt_types import (
BTC_ETH_SIZE_MULTIPLIER,
DURATION_SECOND_IN_NSEC,
Amount,
GrvtOrderSide,
GrvtOrderType,
Num,
)
def rand_uint32():
return random.randint(0, 2**32 - 1)
class TimeInForce(Enum):
"""
| | Must Fill All | Can Fill Partial |
| - | - | - |
| Must Fill Immediately | FOK | IOC |
| Can Fill Till Time | AON | GTC |.
"""
# GTT - Remains open until it is cancelled, or expired
GOOD_TILL_TIME = "GOOD_TILL_TIME"
# AON - Either fill the whole order or none of it (Block Trades Only)
ALL_OR_NONE = "ALL_OR_NONE"
# IOC - Fill the order as much as possible, when hitting the orderbook. Then cancel it
IMMEDIATE_OR_CANCEL = "IMMEDIATE_OR_CANCEL"
# FOK - Both AoN and IoC. Either fill the full order when hitting the orderbook, or cancel it
FILL_OR_KILL = "FILL_OR_KILL"
class SignTimeInForce(Enum):
GOOD_TILL_TIME = 1
ALL_OR_NONE = 2
IMMEDIATE_OR_CANCEL = 3
FILL_OR_KILL = 4
TIME_IN_FORCE_TO_SIGN_TIME_IN_FORCE = {
TimeInForce.GOOD_TILL_TIME: SignTimeInForce.GOOD_TILL_TIME,
TimeInForce.ALL_OR_NONE: SignTimeInForce.ALL_OR_NONE,
TimeInForce.IMMEDIATE_OR_CANCEL: SignTimeInForce.IMMEDIATE_OR_CANCEL,
TimeInForce.FILL_OR_KILL: SignTimeInForce.FILL_OR_KILL,
}
def get_EIP712_domain_data(env: GrvtEnv) -> dict[str, str | int]:
# DO NOT MODIFY THESE VALUES ##############
return {
"name": "GRVT Exchange",
"version": "0",
"chainId": CHAIN_IDS[env.value],
}
def get_cookie_with_expiration(
path: str, api_key: str | None
) -> dict[str, str | float | None] | None:
"""
Authenticates and retrieves the session cookie, its expiration time and grvt-account-id token.
:return: The session cookie.
"""
FN = f"get_cookie_with_expiration {path=}"
if api_key:
data = {}
try:
data = {"api_key": api_key}
session = requests.Session()
return_value = session.post(
path,
json=data,
headers={"Content-Type": "application/json"},
timeout=5,
)
if return_value.ok:
cookie = SimpleCookie()
cookie.load(return_value.headers.get("Set-Cookie", ""))
cookie_value: str = cookie["gravity"].value
cookie_expiry: datetime = datetime.strptime(
cookie["gravity"]["expires"],
"%a, %d %b %Y %H:%M:%S %Z",
)
grvt_account_id: str = return_value.headers.get("X-Grvt-Account-Id", "")
logging.info(
f"{FN} OK response {cookie_value=} {cookie_expiry=} {grvt_account_id=}"
)
return {
"gravity": cookie_value,
"expires": cookie_expiry.timestamp(),
"X-Grvt-Account-Id": grvt_account_id,
}
logging.warning(f"{FN} Invalid return_value {data=} {path=} {return_value=}")
return None
except Exception as e:
logging.error(f"{FN} Error getting cookie: {e}")
return None
else:
return None
async def get_cookie_with_expiration_async(
path: str, api_key: str | None
) -> dict[str, str | float | None] | None:
"""
Authenticates and retrieves the session cookie, its expiration time and grvt-account-id token.
:return: The session cookie.
"""
FN = f"get_cookie_with_expiration_async {path=}"
if api_key:
data = {}
try:
data = {"api_key": api_key}
logging.info(f"{FN} ask for cookie {path=} {data=}")
async with aiohttp.ClientSession() as session:
async with session.post(url=path, json=data, timeout=5) as return_value:
logging.info(f"{FN} {return_value=}")
if return_value.ok:
cookie = SimpleCookie()
cookie.load(return_value.headers.get("Set-Cookie", ""))
cookie_value: str = cookie["gravity"].value
cookie_expiry: datetime = datetime.strptime(
cookie["gravity"]["expires"],
"%a, %d %b %Y %H:%M:%S %Z",
)
grvt_account_id: str = return_value.headers.get("X-Grvt-Account-Id", "")
logging.info(
f"{FN} OK response {cookie_value=} {cookie_expiry=} {grvt_account_id=}"
)
return {
"gravity": cookie_value,
"expires": cookie_expiry.timestamp(),
"X-Grvt-Account-Id": grvt_account_id,
}
except Exception as e:
logging.error(f"{FN} Error getting cookie: {e}")
return None
else:
return None
class GrvtKind(Enum):
PERPETUAL = 1
FUTURE = 2
CALL = 3
PUT = 4
SPOT = 5
class GrvtCurrency(Enum):
USD = 1
USDC = 2
USDT = 3
ETH = 4
BTC = 5
def hexlify(data: bytes) -> str:
"""Convert a byte array to a hex string with a 0x prefix."""
return f"0x{data.hex()}"
class EnumEncoder(json.JSONEncoder):
def default(self, o):
"""
Custom JSON encoder for Enum types.
:param obj: Object to serialize.
:return: Serialized object.
"""
if isinstance(o, Enum):
return o.value
return super().default(o)
def get_kuq_from_symbol(symbol: str) -> tuple[str, str, str]:
parts = symbol.split("_")
if len(parts) == 3:
underlying, quote, kind = parts
if kind == "Perp":
kind = "PERPETUAL"
else:
raise ValueError(f"Invalid {symbol=} {kind=}")
elif len(parts) == 4:
underlying, quote, kind, time_str = parts
if kind == "Fut":
kind = "FUTURE"
else:
raise ValueError(f"Invalid {symbol=} {kind=}")
elif len(parts) == 5:
underlying, quote, kind, time_str, strike_price = parts
if kind in {"Call", "Put"}:
kind = kind.upper()
else:
raise ValueError(f"Invalid {symbol=} {kind=}")
else:
raise ValueError(f"Invalid {symbol=}")
return kind, underlying, quote
# Custom types
EIP712_ORDER_MESSAGE_TYPE = {
"Order": [
{"name": "subAccountID", "type": "uint64"},
{"name": "isMarket", "type": "bool"},
{"name": "timeInForce", "type": "uint8"},
{"name": "postOnly", "type": "bool"},
{"name": "reduceOnly", "type": "bool"},
{"name": "legs", "type": "OrderLeg[]"},
{"name": "nonce", "type": "uint32"},
{"name": "expiration", "type": "int64"},
],
"OrderLeg": [
{"name": "assetID", "type": "uint256"},
{"name": "contractSize", "type": "uint64"},
{"name": "limitPrice", "type": "uint64"},
{"name": "isBuyingContract", "type": "bool"},
],
}
@dataclass
class GrvtSignature:
# The address (public key) of the wallet signing the payload
signer: str
r: str
s: str
v: int
# Timestamp after which this signature expires, expressed in unix nanoseconds.
# Must be capped at 30 days
expiration: str
"""
Users can randomly generate this value, used as a signature deconflicting key.
ie. You can send the same exact instruction twice with different nonces.
When the same nonce is used, the same payload will generate the same signature.
Our system will consider the payload a duplicate, and ignore it.
"""
nonce: int
@dataclass
class OrderMetadata:
"""
Metadata fields are used to support Backend only operations.
Hence, fields in here are never signed, and is never transmitted to the smart contract.
"""
"""
`client_order_id`: A unique identifier of an active order, specified by the client
This is used to identify the order in the client's system
This value must be unique for all active orders in a subaccount,
otehrwise amendment / cancellation will not work as expected
Gravity UI will generate a random clientOrderID for each order in the range [0, 2^63 - 1]
To prevent any conflicts, client machines should generate a random clientOrderID
in the range [2^63, 2^64 - 1].
When GRVT Backend receives an order with duplicate `client_order_id`, it will reject the order
with rejectReason set to duplicate `client_order_id`.
"""
client_order_id: str
# [Filled by GRVT Backend] Time at which the order was received by GRVT in unix nanoseconds
create_time: str | None = None
@dataclass
class GrvtOrderLeg:
# The instrument to trade in this leg
instrument: str
# The total number of contracts to trade in this leg, expressed in base currency units.
size: Decimal
# Specifies if the order leg is a buy or sell
is_buying_asset: bool
"""
The limit price of the order leg, expressed in `9` decimals.
This is the number of quote currency units to pay/receive for this leg.
This should be `null/0` if the order is a market order
"""
limit_price: Decimal
@dataclass
class GrvtOrder:
"""
Order is a typed payload used throughout the GRVT platform to express all orders.
GRVT orders are capable of expressing both single-legged, and multi-legged orders by default.
All fields in the Order payload (except `id`, `metadata`, and `state`) are trustlessly enforced
on our Hyperchain.
This minimizes the amount of trust users have to offer to GRVT.
"""
# The subaccount initiating the order
sub_account_id: str
# Supported time_in_force : GTT, IOC, FOK:<ul>
time_in_force: TimeInForce
legs: list[GrvtOrderLeg]
# The signature approving this order
signature: GrvtSignature
# Order Metadata, ignored by the smart contract, and unsigned by the client
metadata: OrderMetadata
# is_market: If the order is a market order
is_market: bool
post_only: bool = False
# If True, Order must reduce the position size, or be cancelled
reduce_only: bool = False
def get_signable_message(
order: GrvtOrder, env: GrvtEnv, instruments: dict[str, dict]
) -> bytes | None:
FN = f"get_signable_message {order=}"
size_multiplier = BTC_ETH_SIZE_MULTIPLIER
PRICE_MULTIPLIER = 1_000_000_000
legs = []
for leg in order.legs:
instrument = instruments.get(leg.instrument)
if not instrument or not isinstance(instrument, dict):
logging.error(f"{FN}: {leg.instrument=} not found in {instruments=}")
return None
if "base_decimals" not in instrument:
logging.error(f"{FN}: no 'base_decimals' in {instrument=}")
return None
size_multiplier = 10 ** instrument["base_decimals"]
if "instrument_hash" not in instrument:
logging.error(f"{FN}: no 'instrument_hash' in {instrument=}")
return None
legs.append(
{
"assetID": instrument["instrument_hash"],
"contractSize": int(Decimal(leg.size) * Decimal(size_multiplier)),
"limitPrice": int(Decimal(leg.limit_price) * Decimal(PRICE_MULTIPLIER)),
"isBuyingContract": leg.is_buying_asset,
}
)
message_data = {
"subAccountID": order.sub_account_id,
"isMarket": order.is_market or False,
"timeInForce": TIME_IN_FORCE_TO_SIGN_TIME_IN_FORCE[order.time_in_force].value,
"postOnly": order.post_only or False,
"reduceOnly": order.reduce_only or False,
"legs": legs,
"nonce": order.signature.nonce,
"expiration": order.signature.expiration,
}
domain_data: dict[str, str | int]= get_EIP712_domain_data(env)
logging.info(f"{FN} {domain_data=}\n{EIP712_ORDER_MESSAGE_TYPE=}\n{message_data=}")
return encode_typed_data(domain_data, EIP712_ORDER_MESSAGE_TYPE, message_data)
def get_order_payload(
order: GrvtOrder, private_key: str, env: GrvtEnv, instruments: dict[str, dict]
) -> dict:
signable_message = get_signable_message(order, env, instruments)
if signable_message is None:
raise ValueError("Failed to create signable message")
signed_message = Account.sign_message(signable_message, private_key)
order.signature.s = "0x" + signed_message.s.to_bytes(32, byteorder="big").hex()
order.signature.r = "0x" + signed_message.r.to_bytes(32, byteorder="big").hex()
order.signature.v = signed_message.v
order.signature.signer = Account.from_key(private_key).address
return {
"order": {
"sub_account_id": str(order.sub_account_id),
"is_market": order.is_market,
"time_in_force": order.time_in_force.name,
"post_only": order.post_only,
"reduce_only": order.reduce_only,
"legs": [
{
"instrument": leg.instrument,
"size": str(leg.size),
"limit_price": str(leg.limit_price),
"is_buying_asset": bool(leg.is_buying_asset),
}
for leg in order.legs
],
"signature": {
"r": order.signature.r,
"s": order.signature.s,
"v": order.signature.v,
"expiration": order.signature.expiration,
"nonce": order.signature.nonce,
"signer": order.signature.signer,
},
"metadata": {
"client_order_id": order.metadata.client_order_id,
},
}
}
def get_order_rpc_payload(
order: GrvtOrder,
private_key: str,
env: GrvtEnv,
instruments: dict[str, dict],
version: str = "v1",
) -> dict:
order_payload = get_order_payload(order, private_key, env, instruments)
return {
"jsonrpc": "2.0",
"method": f"{version}/create_order",
"params": order_payload,
}
def get_grvt_order(
sub_account_id: str,
symbol: str,
order_type: GrvtOrderType,
side: GrvtOrderSide,
amount: Amount,
limit_price: Num,
order_duration_secs: float = 5 * 60,
params: dict = {},
) -> GrvtOrder:
"""
Creates an order for a specified symbol with the given limit price and size.
Args:
symbol .
limit_price (int): The limit price for the order.
size (float): The size of the order.
is_buying_asset(bool) : Buy or Sell.
Returns:
Order: The created perpetual order.
"""
limit_price = limit_price or 0
is_buying_asset = side == "buy"
is_market = order_type == "market"
leg = GrvtOrderLeg(
instrument=symbol,
size=round(Decimal(amount), 9),
is_buying_asset=is_buying_asset,
limit_price=round(Decimal(limit_price), 9),
)
# create an expiry time
time_in_force = TimeInForce.GOOD_TILL_TIME
if "time_in_force" in params:
time_in_force = TimeInForce[params["time_in_force"]]
post_only: bool = False
if "post_only" in params:
post_only = params["post_only"]
reduce_only: bool = False
if "reduce_only" in params:
reduce_only = params["reduce_only"]
expiry_ns: int = 0
if order_duration_secs:
expiry_ns = time.time_ns() + int(order_duration_secs * DURATION_SECOND_IN_NSEC)
if "client_order_id" in params:
client_order_id = int(params["client_order_id"])
else:
client_order_id = rand_uint32()
signature = GrvtSignature(
signer="",
r="",
s="",
v=0,
expiration=str(expiry_ns),
nonce=rand_uint32(),
)
metadata = OrderMetadata(client_order_id=str(client_order_id))
return GrvtOrder(
sub_account_id=sub_account_id,
time_in_force=time_in_force,
legs=[leg],
signature=signature,
metadata=metadata,
is_market=is_market,
post_only=post_only,
reduce_only=reduce_only,
)
def sign_derisk_mm_ratio_request(
env: GrvtEnv, sub_account_id: int, ratio: str, private_key_hex: str
):
"""
Generate a signature for setting the derisk to maintenance margin ratio.
:param sub_account_id: The sub-account ID to set the ratio for.
:param ratio: The derisk to maintenance margin ratio as a string (e.g., "2.0").
:param private_key_hex: The private key in hexadecimal format.
:return: A dictionary containing the signature for the payload.
"""
derisk_ratio_int = int(Decimal(ratio) * 1_000_000)
expiration_ns = int((time.time() + 86400) * 1_000_000_000)
nonce = random.randint(1, 2**32 - 1)
domain_data = get_EIP712_domain_data(env)
types = {
"SetDeriskToMaintenanceMarginRatio": [
{"name": "subAccountID", "type": "uint64"},
{"name": "deriskToMaintenanceMarginRatio", "type": "uint32"},
{"name": "nonce", "type": "uint32"},
{"name": "expiration", "type": "int64"},
]
}
signature_payload = {
"subAccountID": sub_account_id,
"deriskToMaintenanceMarginRatio": derisk_ratio_int,
"nonce": nonce,
"expiration": expiration_ns,
}
message = encode_typed_data(domain_data, types, signature_payload)
signed = Account.sign_message(message, private_key_hex)
signer = Account.from_key(private_key_hex)
return {
"signer": signer.address.lower(),
"r": hex(signed.r),
"s": hex(signed.s),
"v": signed.v,
"expiration": str(expiration_ns),
"nonce": nonce,
}
@@ -1,785 +0,0 @@
# ruff: noqa: D200
# ruff: noqa: D204
# ruff: noqa: D205
# ruff: noqa: D404
# ruff: noqa: W291
# ruff: noqa: D400
# ruff: noqa: E501
import asyncio
import json
import logging
import traceback
from asyncio.events import AbstractEventLoop
from collections.abc import Callable
from decimal import Decimal
import websockets
# import requests
# from env import ENDPOINTS
from .grvt_ccxt_env import (
GRVT_WS_STREAMS,
GrvtEnv,
GrvtWSEndpointType,
get_grvt_ws_endpoint,
is_trading_ws_endpoint,
)
from .grvt_ccxt_pro import GrvtCcxtPro
from .grvt_ccxt_types import (
GrvtInvalidOrder,
GrvtOrderSide,
GrvtOrderType,
Num,
)
from .grvt_ccxt_utils import get_order_rpc_payload
WS_READ_TIMEOUT = 5
class GrvtCcxtWS(GrvtCcxtPro):
"""
GrvtCcxtPro class to interact with Grvt Rest API and WebSockets in asynchronous mode.
Args:
env: GrvtCcxtPro (DEV, TESTNET, PROD)
parameters: dict with trading_account_id, private_key, api_key etc
Examples:
>>> from grvt_api_pro import GrvtCcxtPro
>>> from grvt_env import GrvtEnv
>>> grvt = GrvtCcxtPro(env=GrvtEnv.TESTNET)
>>> await grvt.fetch_markets()
"""
def __init__(
self,
env: GrvtEnv,
loop: AbstractEventLoop,
logger: logging.Logger | None = None,
parameters: dict = {},
):
"""Initialize the GrvtCcxt instance."""
super().__init__(env, logger, parameters)
self._loop = loop
self._clsname: str = type(self).__name__
self.api_ws_version = parameters.get("api_ws_version", "v1")
self.force_reconnect_flag: bool = False
self.ws: dict[GrvtWSEndpointType, websockets.WebSocketClientProtocol | None] = {}
self.callbacks: dict[GrvtWSEndpointType, dict[str, dict[str, Callable]]] = {}
self.subscribed_streams: dict[GrvtWSEndpointType, dict] = {}
self.api_url: dict[GrvtWSEndpointType, str] = {}
self._last_message: dict[str, dict] = {}
self._request_id = 0
self.endpoint_types = [
GrvtWSEndpointType.MARKET_DATA,
GrvtWSEndpointType.TRADE_DATA,
GrvtWSEndpointType.MARKET_DATA_RPC_FULL,
GrvtWSEndpointType.TRADE_DATA_RPC_FULL,
]
# Initialize dictionaries for each endpoint type
for grvt_endpoint_type in self.endpoint_types:
self.api_url[grvt_endpoint_type] = get_grvt_ws_endpoint(
self.env.value, grvt_endpoint_type
)
self.callbacks[grvt_endpoint_type] = {}
self.subscribed_streams[grvt_endpoint_type] = {}
self.ws[grvt_endpoint_type] = None
self._loop.create_task(self._read_messages(grvt_endpoint_type))
self.logger.info(f"{self._clsname} initialized {self.api_url=}")
self.logger.info(f"{self._clsname} initialized {self.ws=}")
def __repr__(self) -> str:
return f"{self._clsname} {self.env=} {self.api_ws_version=}"
async def __aexit__(self):
for grvt_endpoint_type in self.endpoint_types:
await self._close_connection(grvt_endpoint_type)
def force_reconnect(self) -> None:
self.force_reconnect_flag = True
async def initialize(self):
"""
Prepares the GrvtCcxtPro instance and connects to WS server.
"""
await self.load_markets()
await self.refresh_cookie()
self._loop.create_task(self.connect_all_channels())
def is_connection_open(self, grvt_endpoint_type: GrvtWSEndpointType) -> bool:
return (
self.ws[grvt_endpoint_type] is not None and self.ws[grvt_endpoint_type].open
)
def is_endpoint_connected(self, grvt_endpoint_type: GrvtWSEndpointType) -> bool:
"""
For MARKET_DATA returns True if connection is open.
for TRADE_DATA returns True if one of the following is true:
1. No cookie - this means this is public connection and we can't connect to TRADE_DATA
2. Connection to TRADE_DATA is open
"""
if grvt_endpoint_type in [
GrvtWSEndpointType.MARKET_DATA,
GrvtWSEndpointType.MARKET_DATA_RPC_FULL,
]:
return self.is_connection_open(grvt_endpoint_type)
if grvt_endpoint_type in [
GrvtWSEndpointType.TRADE_DATA,
GrvtWSEndpointType.TRADE_DATA_RPC_FULL,
]:
return bool(not self._cookie or self.is_connection_open(grvt_endpoint_type))
raise ValueError(f"Unknown endpoint type {grvt_endpoint_type}")
def are_endpoints_connected(
self, grvt_endpoint_types: list[GrvtWSEndpointType]
) -> bool:
return all(
self.is_endpoint_connected(endpoint) for endpoint in grvt_endpoint_types
)
async def connect_all_channels(self) -> None:
"""
Connects to all channels that are possible to connect.
If cookie is NOT available, it will NOT connect to GrvtWSEndpointType.TRADE_DATA
For trading connection: run this method after cookie is available.
"""
FN = "connect_all_channels"
while True:
try:
for end_point_type in self.endpoint_types:
if (
not self.is_endpoint_connected(end_point_type)
or self.force_reconnect_flag
):
await self._reconnect(end_point_type)
all_are_connected = self.are_endpoints_connected(self.endpoint_types)
self.logger.info(
f"{FN} Connection status: {all_are_connected=} {self.force_reconnect_flag=}"
)
self.force_reconnect_flag = False
except Exception as e:
self.logger.exception(f"{FN} {e=}")
finally:
await asyncio.sleep(5)
async def connect_channel(self, grvt_endpoint_type: GrvtWSEndpointType) -> bool:
FN = f"{self._clsname} connect_channel {grvt_endpoint_type}"
try:
if self.is_endpoint_connected(grvt_endpoint_type):
self.logger.info(f"{FN} Already connected")
return True
self.subscribed_streams[grvt_endpoint_type] = {}
extra_headers = {}
if self._cookie:
extra_headers = {"Cookie": f"gravity={self._cookie['gravity']}"}
if self._cookie["X-Grvt-Account-Id"]:
extra_headers.update(
{"X-Grvt-Account-Id": self._cookie["X-Grvt-Account-Id"]}
)
if grvt_endpoint_type in [
GrvtWSEndpointType.TRADE_DATA,
GrvtWSEndpointType.TRADE_DATA_RPC_FULL,
]:
if self._cookie:
self.ws[grvt_endpoint_type] = await websockets.connect(
uri=self.api_url[grvt_endpoint_type],
extra_headers=extra_headers,
logger=self.logger,
open_timeout=5,
)
self.logger.info(
f"{FN} Connected to {self.api_url[grvt_endpoint_type]} {extra_headers=}"
)
else:
self.logger.info(f"{FN} Waiting for cookie.")
elif grvt_endpoint_type in [
GrvtWSEndpointType.MARKET_DATA,
GrvtWSEndpointType.MARKET_DATA_RPC_FULL,
]:
self.ws[grvt_endpoint_type] = await websockets.connect(
uri=self.api_url[grvt_endpoint_type],
extra_headers=extra_headers,
logger=self.logger,
open_timeout=5,
)
self.logger.info(f"{FN} Connected to {self.api_url[grvt_endpoint_type]} {extra_headers=}")
except (
websockets.exceptions.ConnectionClosedOK,
websockets.exceptions.ConnectionClosed,
) as e:
self.logger.info(f"{FN} connection already closed:{e}")
self.ws[grvt_endpoint_type] = None
except Exception as e:
self.logger.warning(f"{FN} error:{e} traceback:{traceback.format_exc()}")
self.ws[grvt_endpoint_type] = None
# return True if connection successful
return self.is_endpoint_connected(grvt_endpoint_type)
async def _close_connection(self, grvt_endpoint_type: GrvtWSEndpointType):
try:
if self.ws[grvt_endpoint_type]:
self.logger.info(f"{self._clsname} Closing connection...")
await self.ws[grvt_endpoint_type].close()
self.subscribed_streams[grvt_endpoint_type] = {}
self.logger.info(f"{self._clsname} Connection closed")
else:
self.logger.info(f"{self._clsname} No connection to close")
except Exception:
self.logger.exception(
f"{self._clsname} Error when closing connection {traceback.format_exc()}"
)
async def _reconnect(self, grvt_endpoint_type: GrvtWSEndpointType):
FN = f"{self._clsname} _reconnect {grvt_endpoint_type=}"
try:
self.logger.info(f"{FN} STARTS")
await self._close_connection(grvt_endpoint_type)
success: bool = await self.connect_channel(grvt_endpoint_type)
if success:
await self._resubscribe(grvt_endpoint_type)
except Exception:
self.logger.exception(f"{FN} failed {traceback.format_exc()}")
async def _resubscribe(self, grvt_endpoint_type: GrvtWSEndpointType):
if self.is_connection_open(grvt_endpoint_type):
for versioned_stream in self.callbacks[grvt_endpoint_type]:
for selector in self.callbacks[grvt_endpoint_type][versioned_stream]:
self.logger.info(
f"{self._clsname} _resubscribe {grvt_endpoint_type=}"
f" {versioned_stream=}/{selector=}"
)
await self._subscribe_to_stream(
grvt_endpoint_type, versioned_stream, selector
)
else:
self.logger.warning(f"{self._clsname} _resubscribe - No connection.")
# **************** PUBLIC API CALLS
def is_stream_subscribed(
self, grvt_endpoint_type: GrvtWSEndpointType, stream: str
) -> bool:
versioned_stream = self.get_versioned_stream(stream)
return self.subscribed_streams.get(grvt_endpoint_type, {}).get(
versioned_stream, False
)
def _check_susbcribed_stream(
self, grvt_endpoint_type: GrvtWSEndpointType, message: dict
) -> None:
stream_subscribed: str = ""
if "stream" in message:
stream_subscribed = message["stream"]
elif "result" in message and "stream" in message["result"]:
stream_subscribed = message.get("result", {}).get("stream", "")
if stream_subscribed:
if not self.subscribed_streams[grvt_endpoint_type].get(stream_subscribed):
self.logger.info(
f"{self._clsname} subscribed to stream:{stream_subscribed}"
)
self.subscribed_streams[grvt_endpoint_type][stream_subscribed] = True
async def _read_messages(self, grvt_endpoint_type: GrvtWSEndpointType):
FN = f"{self._clsname} _read_messages {grvt_endpoint_type.value}"
while True:
if self.is_connection_open(grvt_endpoint_type):
try:
self.logger.debug(f"{FN} waiting for message")
response = await asyncio.wait_for(
self.ws[grvt_endpoint_type].recv(), timeout=WS_READ_TIMEOUT
)
message = json.loads(response)
self.logger.debug(f"{FN} received {message=}")
self._check_susbcribed_stream(grvt_endpoint_type, message)
if "feed" in message:
stream_subscribed: str | None = message.get("stream")
selector: str = message.get("selector")
if stream_subscribed is None:
self.logger.warning(f"{FN} missing stream in {message=}")
if selector is None:
self.logger.warning(f"{FN} missing selector in {message=}")
if stream_subscribed and selector:
callback = (
self.callbacks[grvt_endpoint_type]
.get(stream_subscribed, {})
.get(selector, None)
)
if callback:
await callback(message)
stream: str = self.get_non_versioned_stream(
stream_subscribed
)
self._last_message[stream] = message
else:
self.logger.warning(
f"{FN} No callback for {stream_subscribed=}/{selector=}"
)
elif "jsonrpc" in message:
"""
{'jsonrpc': '', 'result': {'result':
{'order_id': '0x00', 'sub_account_id': '8751933338735530',
'is_market': False, 'time_in_force': 'GOOD_TILL_TIME', 'post_only': False,
'reduce_only': False, 'legs': [{'instrument': 'BTC_USDT_Perp', 'size': '0.001',
'limit_price': '50000.0', 'is_buying_asset': True}],
'signature': {'signer': '0x2989e3783e2ae05f9a1538dd411a22a4cd9554ad',
'r': '0xa566702c1e5557ab96e8d5197b6871456765a80556bba46c9d4928bd573ca66c',
's': '0x6f6e0be6dca125643fce884ca28c0ae341b201efe49e10a9626859517b4a09af',
'v': 28, 'expiration': '1729005262433997000', 'nonce': 3898454329},
'metadata': {'client_order_id': '123', 'create_time': '1728918862633971628'},
'state': {'status': 'OPEN', 'reject_reason': 'UNSPECIFIED',
'book_size': ['0.001'], 'traded_size': ['0.0'], 'update_time': '1728918862633971628'}}},
'id': 2}
"""
self.logger.debug(f"{FN} jsonrpc result:{message.get('result')}")
else:
self.logger.info(f"{FN} Non-actionable message:{message}")
except (
websockets.exceptions.ConnectionClosedError,
websockets.exceptions.ConnectionClosedOK,
):
self.logger.exception(
f"{FN} connection closed {traceback.format_exc()}"
)
await self._reconnect(grvt_endpoint_type)
except asyncio.TimeoutError: # noqa: UP041
self.logger.debug(f"{FN} Timeout {WS_READ_TIMEOUT} secs")
pass
except Exception:
self.logger.exception(
f"{FN} connection failed {traceback.format_exc()}"
)
await asyncio.sleep(1)
else:
self.logger.info(f"{FN} connection not open")
await asyncio.sleep(2)
async def _send(self, end_point_type: GrvtWSEndpointType, message: str):
try:
if self.ws[end_point_type] and self.ws[end_point_type].open:
self.logger.info(
f"{self._clsname} _send() {end_point_type=}"
f" url:{self.api_url[end_point_type]} {message=}"
)
await self.ws[end_point_type].send(message)
except websockets.exceptions.ConnectionClosedError as e:
self.logger.info(f"{self._clsname} _send() Restarted connection {e}")
await self._reconnect(end_point_type)
if self.ws[end_point_type]:
self.logger.info(
f"{self._clsname} _send() RESEND on RECONNECT {end_point_type=}"
f" url:{self.api_url[end_point_type]} {message=}"
)
await self.ws[end_point_type].send(message)
except Exception:
self.logger.exception(f"{self._clsname} send failed {traceback.format_exc()}")
await self._reconnect(end_point_type)
def _construct_selector(self, stream: str, params: dict) -> str:
feed: str = ""
# ******** Market Data ********
if stream.endswith(("mini.s", "mini.d", "ticker.s", "ticker.d")):
feed = f"{params.get('instrument', '')}@{params.get('rate', '500')}"
if stream.endswith("book.s"):
feed = (
f"{params.get('instrument', '')}@{params.get('rate', '500')}-"
f"{params.get('depth', '10')}"
)
if stream.endswith("book.d"):
feed = f"{params.get('instrument', '')}@{params.get('rate', '500')}"
if stream.endswith("trade"):
feed = f"{params.get('instrument', '')}@{params.get('limit', '50')}"
if stream.endswith("candle"):
feed = (
f"{params.get('instrument', '')}@{params.get('interval', 'CI_1_M')}-"
f"{params.get('type', 'TRADE')}"
)
# ******** Trade Data ********
if stream.endswith(("order", "state", "position", "fill")):
if not params:
feed = f"{self._trading_account_id}"
elif params.get("instrument"):
feed = f"{self._trading_account_id}-{params.get('instrument', '')}"
else:
feed = (
f"{self._trading_account_id}-{params.get('kind', '')}-"
f"{params.get('base', '')}-{params.get('quote', '')}"
)
# Deposit, Transfer, Withdrawal
if stream.endswith(("deposit", "transfer", "withdrawal")):
feed = ""
# f"{params.get('sub_account_id', '')}-{params.get('main_account_id', '')}"
return feed
async def subscribe(
self,
stream: str,
callback: Callable,
ws_end_point_type: GrvtWSEndpointType | None = None,
params: dict = {},
) -> None:
"""
Subscribe to a stream with optional parameters.
Call the callback function when a message is received.
callback function should have the following signature:
(dict) -> None.
"""
FN = f"{self._clsname} subscribe {stream=}"
if not ws_end_point_type: # use default endpoint type
ws_end_point_type = GRVT_WS_STREAMS.get(stream)
if not ws_end_point_type:
self.logger.error(f"{FN} unknown GrvtWSEndpointType for {stream=}")
return
is_trade_data = is_trading_ws_endpoint(ws_end_point_type)
if is_trade_data and not self._trading_account_id:
self.logger.error(
f"{FN} {stream=} is a trading data connection. Requires trading_account_id."
)
return
# create selector string and register callback
selector: str = self._construct_selector(stream, params)
versioned_stream: str = self.get_versioned_stream(stream)
if versioned_stream not in self.callbacks[ws_end_point_type]:
self.callbacks[ws_end_point_type][versioned_stream] = {}
self.callbacks[ws_end_point_type][versioned_stream][selector] = callback
self.logger.info(
f"{FN} {params=} {ws_end_point_type=}/{versioned_stream=}/{selector=} callback:{callback}"
)
# check if connection is open and subscribe
if self.is_connection_open(ws_end_point_type):
await self._subscribe_to_stream(ws_end_point_type, versioned_stream, selector)
else:
self.logger.info(f"{FN} Connection not open. Will subscribe on connect.")
async def re_subscribe_stream(
self,
stream: str,
callback: Callable,
ws_end_point_type: GrvtWSEndpointType | None = None,
params: dict = {},
) -> None:
""" This method should be called in a separate task -
otherwise it will block the event loop for 5 seconds.
Unsubscribe from a specific stream and subscribe again with optional parameters.
Call the callback function when a message is received.
callback function should have the following signature:
(dict) -> None.
"""
FN = f"{self._clsname} re_subscribe {stream=}"
if not ws_end_point_type: # use default endpoint type
ws_end_point_type = GRVT_WS_STREAMS.get(stream)
if not ws_end_point_type:
self.logger.error(f"{FN} unknown GrvtWSEndpointType for {stream=}")
return
if not self.is_connection_open(ws_end_point_type):
self.logger.info(f"{FN} {ws_end_point_type=} not open. Try again after connect.")
return
is_trade_data = is_trading_ws_endpoint(ws_end_point_type)
if is_trade_data and not self._trading_account_id:
self.logger.error(
f"{FN} {stream=} is a trading data connection. Requires trading_account_id."
)
return
# create selector string and register callback
selector: str = self._construct_selector(stream, params)
versioned_stream: str = self.get_versioned_stream(stream)
if versioned_stream not in self.callbacks[ws_end_point_type]:
self.callbacks[ws_end_point_type][versioned_stream] = {}
self.callbacks[ws_end_point_type][versioned_stream][selector] = callback
# self.logger.info(
# f"{FN} {params=} {ws_end_point_type=}/{versioned_stream=}/{selector=} callback:{callback}"
# )
await self._unsubscribe_to_stream(ws_end_point_type, versioned_stream, selector)
await asyncio.sleep(5) # wait for unsubscribe to complete
await self._subscribe_to_stream(ws_end_point_type, versioned_stream, selector)
def get_versioned_stream(self, stream: str) -> str:
return (
stream if self.api_ws_version == "v0" else f"{self.api_ws_version}.{stream}"
)
def get_non_versioned_stream(self, versioned_stream: str) -> str:
if self.api_ws_version == "v0":
return versioned_stream
return versioned_stream.split(".")[1]
async def _subscribe_to_stream(
self,
ws_end_point_type: GrvtWSEndpointType,
versioned_stream: str,
selector: str,
) -> None:
FN = (
f"{self._clsname} _subscribe_to_stream {ws_end_point_type=}"
f" {versioned_stream=} {selector=}"
)
self._request_id += 1
if ws_end_point_type in [
GrvtWSEndpointType.TRADE_DATA,
GrvtWSEndpointType.MARKET_DATA,
]: # Legacy subscription
subscribe_json = json.dumps(
{
"request_id": self._request_id,
"stream": versioned_stream,
"feed": [selector],
"method": "subscribe",
"is_full": True,
}
)
self.logger.info(f"{FN} {versioned_stream=} {subscribe_json=}")
else: # RPC WS format
self._request_id += 1
subscribe_json = json.dumps(
{
"jsonrpc": "2.0",
"method": "subscribe",
"params": {
"stream": versioned_stream,
"selectors": [selector],
},
"id": self._request_id,
}
)
self.logger.info(f"{FN} {versioned_stream=} {subscribe_json=}")
await self._send(ws_end_point_type, subscribe_json)
stream: str = self.get_non_versioned_stream(versioned_stream)
if stream not in self._last_message:
self._last_message[stream] = {}
async def _unsubscribe_to_stream(
self,
ws_end_point_type: GrvtWSEndpointType,
versioned_stream: str,
selector: str,
) -> None:
FN = (
f"{self._clsname} _unsubscribe_to_stream {ws_end_point_type=}"
f" {versioned_stream=} {selector=}"
)
self._request_id += 1
if ws_end_point_type in [
GrvtWSEndpointType.TRADE_DATA,
GrvtWSEndpointType.MARKET_DATA,
]: # Legacy subscription
subscribe_json = json.dumps(
{
"request_id": self._request_id,
"stream": versioned_stream,
"feed": [selector],
"method": "unsubscribe",
"is_full": True,
}
)
self.logger.info(f"{FN} {versioned_stream=} {subscribe_json=}")
else: # RPC WS format
self._request_id += 1
subscribe_json = json.dumps(
{
"jsonrpc": "2.0",
"method": "unsubscribe",
"params": {
"stream": versioned_stream,
"selectors": [selector],
},
"id": self._request_id,
}
)
self.logger.info(f"{FN} {versioned_stream=} {subscribe_json=}")
await self._send(ws_end_point_type, subscribe_json)
def jsonrpc_wrap_payload(
self, payload: dict, method: str, version: str = "v1"
) -> dict:
"""
Wrap the payload in JSON-RPC format.
"""
self._request_id += 1
return {
"jsonrpc": "2.0",
"method": f"{version}/{method}",
"params": payload,
"id": self._request_id,
}
async def send_rpc_message(
self, end_point_type: GrvtWSEndpointType, message: dict
) -> None:
"""
Send a message to the server.
"""
await self._send(end_point_type, json.dumps(message))
self.logger.info(f"{self._clsname} send_rpc_message {end_point_type=} {message=}")
async def rpc_create_order(
self,
symbol: str,
order_type: GrvtOrderType,
side: GrvtOrderSide,
amount: float | Decimal | str | int,
price: Num = None,
params={},
) -> dict:
"""
Create an order.
"""
FN = f"{self._clsname} rpc_create_order"
if not self.is_endpoint_connected(GrvtWSEndpointType.TRADE_DATA_RPC_FULL):
raise GrvtInvalidOrder("Trade data connection not available.")
order = self._get_order_with_validations(
symbol, order_type, side, amount, price, params
)
self.logger.info(f"{FN} {order=}")
payload = get_order_rpc_payload(order, self._private_key, self.env, self.markets)
self._request_id += 1
payload["id"] = self._request_id
self.logger.info(f"{FN} {payload=}")
await self.send_rpc_message(GrvtWSEndpointType.TRADE_DATA_RPC_FULL, payload)
return payload
async def rpc_create_limit_order(
self,
symbol: str,
side: GrvtOrderSide,
amount: float | Decimal | str | int,
price: Num,
params={},
) -> dict:
return await self.rpc_create_order(symbol, "limit", side, amount, price, params)
async def rpc_cancel_all_orders(
self,
params: dict = {},
) -> dict:
"""
Ccxt compliant signature BUT lacks symbol
Cancel all orders for a sub-account.
params: dictionary with parameters. Valid keys:<br>
`kind` (str): instrument kind. Valid values: 'PERPETUAL'.<br>
`base` (str): base currency. If missing/empty then fetch
orders for all base currencies.<br>
`quote` (str): quote currency. Defaults to all.<br>
"""
self._check_account_auth()
# FN = f"{self._clsname} rpc_cancel_all_orders"
payload: dict = self._get_payload_cancel_all_orders(params)
jsonrpc_payload: dict = self.jsonrpc_wrap_payload(payload, method="cancel_all_orders")
await self.send_rpc_message(
GrvtWSEndpointType.TRADE_DATA_RPC_FULL, jsonrpc_payload
)
return jsonrpc_payload
async def rpc_cancel_order(
self,
id: str | None = None,
symbol: str | None = None,
params: dict = {},
) -> dict:
"""
Ccxt compliant signature
Cancel specific order for the account by sending JsonRpc call on WebSocket.<br>
Private call requires authorization.<br>
See [Cancel order](https://api-docs.grvt.io/trading_api/#cancel-order)
for details.<br>.
Args:
id (str): exchange assigned order ID<br>
symbol (str): trading symbol<br>
params:
* client_order_id (str): client assigned order ID<br>
* time_to_live_ms (str): lifetime of cancel requiest in millisecs<br>
Returns:
payload used to cancel order.<br>
"""
FN = f"{self._clsname} rpc_cancel_order"
if not self.is_endpoint_connected(GrvtWSEndpointType.TRADE_DATA_RPC_FULL):
raise GrvtInvalidOrder("Trade data connection not available.")
self._check_account_auth()
# Prepare payload
payload: dict = {
"sub_account_id": str(self._trading_account_id),
}
if id:
payload["order_id"] = str(id)
elif "client_order_id" in params:
payload["client_order_id"] = str(params["client_order_id"])
else:
raise GrvtInvalidOrder(f"{FN} requires either order_id or client_order_id")
if "time_to_live_ms" in params:
payload["time_to_live_ms"] = str(params["time_to_live_ms"])
# Send cancel requiest
jsonrpc_payload = self.jsonrpc_wrap_payload(payload, method="cancel_order")
await self.send_rpc_message(
GrvtWSEndpointType.TRADE_DATA_RPC_FULL, jsonrpc_payload
)
return jsonrpc_payload
async def rpc_fetch_open_orders(
self,
params: dict = {},
) -> dict:
"""
Fetch open orders for the account.<br>
Private call requires authorization.<br>
See [Open orders](https://api-docs.grvt.io/trading_api/#open-orders)
for details.<br>.
Fetches open orders for the account.<br>
Sends JsonRpc call on WebSocket.<br>
Args:
params: dictionary with parameters. Valid keys:<br>
`kind` (str): instrument kind. Valid values are 'PERPETUAL'.<br>
`base` (str): base currency. If missing/empty then fetch orders
for all base currencies.<br>
`quote` (str): quote currency. Defaults to all.<br>
Returns:
payload used to fetch open orders.<br><br>
"""
self._check_account_auth()
# Prepare request payload
payload: dict = self._get_payload_fetch_open_orders(symbol=None, params=params)
jsonrpc_payload: dict = self.jsonrpc_wrap_payload(payload, method="open_orders")
await self.send_rpc_message(
GrvtWSEndpointType.TRADE_DATA_RPC_FULL, jsonrpc_payload
)
return jsonrpc_payload
async def rpc_fetch_order(
self,
id: str | None = None,
symbol: str | None = None,
params: dict = {},
) -> dict:
"""
Ccxt compliant signature.<br>
Private call requires authorization.<br>
See [Get Order](https://api-docs.grvt.io/trading_api/#get-order)
for details.<br>.
Get Order status by either order_id or client_order_id.<br>
Sends JsonRpc call on WebSocket.<br>
Args:
id: (str) order_id to fetch.<br>
symbol: (str) NOT SUPPRTED.<br>
params: dictionary with parameters. Valid keys:<br>
`client_order_id` (int): client assigned order ID.<br>
Returns:
payload used to fetch order.<br>
"""
FN = f"{self._clsname} rpc_fetch_order"
self._check_account_auth()
payload = {
"sub_account_id": str(self._trading_account_id),
}
if id:
payload["order_id"] = id
elif "client_order_id" in params:
payload["client_order_id"] = str(params["client_order_id"])
else:
raise GrvtInvalidOrder(
f"{FN} requires either order_id or params['client_order_id']"
)
jsonrpc_payload = self.jsonrpc_wrap_payload(payload, method="order")
await self.send_rpc_message(
GrvtWSEndpointType.TRADE_DATA_RPC_FULL, jsonrpc_payload
)
return jsonrpc_payload
@@ -1,25 +0,0 @@
from dataclasses import dataclass
from .grvt_raw_types import Signature, TransferType
@dataclass
class Transfer:
# The account to transfer from
from_account_id: str
# The subaccount to transfer from (0 if transferring from main account)
from_sub_account_id: str
# The account to deposit into
to_account_id: str
# The subaccount to transfer to (0 if transferring to main account)
to_sub_account_id: str
# The token currency to transfer
currency: str
# The number of tokens to transfer
num_tokens: str
# The signature of the transfer
signature: Signature
# The type of transfer
transfer_type: TransferType
# The metadata of the transfer
transfer_metadata: str
@@ -1,365 +0,0 @@
from enum import Enum
from dacite import Config, from_dict
from . import grvt_raw_types as types
from .grvt_raw_base import GrvtApiConfig, GrvtError, GrvtRawAsyncBase
# mypy: disable-error-code="no-any-return"
class GrvtRawAsync(GrvtRawAsyncBase):
def __init__(self, config: GrvtApiConfig):
super().__init__(config)
self.md_rpc = self.env.market_data.rpc_endpoint
self.td_rpc = self.env.trade_data.rpc_endpoint
async def get_instrument_v1(
self, req: types.ApiGetInstrumentRequest
) -> types.ApiGetInstrumentResponse | GrvtError:
resp = await self._post(False, self.md_rpc + "/full/v1/instrument", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiGetInstrumentResponse, resp, Config(cast=[Enum]))
async def get_all_instruments_v1(
self, req: types.ApiGetAllInstrumentsRequest
) -> types.ApiGetAllInstrumentsResponse | GrvtError:
resp = await self._post(False, self.md_rpc + "/full/v1/all_instruments", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiGetAllInstrumentsResponse, resp, Config(cast=[Enum]))
async def get_filtered_instruments_v1(
self, req: types.ApiGetFilteredInstrumentsRequest
) -> types.ApiGetFilteredInstrumentsResponse | GrvtError:
resp = await self._post(False, self.md_rpc + "/full/v1/instruments", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(
types.ApiGetFilteredInstrumentsResponse, resp, Config(cast=[Enum])
)
async def get_currency_v1(
self, req: types.ApiGetCurrencyRequest
) -> types.ApiGetCurrencyResponse | GrvtError:
resp = await self._post(False, self.md_rpc + "/full/v1/currency", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiGetCurrencyResponse, resp, Config(cast=[Enum]))
async def mini_ticker_v1(
self, req: types.ApiMiniTickerRequest
) -> types.ApiMiniTickerResponse | GrvtError:
resp = await self._post(False, self.md_rpc + "/full/v1/mini", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiMiniTickerResponse, resp, Config(cast=[Enum]))
async def ticker_v1(
self, req: types.ApiTickerRequest
) -> types.ApiTickerResponse | GrvtError:
resp = await self._post(False, self.md_rpc + "/full/v1/ticker", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiTickerResponse, resp, Config(cast=[Enum]))
async def orderbook_levels_v1(
self, req: types.ApiOrderbookLevelsRequest
) -> types.ApiOrderbookLevelsResponse | GrvtError:
resp = await self._post(False, self.md_rpc + "/full/v1/book", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiOrderbookLevelsResponse, resp, Config(cast=[Enum]))
async def trade_v1(
self, req: types.ApiTradeRequest
) -> types.ApiTradeResponse | GrvtError:
resp = await self._post(False, self.md_rpc + "/full/v1/trade", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiTradeResponse, resp, Config(cast=[Enum]))
async def trade_history_v1(
self, req: types.ApiTradeHistoryRequest
) -> types.ApiTradeHistoryResponse | GrvtError:
resp = await self._post(False, self.md_rpc + "/full/v1/trade_history", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiTradeHistoryResponse, resp, Config(cast=[Enum]))
async def candlestick_v1(
self, req: types.ApiCandlestickRequest
) -> types.ApiCandlestickResponse | GrvtError:
resp = await self._post(False, self.md_rpc + "/full/v1/kline", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiCandlestickResponse, resp, Config(cast=[Enum]))
async def funding_rate_v1(
self, req: types.ApiFundingRateRequest
) -> types.ApiFundingRateResponse | GrvtError:
resp = await self._post(False, self.md_rpc + "/full/v1/funding", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiFundingRateResponse, resp, Config(cast=[Enum]))
async def create_order_v1(
self, req: types.ApiCreateOrderRequest
) -> types.ApiCreateOrderResponse | GrvtError:
resp = await self._post(True, self.td_rpc + "/full/v1/create_order", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiCreateOrderResponse, resp, Config(cast=[Enum]))
async def cancel_order_v1(
self, req: types.ApiCancelOrderRequest
) -> types.AckResponse | GrvtError:
resp = await self._post(True, self.td_rpc + "/full/v1/cancel_order", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
async def cancel_all_orders_v1(
self, req: types.ApiCancelAllOrdersRequest
) -> types.AckResponse | GrvtError:
resp = await self._post(True, self.td_rpc + "/full/v1/cancel_all_orders", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
async def get_order_v1(
self, req: types.ApiGetOrderRequest
) -> types.ApiGetOrderResponse | GrvtError:
resp = await self._post(True, self.td_rpc + "/full/v1/order", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiGetOrderResponse, resp, Config(cast=[Enum]))
async def open_orders_v1(
self, req: types.ApiOpenOrdersRequest
) -> types.ApiOpenOrdersResponse | GrvtError:
resp = await self._post(True, self.td_rpc + "/full/v1/open_orders", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiOpenOrdersResponse, resp, Config(cast=[Enum]))
async def order_history_v1(
self, req: types.ApiOrderHistoryRequest
) -> types.ApiOrderHistoryResponse | GrvtError:
resp = await self._post(True, self.td_rpc + "/full/v1/order_history", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiOrderHistoryResponse, resp, Config(cast=[Enum]))
async def cancel_on_disconnect_v1(
self, req: types.ApiCancelOnDisconnectRequest
) -> types.AckResponse | GrvtError:
resp = await self._post(True, self.td_rpc + "/full/v1/cancel_on_disconnect", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
async def fill_history_v1(
self, req: types.ApiFillHistoryRequest
) -> types.ApiFillHistoryResponse | GrvtError:
resp = await self._post(True, self.td_rpc + "/full/v1/fill_history", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiFillHistoryResponse, resp, Config(cast=[Enum]))
async def positions_v1(
self, req: types.ApiPositionsRequest
) -> types.ApiPositionsResponse | GrvtError:
resp = await self._post(True, self.td_rpc + "/full/v1/positions", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiPositionsResponse, resp, Config(cast=[Enum]))
async def funding_payment_history_v1(
self, req: types.ApiFundingPaymentHistoryRequest
) -> types.ApiFundingPaymentHistoryResponse | GrvtError:
resp = await self._post(
True, self.td_rpc + "/full/v1/funding_payment_history", req
)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(
types.ApiFundingPaymentHistoryResponse, resp, Config(cast=[Enum])
)
async def deposit_history_v1(
self, req: types.ApiDepositHistoryRequest
) -> types.ApiDepositHistoryResponse | GrvtError:
resp = await self._post(True, self.td_rpc + "/full/v1/deposit_history", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiDepositHistoryResponse, resp, Config(cast=[Enum]))
async def transfer_v1(
self, req: types.ApiTransferRequest
) -> types.ApiTransferResponse | GrvtError:
resp = await self._post(True, self.td_rpc + "/full/v1/transfer", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiTransferResponse, resp, Config(cast=[Enum]))
async def transfer_history_v1(
self, req: types.ApiTransferHistoryRequest
) -> types.ApiTransferHistoryResponse | GrvtError:
resp = await self._post(True, self.td_rpc + "/full/v1/transfer_history", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiTransferHistoryResponse, resp, Config(cast=[Enum]))
async def withdrawal_v1(
self, req: types.ApiWithdrawalRequest
) -> types.AckResponse | GrvtError:
resp = await self._post(True, self.td_rpc + "/full/v1/withdrawal", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
async def withdrawal_history_v1(
self, req: types.ApiWithdrawalHistoryRequest
) -> types.ApiWithdrawalHistoryResponse | GrvtError:
resp = await self._post(True, self.td_rpc + "/full/v1/withdrawal_history", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiWithdrawalHistoryResponse, resp, Config(cast=[Enum]))
async def sub_account_summary_v1(
self, req: types.ApiSubAccountSummaryRequest
) -> types.ApiSubAccountSummaryResponse | GrvtError:
resp = await self._post(True, self.td_rpc + "/full/v1/account_summary", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiSubAccountSummaryResponse, resp, Config(cast=[Enum]))
async def sub_account_history_v1(
self, req: types.ApiSubAccountHistoryRequest
) -> types.ApiSubAccountHistoryResponse | GrvtError:
resp = await self._post(True, self.td_rpc + "/full/v1/account_history", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiSubAccountHistoryResponse, resp, Config(cast=[Enum]))
async def aggregated_account_summary_v1(
self, req: types.EmptyRequest
) -> types.ApiAggregatedAccountSummaryResponse | GrvtError:
resp = await self._post(
True, self.td_rpc + "/full/v1/aggregated_account_summary", req
)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(
types.ApiAggregatedAccountSummaryResponse, resp, Config(cast=[Enum])
)
async def funding_account_summary_v1(
self, req: types.EmptyRequest
) -> types.ApiFundingAccountSummaryResponse | GrvtError:
resp = await self._post(
True, self.td_rpc + "/full/v1/funding_account_summary", req
)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(
types.ApiFundingAccountSummaryResponse, resp, Config(cast=[Enum])
)
async def set_derisk_mm_ratio_v1(
self, req: types.ApiSetDeriskToMaintenanceMarginRatioRequest
) -> types.ApiSetDeriskToMaintenanceMarginRatioResponse | GrvtError:
resp = await self._post(True, self.td_rpc + "/full/v1/set_derisk_mm_ratio", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(
types.ApiSetDeriskToMaintenanceMarginRatioResponse, resp, Config(cast=[Enum])
)
async def get_all_initial_leverage_v1(
self, req: types.ApiGetAllInitialLeverageRequest
) -> types.ApiGetAllInitialLeverageResponse | GrvtError:
resp = await self._post(
True, self.td_rpc + "/full/v1/get_all_initial_leverage", req
)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(
types.ApiGetAllInitialLeverageResponse, resp, Config(cast=[Enum])
)
async def set_initial_leverage_v1(
self, req: types.ApiSetInitialLeverageRequest
) -> types.ApiSetInitialLeverageResponse | GrvtError:
resp = await self._post(True, self.td_rpc + "/full/v1/set_initial_leverage", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiSetInitialLeverageResponse, resp, Config(cast=[Enum]))
async def vault_burn_tokens_v1(
self, req: types.ApiVaultBurnTokensRequest
) -> types.AckResponse | GrvtError:
resp = await self._post(True, self.td_rpc + "/full/v1/vault_burn_tokens", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
async def vault_invest_v1(
self, req: types.ApiVaultInvestRequest
) -> types.AckResponse | GrvtError:
resp = await self._post(True, self.td_rpc + "/full/v1/vault_invest", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
async def vault_investor_summary_v1(
self, req: types.ApiVaultInvestorSummaryRequest
) -> types.ApiVaultInvestorSummaryResponse | GrvtError:
resp = await self._post(
True, self.td_rpc + "/full/v1/vault_investor_summary", req
)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiVaultInvestorSummaryResponse, resp, Config(cast=[Enum]))
async def vault_redeem_v1(
self, req: types.ApiVaultRedeemRequest
) -> types.AckResponse | GrvtError:
resp = await self._post(True, self.td_rpc + "/full/v1/vault_redeem", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
async def vault_redeem_cancel_v1(
self, req: types.ApiVaultRedeemCancelRequest
) -> types.AckResponse | GrvtError:
resp = await self._post(True, self.td_rpc + "/full/v1/vault_redeem_cancel", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
async def vault_redemption_queue_v1(
self, req: types.ApiVaultViewRedemptionQueueRequest
) -> types.ApiVaultViewRedemptionQueueResponse | GrvtError:
resp = await self._post(
True, self.td_rpc + "/full/v1/vault_view_redemption_queue", req
)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(
types.ApiVaultViewRedemptionQueueResponse, resp, Config(cast=[Enum])
)
async def query_vault_manager_investor_history_v1(
self, req: types.ApiQueryVaultManagerInvestorHistoryRequest
) -> types.ApiQueryVaultManagerInvestorHistoryResponse | GrvtError:
resp = await self._post(
True, self.td_rpc + "/full/v1/vault_manager_investor_history", req
)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(
types.ApiQueryVaultManagerInvestorHistoryResponse, resp, Config(cast=[Enum])
)
@@ -1,267 +0,0 @@
import dataclasses
import json
import logging
import time
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from http.cookies import SimpleCookie
from typing import Any
import aiohttp
import requests # type: ignore
from eth_account import Account
from .grvt_raw_env import GrvtEnv, GrvtEnvConfig, get_env_config
@dataclass
class GrvtApiConfig:
env: GrvtEnv
trading_account_id: str | None
private_key: str | None
api_key: str | None
logger: logging.Logger | None
@dataclass
class GrvtError:
code: int
message: str
status: int
@dataclass
class GrvtCookie:
gravity: str
expires: datetime
grvt_account_id: str | None = None
class GrvtRawBase:
"""
GrvtRawBase is base class for Grvt Rest API classes.
This should not be used directly, but rather through a derivative API class.
"""
def __init__(self, config: GrvtApiConfig):
self.config = config
self.env: GrvtEnvConfig = get_env_config(config.env)
self.logger: logging.Logger = config.logger or logging.getLogger(__name__)
self._cookie: GrvtCookie | None = None
if self.config.private_key is not None:
self.account: Account = Account.from_key(self.config.private_key)
"""
Cookie handling
"""
def _should_refresh_cookie(self) -> bool:
if not self.config.api_key:
raise ValueError("Attempting to use Authenticated API without API key set")
time_till_expiration = None
if self._cookie and self._cookie.expires:
time_till_expiration = self._cookie.expires.timestamp() - time.time()
is_cookie_fresh = time_till_expiration is not None and time_till_expiration > 5
if not is_cookie_fresh:
self.logger.info(
f"cookie should be refreshed now={time.time()}"
f" {time_till_expiration=} secs"
)
return not is_cookie_fresh
class GrvtRawSyncBase(GrvtRawBase):
def __init__(self, config: GrvtApiConfig):
super().__init__(config)
# Sync API session
self._session: requests.Session = requests.Session()
self._session.headers.update({"Content-Type": "application/json"})
"""
Cookie handling
"""
def _refresh_cookie(self) -> None:
if not self._should_refresh_cookie():
return None
# Get cookie
self._cookie = self._get_cookie(
self.env.edge.rpc_endpoint + "/auth/api_key/login", str(self.config.api_key)
)
self.logger.info(f"refresh_cookie cookie={self._cookie}")
# Update cookie in session
if self._cookie:
self._session.cookies.update({"gravity": self._cookie.gravity})
if self._cookie.grvt_account_id:
self._session.headers.update(
{"X-Grvt-Account-Id": self._cookie.grvt_account_id}
)
return None
def _get_cookie(self, path: str, api_key: str) -> GrvtCookie | None:
FN = f"_get_cookie {path=}"
try:
return_value = self._session.post(
path,
json={"api_key": api_key},
headers={"Content-Type": "application/json"},
timeout=5,
)
self.logger.info(f"{FN} {return_value=}")
if return_value.ok:
cookie = SimpleCookie()
cookie_header = return_value.headers.get("Set-Cookie")
grvt_cookie = return_value.cookies.get("gravity")
self.logger.info(
f"{FN} OK {return_value.headers=} \n "
f"{return_value.cookies=}\n{grvt_cookie=}\n{cookie_header=}"
)
cookie.load(cookie_header)
cookie_value = cookie["gravity"].value
cookie_expiry = datetime.strptime(
cookie["gravity"]["expires"],
"%a, %d %b %Y %H:%M:%S %Z",
)
grvt_account_id: str | None = return_value.headers.get(
"X-Grvt-Account-Id"
)
return GrvtCookie(
gravity=cookie_value,
expires=cookie_expiry,
grvt_account_id=grvt_account_id,
)
return None
except Exception as e:
self.logger.error(f"{FN} Error getting cookie: {e}")
return None
"""
Post handling
"""
def _post(self, is_auth: bool, path: str, req: Any) -> Any:
FN = f"_post {path=}"
# Always see if need to referesh cookie before sending an authenticated request
if is_auth:
self._refresh_cookie()
req_json = json.dumps(req, cls=DataclassJSONEncoder)
resp_json: Any = {}
self.logger.debug(f"{FN} {req_json=}")
resp: requests.Response = self._session.post(path, data=req_json, timeout=5)
try:
resp_json = resp.json()
if not resp.ok:
self.logger.warning(f"{FN} Error {resp_json=}")
else:
self.logger.debug(f"{FN} OK {resp_json=}")
except Exception as err:
self.logger.error(f"{FN} Unable to parse {resp.text=} as json:{err=}")
return resp_json
class GrvtRawAsyncBase(GrvtRawBase):
def __init__(self, config: GrvtApiConfig):
super().__init__(config)
# Async API session
self._session: aiohttp.ClientSession = aiohttp.ClientSession(
headers={"Content-Type": "application/json"}
)
"""
Cookie handling
"""
async def _refresh_cookie(self) -> None:
if not self._should_refresh_cookie():
return None
# Get cookie
self._cookie = await self._get_cookie(
self.env.edge.rpc_endpoint + "/auth/api_key/login", str(self.config.api_key)
)
self.logger.info(f"refresh_cookie cookie={self._cookie}")
# Update cookie in session
if self._cookie:
self._session.cookie_jar.update_cookies({"gravity": self._cookie.gravity})
if self._cookie.grvt_account_id:
self._session.headers.update(
{"X-Grvt-Account-Id": self._cookie.grvt_account_id}
)
return None
async def _get_cookie(self, path: str, api_key: str) -> GrvtCookie | None:
FN = f"_get_cookie {path=}"
try:
data = {"api_key": api_key}
self.logger.info(f"{FN} ask for cookie {path=} {data=}")
async with aiohttp.ClientSession() as session:
async with session.post(url=path, json=data, timeout=5) as return_value:
self.logger.info(f"{FN} {return_value=}")
if return_value.ok:
cookie = SimpleCookie()
cookie_header = return_value.headers.get("Set-Cookie")
grvt_cookie = return_value.cookies.get("gravity")
self.logger.info(
f"{FN} OK {return_value.headers=} \n "
f"{return_value.cookies=}\n{grvt_cookie=}\n{cookie_header=}"
)
cookie.load(cookie_header)
cookie_value = cookie["gravity"].value
cookie_expiry = datetime.strptime(
cookie["gravity"]["expires"],
"%a, %d %b %Y %H:%M:%S %Z",
)
grvt_account_id: str | None = return_value.headers.get(
"X-Grvt-Account-Id"
)
return GrvtCookie(
gravity=cookie_value,
expires=cookie_expiry,
grvt_account_id=grvt_account_id,
)
return None
except Exception as e:
self.logger.error(f"{FN} Error getting cookie: {e}")
return None
"""
Post handling
"""
async def _post(self, is_auth: bool, path: str, req: Any) -> Any:
FN = f"_post {path=}"
# Always see if need to referesh cookie before sending an authenticated request
if is_auth:
await self._refresh_cookie()
req_json = json.dumps(req, cls=DataclassJSONEncoder)
resp_json: Any = {}
self.logger.debug(f"{FN} {req_json=}")
resp: aiohttp.ClientResponse = await self._session.post(
path, data=req_json, timeout=5
)
try:
resp_text = await resp.text()
resp_json = json.loads(resp_text)
if not resp.ok:
self.logger.warning(f"{FN} Error {resp_text=}")
else:
self.logger.debug(f"{FN} OK {resp_text=}")
except Exception as err:
self.logger.error(f"{FN} Unable to parse {resp_text=} as json:{err=}")
return resp_json
class DataclassJSONEncoder(json.JSONEncoder):
def default(self, o: Any) -> Any:
if dataclasses.is_dataclass(o):
return dataclasses.asdict(o) # type: ignore
if isinstance(o, Enum):
return o.value
return super().default(o)
@@ -1,77 +0,0 @@
from dataclasses import dataclass
from enum import Enum
class GrvtEnv(Enum):
DEV = "dev"
STAGING = "staging"
TESTNET = "testnet"
PROD = "prod"
@dataclass
class GrvtEndpointConfig:
rpc_endpoint: str
ws_endpoint: str | None
@dataclass
class GrvtEnvConfig:
edge: GrvtEndpointConfig
trade_data: GrvtEndpointConfig
market_data: GrvtEndpointConfig
chain_id: int
def get_env_config(environment: GrvtEnv) -> GrvtEnvConfig:
match environment:
case GrvtEnv.PROD:
return GrvtEnvConfig(
edge=GrvtEndpointConfig(
rpc_endpoint="https://edge.grvt.io",
ws_endpoint=None,
),
trade_data=GrvtEndpointConfig(
rpc_endpoint="https://trades.grvt.io",
ws_endpoint="wss://trades.grvt.io/ws",
),
market_data=GrvtEndpointConfig(
rpc_endpoint="https://market-data.grvt.io",
ws_endpoint="wss://market-data.grvt.io/ws",
),
chain_id=325,
)
case GrvtEnv.TESTNET:
return GrvtEnvConfig(
edge=GrvtEndpointConfig(
rpc_endpoint=f"https://edge.{environment.value}.grvt.io",
ws_endpoint=None,
),
trade_data=GrvtEndpointConfig(
rpc_endpoint=f"https://trades.{environment.value}.grvt.io",
ws_endpoint=f"wss://trades.{environment.value}.grvt.io/ws",
),
market_data=GrvtEndpointConfig(
rpc_endpoint=f"https://market-data.{environment.value}.grvt.io",
ws_endpoint=f"wss://market-data.{environment.value}.grvt.io/ws",
),
chain_id=326,
)
case GrvtEnv.DEV | GrvtEnv.STAGING:
return GrvtEnvConfig(
edge=GrvtEndpointConfig(
rpc_endpoint=f"https://edge.{environment.value}.gravitymarkets.io",
ws_endpoint=None,
),
trade_data=GrvtEndpointConfig(
rpc_endpoint=f"https://trades.{environment.value}.gravitymarkets.io",
ws_endpoint=f"wss://trades.{environment.value}.gravitymarkets.io/ws",
),
market_data=GrvtEndpointConfig(
rpc_endpoint=f"https://market-data.{environment.value}.gravitymarkets.io",
ws_endpoint=f"wss://market-data.{environment.value}.gravitymarkets.io/ws",
),
chain_id=327 if environment == GrvtEnv.DEV else 328,
)
case _:
raise ValueError(f"Unknown environment={environment}")
@@ -1,248 +0,0 @@
from enum import Enum
from decimal import Decimal
from typing import Any, Optional
from eth_account import Account
from eth_account.messages import encode_typed_data
from .grvt_ccxt_utils import GrvtCurrency
from .grvt_raw_base import GrvtApiConfig, GrvtEnv
from .grvt_raw_types import Instrument, Order, Withdrawal, TimeInForce
from .grvt_fixed_types import Transfer
#########################
# INSTRUMENT CONVERSION #
#########################
PRICE_MULTIPLIER = 1_000_000_000
class SignTimeInForce(Enum):
GOOD_TILL_TIME = 1
ALL_OR_NONE = 2
IMMEDIATE_OR_CANCEL = 3
FILL_OR_KILL = 4
TIME_IN_FORCE_TO_SIGN_TIME_IN_FORCE = {
TimeInForce.GOOD_TILL_TIME: SignTimeInForce.GOOD_TILL_TIME,
TimeInForce.ALL_OR_NONE: SignTimeInForce.ALL_OR_NONE,
TimeInForce.IMMEDIATE_OR_CANCEL: SignTimeInForce.IMMEDIATE_OR_CANCEL,
TimeInForce.FILL_OR_KILL: SignTimeInForce.FILL_OR_KILL,
}
#####################
# EIP-712 chain IDs #
#####################
CHAIN_IDS = {
GrvtEnv.DEV: 327,
GrvtEnv.STAGING: 327,
GrvtEnv.TESTNET: 326,
GrvtEnv.PROD: 325,
}
def get_EIP712_domain_data(env: GrvtEnv, chainId: int | None) -> dict[str, str | int]:
return {
"name": "GRVT Exchange",
"version": "0",
"chainId": chainId or CHAIN_IDS[env],
}
#####################
# Sign Order #
#####################
EIP712_ORDER_MESSAGE_TYPE = {
"Order": [
{"name": "subAccountID", "type": "uint64"},
{"name": "isMarket", "type": "bool"},
{"name": "timeInForce", "type": "uint8"},
{"name": "postOnly", "type": "bool"},
{"name": "reduceOnly", "type": "bool"},
{"name": "legs", "type": "OrderLeg[]"},
{"name": "nonce", "type": "uint32"},
{"name": "expiration", "type": "int64"},
],
"OrderLeg": [
{"name": "assetID", "type": "uint256"},
{"name": "contractSize", "type": "uint64"},
{"name": "limitPrice", "type": "uint64"},
{"name": "isBuyingContract", "type": "bool"},
],
}
def sign_order(
order: Order,
config: GrvtApiConfig,
account: Account,
instruments: dict[str, Instrument],
) -> Order:
if config.private_key is None:
raise ValueError("Private key is not set")
message_data = build_EIP712_order_message_data(order, instruments)
domain_data = get_EIP712_domain_data(config.env, CHAIN_IDS[config.env])
signable_message = encode_typed_data(
domain_data, EIP712_ORDER_MESSAGE_TYPE, message_data
)
signed_message = account.sign_message(signable_message)
order.signature.s = "0x" + signed_message.s.to_bytes(32, byteorder="big").hex()
order.signature.r = "0x" + signed_message.r.to_bytes(32, byteorder="big").hex()
order.signature.v = signed_message.v
order.signature.signer = str(account.address)
return order
def build_EIP712_order_message_data(
order: Order, instruments: dict[str, Instrument]
) -> dict[str, Any]:
legs = []
for leg in order.legs:
instrument = instruments[leg.instrument]
size_multiplier = 10**instrument.base_decimals
# use Decimal() instead of float() to avoid precision loss
# int(float("1.013") * 1e9) = 1012999999
# int(Decimal("1.013") * Decimal(1e9) = 1013000000
size_int = int(Decimal(leg.size) * Decimal(size_multiplier))
price_int = int(Decimal(leg.limit_price) * Decimal(PRICE_MULTIPLIER))
legs.append(
{
"assetID": instrument.instrument_hash,
"contractSize": size_int,
"limitPrice": price_int,
"isBuyingContract": leg.is_buying_asset,
}
)
return {
"subAccountID": order.sub_account_id,
"isMarket": order.is_market or False,
"timeInForce": TIME_IN_FORCE_TO_SIGN_TIME_IN_FORCE[order.time_in_force].value,
"postOnly": order.post_only or False,
"reduceOnly": order.reduce_only or False,
"legs": legs,
"nonce": order.signature.nonce,
"expiration": order.signature.expiration,
}
#####################
# Sign Transfer #
#####################
EIP712_TRANSFER_MESSAGE_TYPE = {
"Transfer": [
{"name": "fromAccount", "type": "address"},
{"name": "fromSubAccount", "type": "uint64"},
{"name": "toAccount", "type": "address"},
{"name": "toSubAccount", "type": "uint64"},
{"name": "tokenCurrency", "type": "uint8"},
{"name": "numTokens", "type": "uint64"},
{"name": "nonce", "type": "uint32"},
{"name": "expiration", "type": "int64"},
],
}
def build_EIP712_transfer_message_data(transfer: Transfer, currencyId: int):
return {
"fromAccount": transfer.from_account_id,
"fromSubAccount": transfer.from_sub_account_id,
"toAccount": transfer.to_account_id,
"toSubAccount": transfer.to_sub_account_id,
"tokenCurrency": currencyId,
"numTokens": int(
Decimal(transfer.num_tokens) * Decimal(1e6)
), # USDT has 6 decimals
"nonce": transfer.signature.nonce,
"expiration": transfer.signature.expiration,
}
def sign_transfer(
transfer: Transfer,
config: GrvtApiConfig,
account: Account,
chainId: int | None = None,
currencyId: int = 3, # currencyId of USDT; refer to Get Currency API
) -> Transfer:
if config.private_key is None:
raise ValueError("Private key is not set")
domain = get_EIP712_domain_data(config.env, chainId)
message_data = build_EIP712_transfer_message_data(transfer, currencyId)
signable_message = encode_typed_data(
domain, EIP712_TRANSFER_MESSAGE_TYPE, message_data
)
signed_message = account.sign_message(signable_message)
transfer.signature.r = "0x" + signed_message.r.to_bytes(32, byteorder="big").hex()
transfer.signature.s = "0x" + signed_message.s.to_bytes(32, byteorder="big").hex()
transfer.signature.v = signed_message.v
transfer.signature.signer = str(account.address)
return transfer
#####################
# Sign Withdrawal #
#####################
EIP712_WITHDRAWAL_MESSAGE_TYPE = {
"Withdrawal": [
{"name": "fromAccount", "type": "address"},
{"name": "toEthAddress", "type": "address"},
{"name": "tokenCurrency", "type": "uint8"},
{"name": "numTokens", "type": "uint64"},
{"name": "nonce", "type": "uint32"},
{"name": "expiration", "type": "int64"},
],
}
def build_EIP712_withdrawal_message_data(withdrawal: Withdrawal, currencyId: int):
return {
"fromAccount": withdrawal.from_account_id,
"toEthAddress": withdrawal.to_eth_address,
"tokenCurrency": currencyId,
"numTokens": int(
Decimal(withdrawal.num_tokens) * Decimal(1e6)
), # USDT has 6 decimals
"nonce": withdrawal.signature.nonce,
"expiration": withdrawal.signature.expiration,
}
def sign_withdrawal(
withdrawal: Withdrawal,
config: GrvtApiConfig,
account: Account,
chainId: int | None = None,
currencyId: int = 3, # currencyId of USDT; refer to Get Currency API
) -> Withdrawal:
if config.private_key is None:
raise ValueError("Private key is not set")
domain = get_EIP712_domain_data(config.env, chainId)
message_data = build_EIP712_withdrawal_message_data(withdrawal, currencyId)
signable_message = encode_typed_data(
domain, EIP712_WITHDRAWAL_MESSAGE_TYPE, message_data
)
signed_message = account.sign_message(signable_message)
withdrawal.signature.r = "0x" + signed_message.r.to_bytes(32, byteorder="big").hex()
withdrawal.signature.s = "0x" + signed_message.s.to_bytes(32, byteorder="big").hex()
withdrawal.signature.v = signed_message.v
withdrawal.signature.signer = str(account.address)
return withdrawal
@@ -1,351 +0,0 @@
from enum import Enum
from dacite import Config, from_dict
from . import grvt_raw_types as types
from .grvt_raw_base import GrvtApiConfig, GrvtError, GrvtRawSyncBase
# mypy: disable-error-code="no-any-return"
class GrvtRawSync(GrvtRawSyncBase):
def __init__(self, config: GrvtApiConfig):
super().__init__(config)
self.md_rpc = self.env.market_data.rpc_endpoint
self.td_rpc = self.env.trade_data.rpc_endpoint
def get_instrument_v1(
self, req: types.ApiGetInstrumentRequest
) -> types.ApiGetInstrumentResponse | GrvtError:
resp = self._post(False, self.md_rpc + "/full/v1/instrument", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiGetInstrumentResponse, resp, Config(cast=[Enum]))
def get_all_instruments_v1(
self, req: types.ApiGetAllInstrumentsRequest
) -> types.ApiGetAllInstrumentsResponse | GrvtError:
resp = self._post(False, self.md_rpc + "/full/v1/all_instruments", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiGetAllInstrumentsResponse, resp, Config(cast=[Enum]))
def get_filtered_instruments_v1(
self, req: types.ApiGetFilteredInstrumentsRequest
) -> types.ApiGetFilteredInstrumentsResponse | GrvtError:
resp = self._post(False, self.md_rpc + "/full/v1/instruments", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(
types.ApiGetFilteredInstrumentsResponse, resp, Config(cast=[Enum])
)
def get_currency_v1(
self, req: types.ApiGetCurrencyRequest
) -> types.ApiGetCurrencyResponse | GrvtError:
resp = self._post(False, self.md_rpc + "/full/v1/currency", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiGetCurrencyResponse, resp, Config(cast=[Enum]))
def mini_ticker_v1(
self, req: types.ApiMiniTickerRequest
) -> types.ApiMiniTickerResponse | GrvtError:
resp = self._post(False, self.md_rpc + "/full/v1/mini", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiMiniTickerResponse, resp, Config(cast=[Enum]))
def ticker_v1(
self, req: types.ApiTickerRequest
) -> types.ApiTickerResponse | GrvtError:
resp = self._post(False, self.md_rpc + "/full/v1/ticker", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiTickerResponse, resp, Config(cast=[Enum]))
def orderbook_levels_v1(
self, req: types.ApiOrderbookLevelsRequest
) -> types.ApiOrderbookLevelsResponse | GrvtError:
resp = self._post(False, self.md_rpc + "/full/v1/book", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiOrderbookLevelsResponse, resp, Config(cast=[Enum]))
def trade_v1(self, req: types.ApiTradeRequest) -> types.ApiTradeResponse | GrvtError:
resp = self._post(False, self.md_rpc + "/full/v1/trade", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiTradeResponse, resp, Config(cast=[Enum]))
def trade_history_v1(
self, req: types.ApiTradeHistoryRequest
) -> types.ApiTradeHistoryResponse | GrvtError:
resp = self._post(False, self.md_rpc + "/full/v1/trade_history", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiTradeHistoryResponse, resp, Config(cast=[Enum]))
def candlestick_v1(
self, req: types.ApiCandlestickRequest
) -> types.ApiCandlestickResponse | GrvtError:
resp = self._post(False, self.md_rpc + "/full/v1/kline", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiCandlestickResponse, resp, Config(cast=[Enum]))
def funding_rate_v1(
self, req: types.ApiFundingRateRequest
) -> types.ApiFundingRateResponse | GrvtError:
resp = self._post(False, self.md_rpc + "/full/v1/funding", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiFundingRateResponse, resp, Config(cast=[Enum]))
def create_order_v1(
self, req: types.ApiCreateOrderRequest
) -> types.ApiCreateOrderResponse | GrvtError:
resp = self._post(True, self.td_rpc + "/full/v1/create_order", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiCreateOrderResponse, resp, Config(cast=[Enum]))
def cancel_order_v1(
self, req: types.ApiCancelOrderRequest
) -> types.AckResponse | GrvtError:
resp = self._post(True, self.td_rpc + "/full/v1/cancel_order", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
def cancel_all_orders_v1(
self, req: types.ApiCancelAllOrdersRequest
) -> types.AckResponse | GrvtError:
resp = self._post(True, self.td_rpc + "/full/v1/cancel_all_orders", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
def get_order_v1(
self, req: types.ApiGetOrderRequest
) -> types.ApiGetOrderResponse | GrvtError:
resp = self._post(True, self.td_rpc + "/full/v1/order", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiGetOrderResponse, resp, Config(cast=[Enum]))
def open_orders_v1(
self, req: types.ApiOpenOrdersRequest
) -> types.ApiOpenOrdersResponse | GrvtError:
resp = self._post(True, self.td_rpc + "/full/v1/open_orders", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiOpenOrdersResponse, resp, Config(cast=[Enum]))
def order_history_v1(
self, req: types.ApiOrderHistoryRequest
) -> types.ApiOrderHistoryResponse | GrvtError:
resp = self._post(True, self.td_rpc + "/full/v1/order_history", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiOrderHistoryResponse, resp, Config(cast=[Enum]))
def cancel_on_disconnect_v1(
self, req: types.ApiCancelOnDisconnectRequest
) -> types.AckResponse | GrvtError:
resp = self._post(True, self.td_rpc + "/full/v1/cancel_on_disconnect", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
def fill_history_v1(
self, req: types.ApiFillHistoryRequest
) -> types.ApiFillHistoryResponse | GrvtError:
resp = self._post(True, self.td_rpc + "/full/v1/fill_history", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiFillHistoryResponse, resp, Config(cast=[Enum]))
def positions_v1(
self, req: types.ApiPositionsRequest
) -> types.ApiPositionsResponse | GrvtError:
resp = self._post(True, self.td_rpc + "/full/v1/positions", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiPositionsResponse, resp, Config(cast=[Enum]))
def funding_payment_history_v1(
self, req: types.ApiFundingPaymentHistoryRequest
) -> types.ApiFundingPaymentHistoryResponse | GrvtError:
resp = self._post(True, self.td_rpc + "/full/v1/funding_payment_history", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(
types.ApiFundingPaymentHistoryResponse, resp, Config(cast=[Enum])
)
def deposit_history_v1(
self, req: types.ApiDepositHistoryRequest
) -> types.ApiDepositHistoryResponse | GrvtError:
resp = self._post(True, self.td_rpc + "/full/v1/deposit_history", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiDepositHistoryResponse, resp, Config(cast=[Enum]))
def transfer_v1(
self, req: types.ApiTransferRequest
) -> types.ApiTransferResponse | GrvtError:
resp = self._post(True, self.td_rpc + "/full/v1/transfer", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiTransferResponse, resp, Config(cast=[Enum]))
def transfer_history_v1(
self, req: types.ApiTransferHistoryRequest
) -> types.ApiTransferHistoryResponse | GrvtError:
resp = self._post(True, self.td_rpc + "/full/v1/transfer_history", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiTransferHistoryResponse, resp, Config(cast=[Enum]))
def withdrawal_v1(
self, req: types.ApiWithdrawalRequest
) -> types.AckResponse | GrvtError:
resp = self._post(True, self.td_rpc + "/full/v1/withdrawal", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
def withdrawal_history_v1(
self, req: types.ApiWithdrawalHistoryRequest
) -> types.ApiWithdrawalHistoryResponse | GrvtError:
resp = self._post(True, self.td_rpc + "/full/v1/withdrawal_history", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiWithdrawalHistoryResponse, resp, Config(cast=[Enum]))
def sub_account_summary_v1(
self, req: types.ApiSubAccountSummaryRequest
) -> types.ApiSubAccountSummaryResponse | GrvtError:
resp = self._post(True, self.td_rpc + "/full/v1/account_summary", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiSubAccountSummaryResponse, resp, Config(cast=[Enum]))
def sub_account_history_v1(
self, req: types.ApiSubAccountHistoryRequest
) -> types.ApiSubAccountHistoryResponse | GrvtError:
resp = self._post(True, self.td_rpc + "/full/v1/account_history", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiSubAccountHistoryResponse, resp, Config(cast=[Enum]))
def aggregated_account_summary_v1(
self, req: types.EmptyRequest
) -> types.ApiAggregatedAccountSummaryResponse | GrvtError:
resp = self._post(True, self.td_rpc + "/full/v1/aggregated_account_summary", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(
types.ApiAggregatedAccountSummaryResponse, resp, Config(cast=[Enum])
)
def funding_account_summary_v1(
self, req: types.EmptyRequest
) -> types.ApiFundingAccountSummaryResponse | GrvtError:
resp = self._post(True, self.td_rpc + "/full/v1/funding_account_summary", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(
types.ApiFundingAccountSummaryResponse, resp, Config(cast=[Enum])
)
def set_derisk_mm_ratio_v1(
self, req: types.ApiSetDeriskToMaintenanceMarginRatioRequest
) -> types.ApiSetDeriskToMaintenanceMarginRatioResponse | GrvtError:
resp = self._post(True, self.td_rpc + "/full/v1/set_derisk_mm_ratio", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(
types.ApiSetDeriskToMaintenanceMarginRatioResponse, resp, Config(cast=[Enum])
)
def get_all_initial_leverage_v1(
self, req: types.ApiGetAllInitialLeverageRequest
) -> types.ApiGetAllInitialLeverageResponse | GrvtError:
resp = self._post(True, self.td_rpc + "/full/v1/get_all_initial_leverage", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(
types.ApiGetAllInitialLeverageResponse, resp, Config(cast=[Enum])
)
def set_initial_leverage_v1(
self, req: types.ApiSetInitialLeverageRequest
) -> types.ApiSetInitialLeverageResponse | GrvtError:
resp = self._post(True, self.td_rpc + "/full/v1/set_initial_leverage", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiSetInitialLeverageResponse, resp, Config(cast=[Enum]))
def vault_burn_tokens_v1(
self, req: types.ApiVaultBurnTokensRequest
) -> types.AckResponse | GrvtError:
resp = self._post(True, self.td_rpc + "/full/v1/vault_burn_tokens", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
def vault_invest_v1(
self, req: types.ApiVaultInvestRequest
) -> types.AckResponse | GrvtError:
resp = self._post(True, self.td_rpc + "/full/v1/vault_invest", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
def vault_investor_summary_v1(
self, req: types.ApiVaultInvestorSummaryRequest
) -> types.ApiVaultInvestorSummaryResponse | GrvtError:
resp = self._post(True, self.td_rpc + "/full/v1/vault_investor_summary", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.ApiVaultInvestorSummaryResponse, resp, Config(cast=[Enum]))
def vault_redeem_v1(
self, req: types.ApiVaultRedeemRequest
) -> types.AckResponse | GrvtError:
resp = self._post(True, self.td_rpc + "/full/v1/vault_redeem", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
def vault_redeem_cancel_v1(
self, req: types.ApiVaultRedeemCancelRequest
) -> types.AckResponse | GrvtError:
resp = self._post(True, self.td_rpc + "/full/v1/vault_redeem_cancel", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
def vault_redemption_queue_v1(
self, req: types.ApiVaultViewRedemptionQueueRequest
) -> types.ApiVaultViewRedemptionQueueResponse | GrvtError:
resp = self._post(True, self.td_rpc + "/full/v1/vault_view_redemption_queue", req)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(
types.ApiVaultViewRedemptionQueueResponse, resp, Config(cast=[Enum])
)
def query_vault_manager_investor_history_v1(
self, req: types.ApiQueryVaultManagerInvestorHistoryRequest
) -> types.ApiQueryVaultManagerInvestorHistoryResponse | GrvtError:
resp = self._post(
True, self.td_rpc + "/full/v1/vault_manager_investor_history", req
)
if resp.get("code"):
return GrvtError(**resp)
return from_dict(
types.ApiQueryVaultManagerInvestorHistoryResponse, resp, Config(cast=[Enum])
)
File diff suppressed because it is too large Load Diff
@@ -1,316 +0,0 @@
import os
import time
import traceback
from datetime import datetime
from decimal import Decimal
from pysdk.grvt_ccxt import GrvtCcxt
from pysdk.grvt_ccxt_env import GrvtEnv
from pysdk.grvt_ccxt_logging_selector import logger
from pysdk.grvt_ccxt_test_utils import validate_return_values
from pysdk.grvt_ccxt_types import DURATION_SECOND_IN_NSEC, GrvtOrderSide
from pysdk.grvt_ccxt_utils import rand_uint32
def get_open_orders(api: GrvtCcxt) -> list[dict]:
open_orders: list[dict] = api.fetch_open_orders(
symbol="BTC_USDT_Perp",
params={"kind": "PERPETUAL"},
)
logger.info(f"open_orders: {open_orders=}")
return open_orders
def fetch_order_history(api: GrvtCcxt) -> dict:
order_history: dict = api.fetch_order_history(
params={"kind": "PERPETUAL", "limit": 3},
)
logger.info(f"order_history: {order_history=}")
return order_history
def fetch_funding_history(api: GrvtCcxt) -> dict:
start_date: datetime = datetime.strptime("2025-05-01T00:00:00Z", "%Y-%m-%dT%H:%M:%SZ")
funding_history: dict = api.fetch_funding_rate_history(
symbol="BTC_USDT_Perp",
since=int(start_date.timestamp() * DURATION_SECOND_IN_NSEC), # Convert to nanoseconds
limit=500,
)
results: list = funding_history.get("result", [])
if results:
logger.info(f"funding_history: START={results[0]}")
logger.info(f"funding_history: END={results[-1]}")
else:
logger.info(f"funding_history: No results found in {funding_history=}")
return funding_history
def cancel_orders(api: GrvtCcxt, open_orders: list) -> int:
FN = "cancel_orders"
order_count = 0
for order_dict in open_orders:
client_order_id = order_dict["metadata"].get("client_order_id")
if client_order_id:
# Cancel
logger.info(f"{FN} cancel order by id:{order_dict['order_id']}")
success = api.cancel_order(
id=order_dict["order_id"], params={"time_to_live_ms": "1000"}
)
order_count += int(success)
else:
logger.warning(f"{FN} client_order_id not found in {order_dict=}")
return order_count
def show_derisk_mm_ratios(api: GrvtCcxt, keyword: str) -> None:
"""Show the current derisking market making ratios."""
FN = "show_derisk_mm_ratios"
acc_summary = api.get_account_summary(type="sub-account")
maintenance_margin = acc_summary.get("maintenance_margin")
derisk_margin = acc_summary.get("derisk_margin")
derisk_ratio = acc_summary.get("derisk_to_maintenance_margin_ratio")
logger.info(f"{FN} {keyword} {maintenance_margin=}")
logger.info(f"{FN} {keyword} {derisk_margin=}")
logger.info(f"{FN} {keyword} {derisk_ratio=}")
logger.info(f"sub-account summary:\n{acc_summary}")
def set_derisk_mm_ratio(api: GrvtCcxt, ratio: str = "1.4") -> None:
"""Set the derisking market making ratio."""
FN = f"set_derisk_mm_ratio {ratio=}"
logger.info(f"{FN} START")
show_derisk_mm_ratios(api, "BEFORE")
api.set_derisk_mm_ratio(ratio)
show_derisk_mm_ratios(api, "AFTER")
def cancel_all_orders(api: GrvtCcxt) -> bool:
FN = "cancel_all_orders"
logger.info(f"{FN} START")
cancel_response = api.cancel_all_orders()
logger.info(f"{FN} {cancel_response=}")
return cancel_response
def print_instruments(api: GrvtCcxt):
logger.info("print_instruments: START")
if not api.markets:
return
for market in list(api.markets.values())[:3]:
logger.info(f"{market=}")
instrument = market["instrument"]
logger.info(f"fetch_market: {instrument=}, {api.fetch_market(instrument)}")
logger.info(f"fetch_mini_ticker: {instrument=}, {api.fetch_mini_ticker(instrument)}")
logger.info(f"fetch_ticker: {instrument=}, {api.fetch_ticker(instrument)}")
logger.info(f"fetch_order_book {instrument=}, {api.fetch_order_book(instrument, limit=10)}")
logger.info(
f"fetch_recent_trades {instrument=}, {api.fetch_recent_trades(instrument, limit=5)}"
)
logger.info(f"fetch_trades {instrument=}, {api.fetch_trades(instrument, limit=5)}")
logger.info(
f"fetch_funding_rate_history {instrument=}, "
f"{api.fetch_funding_rate_history(instrument, limit=5)}"
)
for type in ["TRADE", "MARK", "INDEX", "MID"]:
ohlc = api.fetch_ohlcv(
instrument, timeframe="5m", limit=5, params={"candle_type": type}
)
logger.info(f"fetch_ohlcv {type} {instrument=}, {ohlc}")
def send_order(api: GrvtCcxt, side: GrvtOrderSide, client_order_id: int) -> dict:
price = 94_000 if side == "buy" else 95_000
send_order_response: dict = api.create_order(
symbol="BTC_USDT_Perp",
order_type="limit",
side=side,
amount=0.01,
price=price,
params={"client_order_id": client_order_id},
)
logger.info(f"send order: {send_order_response=} {client_order_id=}")
return send_order_response
def send_mkt_order(
api: GrvtCcxt, symbol: str, side: GrvtOrderSide, amount: Decimal, client_order_id: int
) -> dict:
send_order_response: dict = api.create_order(
symbol=symbol,
order_type="market",
side=side,
amount=amount,
params={"client_order_id": client_order_id},
)
logger.info(f"send mkt order: {send_order_response=} {client_order_id=}")
return send_order_response
# Test scenarios, called by the __main__ test routine
def send_fetch_order(api: GrvtCcxt):
client_order_id = rand_uint32()
_ = send_order(api, side="buy", client_order_id=client_order_id)
time.sleep(0.1)
order_status = api.fetch_order(
id=None,
params={"client_order_id": client_order_id},
)
logger.info(f"result of fetch_order: {order_status=}")
def check_cancel_check_orders(api: GrvtCcxt):
logger.info("check_cancel_check_orders: START")
open_orders = get_open_orders(api)
if open_orders:
cancel_orders(api, open_orders)
get_open_orders(api)
def fetch_my_trades(api: GrvtCcxt):
logger.info("fetch_my_trades: START")
my_trades = api.fetch_my_trades(
symbol="BTC_USDT_Perp",
limit=10,
params={},
)
logger.info(f"my_trades: num trades:{len(my_trades)}")
logger.info(f"my_trades: {my_trades=}")
def cancel_send_order(api: GrvtCcxt):
FN = "cancel_send_order"
logger.info(f"{FN}: START")
client_order_id: int = rand_uint32()
logger.info(f"{FN} cancel order by {client_order_id=}")
result = api.cancel_order(
params={"client_order_id": client_order_id, "time_to_live_ms": "1000"}
)
logger.info(f"{FN} cancel_order: {result=}")
order_response = send_mkt_order(
api,
symbol="BTC_USDT_Perp",
side="sell",
amount=Decimal("0.01"),
client_order_id=client_order_id,
)
if order_response:
time.sleep(0.1)
# Get status
logger.info(f"{FN} fetch_order by {client_order_id=}")
order_status = api.fetch_order(params={"client_order_id": client_order_id})
logger.info(f"{FN} {order_status=}")
else:
logger.warning(f"{FN}: order_response is None")
def send_fetch_mkt_order(api: GrvtCcxt):
FN = "send_fetch_mkt_order"
logger.info(f"{FN}: START")
client_order_id: int = rand_uint32()
order_response = send_mkt_order(
api,
symbol="BTC_USDT_Perp",
side="sell",
amount=Decimal("0.01"),
client_order_id=client_order_id,
)
if order_response:
time.sleep(0.1)
# Get status
logger.info(f"{FN} fetch_order by {client_order_id=}")
order_status = api.fetch_order(params={"client_order_id": client_order_id})
logger.info(f"{FN} {order_status=}")
else:
logger.warning(f"{FN}: order_response is None")
def print_markets(api: GrvtCcxt):
logger.info("print_markets: START")
if api.markets:
logger.info(f"MARKETS:{len(api.markets)}")
for market in api.markets.values():
logger.info(f"MARKET:{market}")
def fetch_all_markets(api: GrvtCcxt):
logger.info("fetch_all_markets: START")
instruments = api.fetch_all_markets()
logger.info(f"fetch_all_markets: num instruments={len(instruments)}")
def print_account_summary(api: GrvtCcxt):
try:
logger.info(f"sub-account summary:\n{api.get_account_summary(type='sub-account')}")
logger.info(f"funding-account summary:\n{api.get_account_summary(type='funding')}")
logger.info(f"aggregated-account summary:\n{api.get_account_summary(type='aggregated')}")
logger.info(f"fetch_balance:\n{api.fetch_balance()}")
except Exception as e:
logger.error(f"account summary failed: {e}")
def print_account_history(api: GrvtCcxt):
try:
hist = api.fetch_account_history(params={})
logger.info(f"account history:\n{hist}")
except Exception as e:
logger.error(f"account history failed: {e}")
def print_positions(api: GrvtCcxt):
try:
logger.info(f"positions:\n{api.fetch_positions(symbols=['BTC_USDT_Perp'])}")
except Exception as e:
logger.error(f"positions failed: {e}")
def print_description(api: GrvtCcxt):
try:
logger.info(f"print_description: {api.describe()}")
except Exception as e:
logger.error(f"print_description failed: {e}")
def test_grvt_ccxt():
params = {
"api_key": os.getenv("GRVT_API_KEY"),
"trading_account_id": os.getenv("GRVT_TRADING_ACCOUNT_ID"),
"private_key": os.getenv("GRVT_PRIVATE_KEY"),
}
env = GrvtEnv(os.getenv("GRVT_ENV", "testnet"))
test_api = GrvtCcxt(env, logger, parameters=params, order_book_ccxt_format=True)
function_list = [
print_description,
# -------- MARKET related
fetch_all_markets,
print_markets,
print_instruments,
print_account_summary,
print_account_history,
# print_positions,
# -------- TRADE related
# fetch_my_trades,
fetch_order_history,
fetch_funding_history,
# # -------- order related
send_fetch_order,
fetch_my_trades,
print_positions,
check_cancel_check_orders,
cancel_send_order,
send_fetch_mkt_order,
get_open_orders,
send_fetch_order,
get_open_orders,
cancel_all_orders,
get_open_orders,
# Derisk MM ratio
set_derisk_mm_ratio,
]
for f in function_list:
try:
f(test_api)
except Exception as e:
logger.error(f"Error in {f.__name__}: {e} {traceback.format_exc()}")
validate_return_values(test_api, "test_results_sync.csv")
if __name__ == "__main__":
test_grvt_ccxt()
@@ -1,300 +0,0 @@
import asyncio
import os
import traceback
from datetime import datetime
from decimal import Decimal
from pysdk.grvt_ccxt_env import GrvtEnv
from pysdk.grvt_ccxt_logging_selector import logger
from pysdk.grvt_ccxt_pro import GrvtCcxtPro
from pysdk.grvt_ccxt_test_utils import validate_return_values
from pysdk.grvt_ccxt_types import DURATION_SECOND_IN_NSEC, GrvtOrderSide
from pysdk.grvt_ccxt_utils import rand_uint32
# Utility functions , not called directly by the __main__ test routine
async def get_open_orders(api: GrvtCcxtPro) -> list[dict]:
open_orders = await api.fetch_open_orders(
symbol="BTC_USDT_Perp",
params={"kind": "PERPETUAL"},
)
logger.info(f"open_orders: {open_orders=}")
return open_orders
async def fetch_order_history(api: GrvtCcxtPro) -> dict:
order_history: dict = await api.fetch_order_history(
params={"kind": "PERPETUAL", "limit": 3},
)
logger.info(f"order_history: {order_history=}")
return order_history
async def fetch_funding_history(api: GrvtCcxtPro) -> dict:
start_date: datetime = datetime.strptime("2025-05-01T00:00:00Z", "%Y-%m-%dT%H:%M:%SZ")
funding_history: dict = await api.fetch_funding_rate_history(
symbol="BTC_USDT_Perp",
since=int(start_date.timestamp() * DURATION_SECOND_IN_NSEC), # Convert to nanoseconds
limit=500,
)
results: list = funding_history.get("result", [])
if results:
logger.info(f"funding_history: START={results[0]}")
logger.info(f"funding_history: END={results[-1]}")
else:
logger.info(f"funding_history: No results found in {funding_history=}")
return funding_history
async def cancel_orders(api: GrvtCcxtPro, open_orders: list) -> int:
FN = "cancel_orders"
logger.info(f"{FN} START")
order_count: int = 0
for order_dict in open_orders:
client_order_id = order_dict["metadata"].get("client_order_id")
if client_order_id:
# Cancel
logger.info(f"{FN} cancel order by id:{order_dict['order_id']}")
await api.cancel_order(id=order_dict["order_id"])
order_count += 1
else:
logger.warning(f"{FN} client_order_id not found in {order_dict=}")
return order_count
async def show_derisk_mm_ratios(api: GrvtCcxtPro, keyword: str) -> None:
"""Show the current derisking market making ratios."""
FN = "show_derisk_mm_ratios"
acc_summary = await api.get_account_summary(type="sub-account")
maintenance_margin = acc_summary.get("maintenance_margin")
derisk_margin = acc_summary.get("derisk_margin")
derisk_ratio = acc_summary.get("derisk_to_maintenance_margin_ratio")
logger.info(f"{FN} {keyword} {maintenance_margin=}")
logger.info(f"{FN} {keyword} {derisk_margin=}")
logger.info(f"{FN} {keyword} {derisk_ratio=}")
logger.info(f"sub-account summary:\n{acc_summary}")
async def set_derisk_mm_ratio(api: GrvtCcxtPro, ratio: str = "1.5") -> None:
"""Set the derisking market making ratio."""
FN = f"set_derisk_mm_ratio {ratio=}"
logger.info(f"{FN} START")
await show_derisk_mm_ratios(api, "BEFORE")
await api.set_derisk_mm_ratio(ratio)
await show_derisk_mm_ratios(api, "AFTER")
async def cancel_all_orders(api: GrvtCcxtPro) -> bool:
FN = "cancel_all_orders"
logger.info(f"{FN} START")
cancel_response = await api.cancel_all_orders()
logger.info(f"{FN} {cancel_response=}")
return cancel_response
async def print_instruments(api: GrvtCcxtPro):
logger.info("print_instruments: START")
if not api.markets:
return
for market in list(api.markets.values())[:3]:
logger.info(f"{market=}")
instrument = market["instrument"]
logger.info(f"fetch_mini_ticker: {instrument=}, {await api.fetch_mini_ticker(instrument)}")
logger.info(f"fetch_ticker: {instrument=}, {await api.fetch_ticker(instrument)}")
logger.info(
f"fetch_order_book {instrument=}, {await api.fetch_order_book(instrument, limit=10)}"
)
logger.info(
f"fetch_recent_trades {instrument=}, "
f"{await api.fetch_recent_trades(instrument, limit=7)}"
)
logger.info(f"fetch_trades {instrument=}, {await api.fetch_trades(instrument, limit=5)}")
logger.info(
f"fetch_funding_rate_history {instrument=}, "
f"{await api.fetch_funding_rate_history(instrument, limit=5)}"
)
for type in ["TRADE", "MARK", "INDEX", "MID"]:
ohlc = await api.fetch_ohlcv(
instrument, timeframe="1m", limit=5, params={"candle_type": type}
)
logger.info(f"fetch_ohlcv {type} {instrument=}, {ohlc}")
async def send_order(api: GrvtCcxtPro, side: GrvtOrderSide, client_order_id: int) -> dict:
price = 64_000 if side == "buy" else 65_000
send_order_response = await api.create_order(
symbol="BTC_USDT_Perp",
order_type="limit",
side=side,
amount=0.01,
price=price,
params={"client_order_id": client_order_id},
)
logger.info(f"send order: {send_order_response=} {client_order_id=}")
return send_order_response
# Test scenarios, called by the __main__ test routine
async def send_fetch_order(api: GrvtCcxtPro):
client_order_id = rand_uint32()
_ = await send_order(api, side="buy", client_order_id=client_order_id)
order_status = await api.fetch_order(
id=None,
params={"client_order_id": client_order_id},
)
logger.info(f"result of fetch_order: {order_status=}")
async def send_mkt_order(
api: GrvtCcxtPro, symbol: str, side: GrvtOrderSide, amount: Decimal, client_order_id: int
) -> dict:
send_order_response = await api.create_order(
symbol=symbol,
order_type="market",
side=side,
amount=amount,
params={"client_order_id": client_order_id},
)
logger.info(f"send mkt order: {send_order_response=} {client_order_id=}")
return send_order_response
async def check_cancel_check_orders(api: GrvtCcxtPro):
logger.info("check_cancel_check_orders: START")
open_orders = await get_open_orders(api)
if open_orders:
await cancel_orders(api, open_orders)
await get_open_orders(api)
async def fetch_my_trades(api: GrvtCcxtPro):
logger.info("fetch_my_trades: START")
my_trades = await api.fetch_my_trades(
symbol="BTC_USDT_Perp",
limit=10,
params={},
)
logger.info(f"my_trades: num trades:{len(my_trades)}")
logger.info(f"my_trades: {my_trades=}")
async def cancel_send_order(api: GrvtCcxtPro):
FN = "cancel_send_order"
logger.info(f"{FN}: START")
client_order_id: int = rand_uint32()
logger.info(f"{FN} cancel order by {client_order_id=}")
result = await api.cancel_order(
params={"client_order_id": client_order_id, "time_to_live_ms": "1000"}
)
logger.info(f"{FN} cancel_order: {result=}")
order_response = await send_mkt_order(
api,
symbol="BTC_USDT_Perp",
side="sell",
amount=Decimal("0.01"),
client_order_id=client_order_id,
)
if order_response:
# Get status
logger.info(f"{FN} fetch_order by {client_order_id=}")
order_status = await api.fetch_order(params={"client_order_id": client_order_id})
logger.info(f"{FN} {order_status=}")
async def print_markets(api: GrvtCcxtPro):
logger.info("print_markets: START")
if api.markets:
logger.info(f"MARKETS:{len(api.markets)}")
for market in api.markets.values():
logger.info(f"MARKET:{market}")
async def fetch_all_markets(api: GrvtCcxtPro):
logger.info("fetch_all_markets: START")
instruments = await api.fetch_all_markets()
logger.info(f"fetch_all_markets: num instruments={len(instruments)}")
async def print_account_summary(api: GrvtCcxtPro):
try:
logger.info("print_account_summary: START")
logger.info(f"sub-account summary:\n{await api.get_account_summary(type='sub-account')}")
logger.info(f"funding-account summary:\n{await api.get_account_summary(type='funding')}")
logger.info(
f"aggregated-account summary:\n{await api.get_account_summary(type='aggregated')}"
)
logger.info(f"fetch_balance:\n{await api.fetch_balance()}")
except Exception as e:
logger.error(f"account summary failed: {e}")
async def print_account_history(api: GrvtCcxtPro):
try:
hist = await api.fetch_account_history(params={})
logger.info(f"account history:\n{hist}")
except Exception as e:
logger.error(f"account history failed: {e}")
async def print_positions(api: GrvtCcxtPro):
try:
logger.info(f"positions:\n{await api.fetch_positions(symbols=['BTC_USDT_Perp'])}")
except Exception as e:
logger.error(f"positions failed: {e}")
async def print_description(api: GrvtCcxtPro):
try:
logger.info(f"print_description: {api.describe()}")
except Exception as e:
logger.error(f"print_description failed: {e}")
async def grvt_ccxt_pro():
params = {
"api_key": os.getenv("GRVT_API_KEY"),
"trading_account_id": os.getenv("GRVT_TRADING_ACCOUNT_ID"),
"private_key": os.getenv("GRVT_PRIVATE_KEY"),
}
env = GrvtEnv(os.getenv("GRVT_ENV", "testnet"))
test_api = GrvtCcxtPro(env, logger, parameters=params, order_book_ccxt_format=True)
await test_api.load_markets()
await asyncio.sleep(2)
function_list = [
print_description,
# -------- MARKET related
fetch_all_markets,
print_markets,
print_instruments,
print_account_summary,
print_account_history,
print_positions,
# Order / Trade history
fetch_my_trades,
fetch_order_history,
fetch_funding_history,
# Trade related
send_fetch_order,
check_cancel_check_orders,
fetch_my_trades,
fetch_order_history,
print_positions,
cancel_send_order,
get_open_orders,
send_fetch_order,
get_open_orders,
cancel_all_orders,
get_open_orders,
set_derisk_mm_ratio,
]
for f in function_list:
try:
await f(test_api)
except Exception as e:
logger.error(f"Error in {f.__name__}: {e} {traceback.format_exc()}")
validate_return_values(test_api, "test_results.csv")
def test_grvt_ccxt_pro() -> None:
asyncio.run(grvt_ccxt_pro())
if __name__ == "__main__":
test_grvt_ccxt_pro()
@@ -1,52 +0,0 @@
import os
import traceback
from pysdk.grvt_ccxt import GrvtCcxt
from pysdk.grvt_ccxt_env import GrvtEnv
from pysdk.grvt_ccxt_logging_selector import logger
def call_vault_manager_investor_history(api: GrvtCcxt):
FN = "call_vault_manager_investor_history"
logger.info(f"{FN}: START")
try:
history = api.fetch_vault_manager_investor_history()
# Expect dict with result key and list of dicts with history items
# [{'event_time': '1752057360756255849', 'off_chain_account_id': 'ACC:2s**fW',
# 'vault_id': '2002239639', 'type': 'VAULT_REDEEM', 'price': '0.998912', 'size': '1900.0',
# 'realized_pnl': '-0.077452', 'performance_fee': '0.0'}, ...]
logger.info(f"{FN}: {history=}")
except Exception as e:
logger.error(f"{FN} failed: {e}")
def call_vault_redemption_queue(api: GrvtCcxt):
FN = "call_vault_redemption_queue"
logger.info(f"{FN}: START")
try:
redemption_queue = api.fetch_vault_redemption_queue()
logger.info(f"{FN}: {redemption_queue=}")
except Exception as e:
logger.error(f"{FN} failed: {e}")
def test_grvt_ccxt_vault():
params = {
"api_key": os.getenv("GRVT_API_KEY"),
"trading_account_id": os.getenv("GRVT_TRADING_ACCOUNT_ID"),
"private_key": os.getenv("GRVT_PRIVATE_KEY"),
}
env = GrvtEnv(os.getenv("GRVT_ENV", "testnet"))
test_api = GrvtCcxt(env, logger, parameters=params, order_book_ccxt_format=True)
function_list = [
call_vault_manager_investor_history,
call_vault_redemption_queue,
]
for f in function_list:
try:
f(test_api)
except Exception as e:
logger.error(f"Error in {f.__name__}: {e} {traceback.format_exc()}")
if __name__ == "__main__":
test_grvt_ccxt_vault()
@@ -1,60 +0,0 @@
import asyncio
import os
import traceback
from pysdk.grvt_ccxt_env import GrvtEnv
from pysdk.grvt_ccxt_logging_selector import logger
from pysdk.grvt_ccxt_pro import GrvtCcxtPro
async def call_vault_manager_investor_history(api: GrvtCcxtPro):
FN = "call_vault_manager_investor_history"
logger.info(f"{FN}: START")
try:
history = await api.fetch_vault_manager_investor_history()
# Expect dict with result key and list of dicts with history items
# [{'event_time': '1752057360756255849', 'off_chain_account_id': 'ACC:2s**fW',
# 'vault_id': '2002239639', 'type': 'VAULT_REDEEM', 'price': '0.998912', 'size': '1900.0',
# 'realized_pnl': '-0.077452', 'performance_fee': '0.0'}, ...]
logger.info(f"{FN}: {history=}")
except Exception as e:
logger.error(f"{FN} failed: {e}")
async def call_vault_redemption_queue(api: GrvtCcxtPro):
FN = "call_vault_redemption_queue"
logger.info(f"{FN}: START")
try:
redemption_queue = await api.fetch_vault_redemption_queue()
logger.info(f"{FN}: {redemption_queue=}")
except Exception as e:
logger.error(f"{FN} failed: {e}")
async def grvt_ccxt_vault_pro():
params = {
"api_key": os.getenv("GRVT_API_KEY"),
"trading_account_id": os.getenv("GRVT_TRADING_ACCOUNT_ID"),
"private_key": os.getenv("GRVT_PRIVATE_KEY"),
}
env = GrvtEnv(os.getenv("GRVT_ENV", "testnet"))
test_api = GrvtCcxtPro(env, logger, parameters=params, order_book_ccxt_format=True)
await test_api.load_markets()
await asyncio.sleep(2)
function_list = [
call_vault_manager_investor_history,
call_vault_redemption_queue,
]
for f in function_list:
try:
await f(test_api)
except Exception as e:
logger.error(f"Error in {f.__name__}: {e} {traceback.format_exc()}")
def test_grvt_ccxt_vault_pro() -> None:
asyncio.run(grvt_ccxt_vault_pro())
if __name__ == "__main__":
test_grvt_ccxt_vault_pro()
@@ -1,297 +0,0 @@
import asyncio
import os
import signal
import sys
import traceback
from pysdk.grvt_ccxt_env import GrvtEnv, GrvtWSEndpointType
from pysdk.grvt_ccxt_logging_selector import logger
from pysdk.grvt_ccxt_types import GrvtOrderSide
from pysdk.grvt_ccxt_utils import rand_uint32
from pysdk.grvt_ccxt_ws import GrvtCcxtWS
# Utility functions , not called directly by the __main__ test routine
async def callback_general(message: dict) -> None:
message.get("params", {}).get("channel")
logger.info(f"callback_general(): message:{message}")
async def grvt_ws_subscribe(api: GrvtCcxtWS, args_list: dict) -> None:
"""Subscribes to Websocket channels/feeds in args list."""
for stream, (callback, ws_endpoint_type, params) in args_list.items():
logger.info(f"Subscribing to {stream} {params=}")
await api.subscribe(
stream=stream,
callback=callback,
ws_end_point_type=ws_endpoint_type,
params=params,
)
await asyncio.sleep(0)
async def subscribe(loop) -> GrvtCcxtWS:
"""Subscribe to Websocket channels and feeds."""
params = {
"api_key": os.getenv("GRVT_API_KEY"),
"trading_account_id": os.getenv("GRVT_TRADING_ACCOUNT_ID"),
"api_ws_version": os.getenv("GRVT_WS_STREAM_VERSION", "v1"),
}
if os.getenv("GRVT_PRIVATE_KEY"):
params["private_key"] = os.getenv("GRVT_PRIVATE_KEY")
env = GrvtEnv(os.getenv("GRVT_ENV", "testnet"))
test_api = GrvtCcxtWS(env, loop, logger, parameters=params)
await test_api.initialize()
pub_args_dict = {
# ********* Market Data *********
"mini.s": (
callback_general,
None, # use deafult endpoint
{"instrument": "BTC_USDT_Perp"},
),
"mini.d": (
callback_general,
GrvtWSEndpointType.MARKET_DATA_RPC_FULL,
{"instrument": "BTC_USDT_Perp", "rate": 0},
),
"ticker.s": (
callback_general,
None, # use deafult endpoint
{"instrument": "BTC_USDT_Perp"},
),
"ticker.d": (
callback_general,
GrvtWSEndpointType.MARKET_DATA_RPC_FULL,
{"instrument": "BTC_USDT_Perp"},
),
"book.s": (
callback_general,
GrvtWSEndpointType.MARKET_DATA_RPC_FULL,
{"instrument": "BTC_USDT_Perp"},
),
"book.d": (
callback_general,
GrvtWSEndpointType.MARKET_DATA_RPC_FULL,
{"instrument": "BTC_USDT_Perp"},
),
"trade": (
callback_general,
GrvtWSEndpointType.MARKET_DATA_RPC_FULL,
{"instrument": "BTC_USDT_Perp"},
),
"candle": (
callback_general,
GrvtWSEndpointType.MARKET_DATA_RPC_FULL,
{
"instrument": "BTC_USDT_Perp",
"interval": "CI_1_M",
"type": "TRADE",
},
),
}
prv_args_dict = {
# ********* Trade Data *********
"position": (
callback_general,
GrvtWSEndpointType.TRADE_DATA_RPC_FULL,
{},
),
"order": (
callback_general,
GrvtWSEndpointType.TRADE_DATA_RPC_FULL,
{
"instrument": "BTC_USDT_Perp",
},
),
"cancel": (
callback_general,
GrvtWSEndpointType.TRADE_DATA_RPC_FULL,
{},
),
"state": (
callback_general,
GrvtWSEndpointType.TRADE_DATA_RPC_FULL,
{
"instrument": "BTC_USDT_Perp",
},
),
"fill": (
callback_general,
GrvtWSEndpointType.TRADE_DATA_RPC_FULL,
{
"instrument": "BTC_USDT_Perp",
},
),
"deposit": (callback_general, GrvtWSEndpointType.TRADE_DATA, {}),
"transfer": (callback_general, GrvtWSEndpointType.TRADE_DATA, {}),
"withdrawal": (callback_general, GrvtWSEndpointType.TRADE_DATA, {}),
}
try:
if "private_key" in params:
await grvt_ws_subscribe(test_api, {**pub_args_dict, **prv_args_dict})
else: # not private_key , subscribe to public feeds only
await grvt_ws_subscribe(test_api, pub_args_dict)
except Exception as e:
logger.error(f"Error in grvt_ws_subscribe: {e} {traceback.format_exc()}")
return test_api
async def rpc_create_order(
test_api: GrvtCcxtWS, side: GrvtOrderSide, price: str, client_order_id: str = ""
) -> str:
if test_api and test_api._private_key:
# Send order
if not client_order_id:
client_order_id = str(rand_uint32())
payload = await test_api.rpc_create_order(
symbol="BTC_USDT_Perp",
order_type="limit",
side=side,
amount=0.001,
price=price,
params={
"client_order_id": client_order_id,
# "time_in_force": "IMMEDIATE_OR_CANCEL",
"time_in_force": "GOOD_TILL_TIME",
},
)
logger.info(f"rpc_create_order: {payload=}")
return client_order_id
return ""
async def rpc_create_mkt_order(
test_api: GrvtCcxtWS, symbol: str, side: GrvtOrderSide, client_order_id: str = ""
) -> str:
FN = "rpc_create_mkt_order"
if test_api and test_api._private_key:
# Send order
if not client_order_id:
client_order_id = str(rand_uint32())
payload = await test_api.rpc_create_order(
symbol=symbol,
order_type="market",
side=side,
amount=0.001,
params={
"client_order_id": client_order_id,
"time_in_force": "GOOD_TILL_TIME",
},
)
logger.info(f"{FN}: {payload=}")
return client_order_id
return ""
async def rpc_fetch_order(test_api: GrvtCcxtWS, client_order_id: str) -> None:
if test_api and test_api._private_key:
# Send order
payload = await test_api.rpc_fetch_order(
params={
"client_order_id": client_order_id,
},
)
logger.info(f"rpc_fetch_order: {payload=}")
async def rpc_fetch_open_orders(test_api: GrvtCcxtWS) -> None:
if test_api and test_api._private_key:
# Send order
payload: dict = await test_api.rpc_fetch_open_orders()
logger.info(f"rpc_fetch_open_orders: {payload=}")
async def rpc_cancel_order(
test_api: GrvtCcxtWS, client_order_id: str, time_to_live_ms: str = ""
) -> None:
if test_api and test_api._private_key:
# Send order
params: dict = {"client_order_id": client_order_id}
if time_to_live_ms:
params["time_to_live_ms"] = time_to_live_ms
payload = await test_api.rpc_cancel_order(params=params)
logger.info(f"rpc_cancel_order: {payload=}")
async def rpc_cancel_all_orders(test_api: GrvtCcxtWS) -> None:
if test_api and test_api._private_key:
# Send order
payload: dict = await test_api.rpc_cancel_all_orders()
logger.info(f"rpc_cancel_all_orders: {payload=}")
async def cancel_send_rpc_order(test_api: GrvtCcxtWS) -> None:
FN = "cancel_send_rpc_order"
"""Cancels order then sends an order to be canceled."""
if test_api and test_api._private_key:
cloid = str(rand_uint32())
logger.info(f"{FN} {cloid=}")
await rpc_cancel_order(test_api, cloid, time_to_live_ms="0")
await asyncio.sleep(1)
await rpc_cancel_order(test_api, cloid, time_to_live_ms="5000")
await asyncio.sleep(0.01)
await rpc_create_mkt_order(
test_api, symbol="BTC_USDT_Perp", side="buy", client_order_id=cloid
)
await asyncio.sleep(0.01)
await rpc_fetch_order(test_api, cloid)
async def send_check_cancel_rpc_order(test_api: GrvtCcxtWS) -> None:
if test_api and test_api._private_key:
# Send order
cloid = await rpc_create_order(test_api, side="buy", price="60000")
if cloid:
await rpc_fetch_open_orders(test_api)
await rpc_fetch_order(test_api, cloid)
await asyncio.sleep(5)
await rpc_cancel_order(test_api, cloid)
cloid = await rpc_create_order(test_api, side="sell", price="70000")
if cloid:
await rpc_fetch_open_orders(test_api)
await rpc_fetch_order(test_api, cloid)
await asyncio.sleep(5)
await rpc_cancel_all_orders(test_api)
async def send_rpc_messages(test_api: GrvtCcxtWS) -> None:
"""Sends test RPC messages for send/fetch/cancel orders."""
await send_check_cancel_rpc_order(test_api)
await cancel_send_rpc_order(test_api)
async def shutdown(loop, test_api: GrvtCcxtWS) -> None:
"""Clean up resources and stop the bot gracefully."""
import time
logger.info("Shutting down gracefully...")
if test_api:
for stream, message in test_api._last_message.items():
logger.info(f"Last message: {stream=} {message=}")
logger.info("Delete GrvtCcxtWS...")
del test_api # Close the websocket connection and session
time.sleep(3) # Allow time for cleanup
await asyncio.sleep(5) # Allow time for cleanup
logger.info("Cancelling all tasks...")
tasks = [t for t in asyncio.all_tasks(loop) if t is not asyncio.current_task(loop)]
_ = [task.cancel() for task in tasks]
logger.info(f"Cancelling {len(tasks)=}")
await asyncio.gather(*tasks, return_exceptions=True)
logger.info("Shutdown complete.")
sys.exit(0)
if __name__ == "__main__":
loop = asyncio.get_event_loop()
test_api = loop.run_until_complete(subscribe(loop))
if not test_api:
logger.error("Failed to subscribe to Websocket channels.")
sys.exit(1)
for sig in (signal.SIGINT, signal.SIGTERM):
loop.add_signal_handler(
sig, lambda: asyncio.create_task(shutdown(loop, test_api))
)
loop.run_until_complete(asyncio.sleep(5))
loop.run_until_complete(send_rpc_messages(test_api))
loop.run_forever()
loop.close()
@@ -1,137 +0,0 @@
import asyncio
from pysdk import grvt_raw_types
from pysdk.grvt_raw_async import GrvtRawAsync
from pysdk.grvt_raw_base import GrvtError
from .test_raw_utils import (
get_config,
get_test_order,
get_test_transfer,
get_test_withdrawal,
)
async def get_all_instruments() -> None:
api = GrvtRawAsync(config=get_config())
resp = await api.get_all_instruments_v1(
grvt_raw_types.ApiGetAllInstrumentsRequest(is_active=True)
)
if isinstance(resp, GrvtError):
raise ValueError(f"Received error: {resp}")
if resp.result is None:
raise ValueError("Expected results to be non-null")
if len(resp.result) == 0:
raise ValueError("Expected results to be non-empty")
async def open_orders() -> None:
api = GrvtRawAsync(config=get_config())
# Skip test if trading account id is not set
if api.config.trading_account_id is None or api.config.api_key is None:
return None # Skip test if configs are not set
resp = await api.open_orders_v1(
grvt_raw_types.ApiOpenOrdersRequest(
sub_account_id=str(api.config.trading_account_id),
kind=[grvt_raw_types.Kind.PERPETUAL],
base=["BTC", "ETH"],
quote=["USDT"],
)
)
if isinstance(resp, GrvtError):
api.logger.error(f"Received error: {resp}")
return None
if resp.result is None:
raise ValueError("Expected orders to be non-null")
if len(resp.result) == 0:
api.logger.info("Expected orders to be non-empty")
async def create_order_with_signing() -> None:
api = GrvtRawAsync(config=get_config())
inst_resp = await api.get_all_instruments_v1(
grvt_raw_types.ApiGetAllInstrumentsRequest(is_active=True)
)
if isinstance(inst_resp, GrvtError):
raise ValueError(f"Received error: {inst_resp}")
order = get_test_order(api, {inst.instrument: inst for inst in inst_resp.result})
if order is None:
return None # Skip test if configs are not set
resp = await api.create_order_v1(grvt_raw_types.ApiCreateOrderRequest(order=order))
if isinstance(resp, GrvtError):
raise ValueError(f"Received error: {resp}")
if resp.result is None:
raise ValueError("Expected order to be non-null")
async def transfer_with_signing_async() -> None:
api = GrvtRawAsync(config=get_config())
transfer = get_test_transfer(api)
if transfer is None:
return None # Skip test if configs are not set
resp = await api.transfer_v1(
grvt_raw_types.ApiTransferRequest(
transfer.from_account_id,
transfer.from_sub_account_id,
transfer.to_account_id,
transfer.to_sub_account_id,
transfer.currency,
transfer.num_tokens,
transfer.signature,
)
)
if isinstance(resp, GrvtError):
raise ValueError(f"Received error: {resp}")
if resp.result is None:
raise ValueError("Expected transfer response to be non-null")
async def withdrawal_with_signing_async() -> None:
api = GrvtRawAsync(config=get_config())
withdrawal = get_test_withdrawal(api)
if withdrawal is None:
return None # Skip test if configs are not set
resp = await api.withdrawal_v1(
grvt_raw_types.ApiWithdrawalRequest(
withdrawal.from_account_id,
withdrawal.to_eth_address,
withdrawal.currency,
withdrawal.num_tokens,
withdrawal.signature,
)
)
if isinstance(resp, GrvtError):
raise ValueError(f"Received error: {resp}")
if resp.result is None:
raise ValueError("Expected withdrawal response to be non-null")
def test_get_all_instruments() -> None:
asyncio.run(get_all_instruments())
def test_open_orders() -> None:
asyncio.run(open_orders())
def test_create_order_with_signing() -> None:
asyncio.run(create_order_with_signing())
def test_transfer_with_signing_async() -> None:
asyncio.run(transfer_with_signing_async())
def test_withdrawal_with_signing_async() -> None:
asyncio.run(withdrawal_with_signing_async())
@@ -1,400 +0,0 @@
import logging
import traceback
from pprint import pprint
from eth_account import Account
from pysdk.grvt_fixed_types import Transfer
from pysdk.grvt_raw_base import GrvtApiConfig
from pysdk.grvt_raw_env import GrvtEnv
from pysdk.grvt_raw_signing import sign_order, sign_transfer
from pysdk.grvt_raw_types import (
Instrument,
InstrumentSettlementPeriod,
Kind,
Order,
OrderLeg,
OrderMetadata,
Signature,
TimeInForce,
TransferType,
)
# Setup logger
logging.basicConfig()
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
def test_sign_order_table():
private_key = "f7934647276a6e1fa0af3f4467b4b8ddaf45d25a7368fa1a295eef49a446819d"
sub_account_id = "8289849667772468"
expiry = 1730800479321350000
nonce = 828700936
test_cases = [
{
"name": "test decimal precision 1, 3 decimals",
"order": Order(
metadata=OrderMetadata(
client_order_id="1", create_time="1730800479321350000"
),
sub_account_id=sub_account_id,
time_in_force=TimeInForce.GOOD_TILL_TIME,
post_only=False,
is_market=False,
reduce_only=False,
legs=[
OrderLeg(
instrument="BTC_USDT_Perp",
size="1.013",
limit_price="68900.5",
is_buying_asset=False,
)
],
signature=Signature(
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
),
),
"want_r": "0xb00512d986a718b15136a8ba23de1c1ec84bbdb9958629cbbe4909bae620bb04",
"want_s": "0x79f706de61c68cc14d7734594b5d8689df2b2a7b25951f9a3f61d799f4327ffc",
"want_v": 28,
"want_error": None,
},
{
"name": "test decimal precision 2, 9 decimals",
"order": Order(
metadata=OrderMetadata(
client_order_id="1", create_time="1730800479321350000"
),
sub_account_id=sub_account_id,
time_in_force=TimeInForce.GOOD_TILL_TIME,
post_only=False,
is_market=False,
reduce_only=False,
legs=[
OrderLeg(
instrument="BTC_USDT_Perp",
size="1.123123123",
limit_price="68900.777123479",
is_buying_asset=False,
)
],
signature=Signature(
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
),
),
"want_r": "0x365ec79d299c8bcd5f2acff89faf741a90ca02a4b8a6b1b1a5d4f3d16130f9f0",
"want_s": "0x465129bca7855f008ea5bc22fe3ee630e4a8e3b9b99c1745631deef29957048a",
"want_v": 28,
"want_error": None,
},
{
"name": "test decimal precision 3, round down",
"order": Order(
metadata=OrderMetadata(
client_order_id="1", create_time="1730800479321350000"
),
sub_account_id=sub_account_id,
time_in_force=TimeInForce.GOOD_TILL_TIME,
post_only=False,
is_market=False,
reduce_only=False,
legs=[
OrderLeg(
instrument="BTC_USDT_Perp",
size="1.1231231234",
limit_price="68900.7771234794",
is_buying_asset=False,
)
],
signature=Signature(
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
),
),
"want_r": "0x365ec79d299c8bcd5f2acff89faf741a90ca02a4b8a6b1b1a5d4f3d16130f9f0",
"want_s": "0x465129bca7855f008ea5bc22fe3ee630e4a8e3b9b99c1745631deef29957048a",
"want_v": 28,
"want_error": None,
},
{
"name": "test decimal precision 4, round down",
"order": Order(
metadata=OrderMetadata(
client_order_id="1", create_time="1730800479321350000"
),
sub_account_id=sub_account_id,
time_in_force=TimeInForce.GOOD_TILL_TIME,
post_only=False,
is_market=False,
reduce_only=False,
legs=[
OrderLeg(
instrument="BTC_USDT_Perp",
size="1.1231231239",
limit_price="68900.7771234799",
is_buying_asset=False,
)
],
signature=Signature(
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
),
),
"want_r": "0x365ec79d299c8bcd5f2acff89faf741a90ca02a4b8a6b1b1a5d4f3d16130f9f0",
"want_s": "0x465129bca7855f008ea5bc22fe3ee630e4a8e3b9b99c1745631deef29957048a",
"want_v": 28,
"want_error": None,
},
# {
# "name": "no private key",
# "order": Order(),
# "want_error": ValueError("Private key is not set")
# },
# {
# "name": "decimal precision test",
# "order": Order(
# sub_account_id="123",
# time_in_force=TimeInForce.GOOD_TILL_TIME,
# legs=[
# OrderLeg(
# instrument="BTC_USDT_Perp",
# size="1.013",
# limit_price="64170.7",
# is_buying_asset=True
# )
# ],
# signature=Signature(
# expiration=expiry,
# nonce=nonce
# )
# ),
# "want_error": None
# }
]
account = Account.from_key(private_key)
instruments = {
"BTC_USDT_Perp": Instrument(
instrument="BTC_USDT_Perp",
instrument_hash="0x030501",
base="BTC",
quote="USDT",
kind=Kind.PERPETUAL,
venues=[],
settlement_period=InstrumentSettlementPeriod.DAILY,
tick_size="0.00000001",
min_size="0.00000001",
create_time="123",
base_decimals=9,
quote_decimals=9,
max_position_size="1000000",
)
}
for tc in test_cases:
config = GrvtApiConfig(
env=GrvtEnv.TESTNET,
private_key=private_key,
trading_account_id=sub_account_id,
api_key="not-needed",
logger=logger,
)
signed_order = sign_order(tc["order"], config, account, instruments)
pprint(signed_order)
# Verify signature fields are populated
assert signed_order.signature.signer == str(account.address)
# Compare r, s, v values with expected values
if "want_r" in tc:
assert (
signed_order.signature.r == tc["want_r"]
), f"Test '{tc['name']}' failed: r value mismatch"
if "want_s" in tc:
assert (
signed_order.signature.s == tc["want_s"]
), f"Test '{tc['name']}' failed: s value mismatch"
if "want_v" in tc:
assert (
signed_order.signature.v == tc["want_v"]
), f"Test '{tc['name']}' failed: v value mismatch"
def test_sign_transfer_table():
chainId = 1
private_key = "f7934647276a6e1fa0af3f4467b4b8ddaf45d25a7368fa1a295eef49a446819d"
main_account_id = "0x0c1f4c8ee7acd9ea19b91bbb343cbaf6efd58ce1"
sub_account_id = "8289849667772468"
expiry = "1730800479321350000"
nonce = 828700936
test_cases = [
{
"name": "Transfer $1 from main account to sub account",
"transfer": Transfer(
from_account_id=main_account_id,
from_sub_account_id="0",
to_account_id=main_account_id,
to_sub_account_id=sub_account_id,
currency="USDT",
num_tokens="1",
signature=Signature(
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
),
transfer_type=TransferType.STANDARD,
transfer_metadata="",
),
"want_r": "0x21c7d7a8e225cb146c80dc79bbe818f915536817f1343e974cfdbe2bfc952cf1",
"want_s": "0x6de83999555f6236e5a56c86876defe00b4776c428ab5a4d7f997d290baaea10",
"want_v": 28,
"want_error": None,
},
{
"name": "Transfer $1.5 from main account to sub account",
"transfer": Transfer(
from_account_id=main_account_id,
from_sub_account_id="0",
to_account_id=main_account_id,
to_sub_account_id=sub_account_id,
currency="USDT",
num_tokens="1.5",
signature=Signature(
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
),
transfer_type=TransferType.STANDARD,
transfer_metadata="",
),
"want_r": "0xe0a9c66d8d11c3a9ae3624e150cbbdf85d542722cac5255cad4e50af5ac1ddcb",
"want_s": "0x06769e5284352ead5735b8b11f9e9510d024bbb889a828889db4cb04132b52aa",
"want_v": 28,
"want_error": None,
},
{
"name": "Transfer $1 from sub account to main account",
"transfer": Transfer(
from_account_id=main_account_id,
from_sub_account_id=sub_account_id,
to_account_id=main_account_id,
to_sub_account_id="0",
currency="USDT",
num_tokens="1",
signature=Signature(
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
),
transfer_type=TransferType.STANDARD,
transfer_metadata="",
),
"want_r": "0xc1214ee17dbc14f183297b9dd3f93120b16e633691817ee26045451bc629101c",
"want_s": "0x2de27d226a3d3188742629ab222d430d7989d6ea3e6a86bc259606e371123df3",
"want_v": 27,
"want_error": None,
},
{
"name": "Transfer $1.5 from sub account to main account",
"transfer": Transfer(
from_account_id=main_account_id,
from_sub_account_id=sub_account_id,
to_account_id=main_account_id,
to_sub_account_id="0",
currency="USDT",
num_tokens="1.5",
signature=Signature(
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
),
transfer_type=TransferType.STANDARD,
transfer_metadata="",
),
"want_r": "0xb9b80dfd4b0d53e64b6dd1067d7d936c79a8c3966175bcefb2021cc71d08116f",
"want_s": "0x3cbe955c9f56e41f70c658e02df07873e77d347aa5c422943b87fdfd94293ae6",
"want_v": 27,
"want_error": None,
},
{
"name": "Transfer $1 external",
"transfer": Transfer(
from_account_id="0x922a4874196806460fc63b5bcbff45f94c87f76f",
from_sub_account_id="0",
to_account_id="0x6a3434fce60ff567f60d80fb98f2f981e9b081fd",
to_sub_account_id="0",
currency="USDT",
num_tokens="1",
signature=Signature(
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
),
transfer_type=TransferType.STANDARD,
transfer_metadata="",
),
"want_r": "0x185ad129ca0de3584fcd91f6df0c25d8065411041db117c50dabd057249a1a43",
"want_s": "0x58b1979c1f8c65970578bc2756f17a0b5c7352c27007811800dcd8966351647d",
"want_v": 28,
"want_error": None,
},
{
"name": "Transfer $1.5 external revert",
"transfer": Transfer(
from_account_id="0x6a3434fce60ff567f60d80fb98f2f981e9b081fd",
from_sub_account_id="0",
to_account_id="0x922a4874196806460fc63b5bcbff45f94c87f76f",
to_sub_account_id="0",
currency="USDT",
num_tokens="1.5",
signature=Signature(
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
),
transfer_type=TransferType.STANDARD,
transfer_metadata="",
),
"want_r": "0xbbdd4726fef5cddb6eeb5fac07ed95702673293133d66481777a6a3ee82adc12",
"want_s": "0x0ad3592ea3ebf428b260c56723386383253481bc188d9aeb87d9b21b85069821",
"want_v": 28,
"want_error": None,
},
]
account = Account.from_key(private_key)
config = GrvtApiConfig(
env=GrvtEnv.TESTNET,
private_key=private_key,
trading_account_id=sub_account_id,
api_key="not-needed",
logger=logger,
)
for tc in test_cases:
signed = sign_transfer(tc["transfer"], config, account, chainId)
pprint(signed)
# Verify signature fields are populated
assert signed.signature.signer == str(account.address)
# Compare r, s, v values with expected values
if "want_r" in tc:
assert (
signed.signature.r == tc["want_r"]
), f"Test '{tc['name']}' failed: r value mismatch"
if "want_s" in tc:
assert (
signed.signature.s == tc["want_s"]
), f"Test '{tc['name']}' failed: s value mismatch"
if "want_v" in tc:
assert (
signed.signature.v == tc["want_v"]
), f"Test '{tc['name']}' failed: v value mismatch"
def main():
functions = [
test_sign_order_table,
test_sign_transfer_table,
]
for f in functions:
try:
f()
except Exception as e:
logger.error(f"Error in {f.__name__}: {e} {traceback.format_exc()}")
if __name__ == "__main__":
main()
@@ -1,837 +0,0 @@
import json
import logging
import traceback
from eth_account import Account
from eth_account.messages import encode_typed_data
from pysdk.grvt_fixed_types import Transfer
from pysdk.grvt_raw_base import GrvtApiConfig
from pysdk.grvt_raw_env import GrvtEnv
from pysdk.grvt_raw_signing import (
EIP712_ORDER_MESSAGE_TYPE,
EIP712_TRANSFER_MESSAGE_TYPE,
build_EIP712_order_message_data,
build_EIP712_transfer_message_data,
get_EIP712_domain_data,
sign_order,
)
from pysdk.grvt_raw_types import (
Instrument,
InstrumentSettlementPeriod,
Kind,
Order,
OrderLeg,
OrderMetadata,
Signature,
TimeInForce,
TransferType,
)
# Setup logger
logging.basicConfig()
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
def test_sign_order_table():
# Generated using https://key.tokenpocket.pro/#/?network=ETH
# NOTE: `0x` hexadecimal prefix removed
public_key = "ee2060eECaC18beC7F8F670D751801294911E445"
private_key = "c0663ca94684aead40c41a1cb3a94b68a24296e87245be3a186e882a29a15ee0"
sub_account_id = "8289849667772468"
expiry = 1730800479321350000
nonce = 828700936
test_cases = [
{
"name": "test decimal precision 1, 3 decimals",
"order": Order(
metadata=OrderMetadata(
client_order_id="1", create_time="1730800479321350000"
),
sub_account_id=sub_account_id,
time_in_force=TimeInForce.GOOD_TILL_TIME,
post_only=False,
is_market=False,
reduce_only=False,
legs=[
OrderLeg(
instrument="BTC_USDT_Perp",
size="1.013",
limit_price="68900.5",
is_buying_asset=False,
)
],
signature=Signature(
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
),
),
"expected_message_data_json": """
{
"subAccountID": "8289849667772468",
"isMarket": false,
"timeInForce": 1,
"postOnly": false,
"reduceOnly": false,
"legs": [
{
"assetID": "0x030501",
"contractSize": 1013000000,
"limitPrice": 68900500000000,
"isBuyingContract": false
}
],
"nonce": 828700936,
"expiration": 1730800479321350000
}""",
"expected_domain_data_json": """
{
"name": "GRVT Exchange",
"version": "0",
"chainId": 326
}""",
# Per EIP-191: https://eips.ethereum.org/EIPS/eip-191
"expected_signable_message": """
{
"version": "01",
"header": "1254f97f8495f704630a238cbcd898a4b8ab20d77bb93e17049d3445f4f81f16",
"body": "41650c08ab6e720f899307d7c2b4381a10c1301888375cec2dfefd6a583859eb"
}""",
# EIP-712 signable message format: https://eips.ethereum.org/EIPS/eip-712
# Pre-image of the signed message's message hash
# encode(domainSeparator : 𝔹²⁵⁶, message : 𝕊) = "\x19"‖ version ‖ domainSeparator ‖ hashStruct(message)
"digest_input": "0x19011254f97f8495f704630a238cbcd898a4b8ab20d77bb93e17049d3445f4f81f1641650c08ab6e720f899307d7c2b4381a10c1301888375cec2dfefd6a583859eb",
"expected_signed_message": """
{
"message_hash": "03cb7ca7b353969ab2c00ff92fd472f81f59a84d28fa1aa39128176f21062982",
"r": 14615867946748605809126568669694142791729730263440553623296112010401671344650,
"s": 41758084966111141828834491248882149351523023670747687501271185591898818786045,
"v": 28,
"signature": "205049c0db6e38fd88e4e46be81db7bc354520d52ec6268d210fa35b86c4f20a5c523d0ff8e6f12542fdcf34a6e6e2c547986b7c8fa53f86a5e656d9ab44b2fd1c"
}""",
},
{
"name": "test decimal precision 2, 9 decimals",
"order": Order(
metadata=OrderMetadata(
client_order_id="1", create_time="1730800479321350000"
),
sub_account_id=sub_account_id,
time_in_force=TimeInForce.GOOD_TILL_TIME,
post_only=False,
is_market=False,
reduce_only=False,
legs=[
OrderLeg(
instrument="BTC_USDT_Perp",
size="1.123123123",
limit_price="68900.777123479",
is_buying_asset=False,
)
],
signature=Signature(
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
),
),
"expected_message_data_json": """
{
"subAccountID": "8289849667772468",
"isMarket": false,
"timeInForce": 1,
"postOnly": false,
"reduceOnly": false,
"legs": [
{
"assetID": "0x030501",
"contractSize": 1123123123,
"limitPrice": 68900777123479,
"isBuyingContract": false
}
],
"nonce": 828700936,
"expiration": 1730800479321350000
}""",
"expected_domain_data_json": """
{
"name": "GRVT Exchange",
"version": "0",
"chainId": 326
}""",
"expected_signable_message": """
{
"version": "01",
"header": "1254f97f8495f704630a238cbcd898a4b8ab20d77bb93e17049d3445f4f81f16",
"body": "e85ffec169d4ebd4b8057e5fbd31ed31d4511b2f2299bbce0adab6beb5fe2814"
}""",
"digest_input": "0x19011254f97f8495f704630a238cbcd898a4b8ab20d77bb93e17049d3445f4f81f16e85ffec169d4ebd4b8057e5fbd31ed31d4511b2f2299bbce0adab6beb5fe2814",
"expected_signed_message": """
{
"message_hash": "2d0a437f7d64523386974c2a729d69604f8e2a7f0684b7de923845d8b175ab69",
"r": 30067953785684815030293507105225786115862149795459199007947867478230986262090,
"s": 23299979093064779986156565292425191814752388247725004533191995996670719399433,
"v": 28,
"signature": "4279dbd734584fb07b016bce7d9f029e7f7f80e378dc65cddc06aa6b0c9e064a33835221a0fbcb700de3068169b45572a27756bff479f2887e8c89139e6f96091c"
}""",
},
{
"name": "test decimal precision 3, round down",
"order": Order(
metadata=OrderMetadata(
client_order_id="1", create_time="1730800479321350000"
),
sub_account_id=sub_account_id,
time_in_force=TimeInForce.GOOD_TILL_TIME,
post_only=False,
is_market=False,
reduce_only=False,
legs=[
OrderLeg(
instrument="BTC_USDT_Perp",
size="1.1231231234",
limit_price="68900.7771234794",
is_buying_asset=False,
)
],
signature=Signature(
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
),
),
"expected_message_data_json": """
{
"subAccountID": "8289849667772468",
"isMarket": false,
"timeInForce": 1,
"postOnly": false,
"reduceOnly": false,
"legs": [
{
"assetID": "0x030501",
"contractSize": 1123123123,
"limitPrice": 68900777123479,
"isBuyingContract": false
}
],
"nonce": 828700936,
"expiration": 1730800479321350000
}""",
"expected_domain_data_json": """
{
"name": "GRVT Exchange",
"version": "0",
"chainId": 326
}""",
"expected_signable_message": """
{
"version": "01",
"header": "1254f97f8495f704630a238cbcd898a4b8ab20d77bb93e17049d3445f4f81f16",
"body": "e85ffec169d4ebd4b8057e5fbd31ed31d4511b2f2299bbce0adab6beb5fe2814"
}""",
"digest_input": "0x19011254f97f8495f704630a238cbcd898a4b8ab20d77bb93e17049d3445f4f81f16e85ffec169d4ebd4b8057e5fbd31ed31d4511b2f2299bbce0adab6beb5fe2814",
"expected_signed_message": """
{
"message_hash": "2d0a437f7d64523386974c2a729d69604f8e2a7f0684b7de923845d8b175ab69",
"r": 30067953785684815030293507105225786115862149795459199007947867478230986262090,
"s": 23299979093064779986156565292425191814752388247725004533191995996670719399433,
"v": 28,
"signature": "4279dbd734584fb07b016bce7d9f029e7f7f80e378dc65cddc06aa6b0c9e064a33835221a0fbcb700de3068169b45572a27756bff479f2887e8c89139e6f96091c"
}""",
},
{
"name": "test decimal precision 4, round down",
"order": Order(
metadata=OrderMetadata(
client_order_id="1", create_time="1730800479321350000"
),
sub_account_id=sub_account_id,
time_in_force=TimeInForce.GOOD_TILL_TIME,
post_only=False,
is_market=False,
reduce_only=False,
legs=[
OrderLeg(
instrument="BTC_USDT_Perp",
size="1.1231231239",
limit_price="68900.7771234799",
is_buying_asset=False,
)
],
signature=Signature(
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
),
),
"expected_message_data_json": """
{
"subAccountID": "8289849667772468",
"isMarket": false,
"timeInForce": 1,
"postOnly": false,
"reduceOnly": false,
"legs": [
{
"assetID": "0x030501",
"contractSize": 1123123123,
"limitPrice": 68900777123479,
"isBuyingContract": false
}
],
"nonce": 828700936,
"expiration": 1730800479321350000
}""",
"expected_domain_data_json": """
{
"name": "GRVT Exchange",
"version": "0",
"chainId": 326
}""",
"expected_signable_message": """
{
"version": "01",
"header": "1254f97f8495f704630a238cbcd898a4b8ab20d77bb93e17049d3445f4f81f16",
"body": "e85ffec169d4ebd4b8057e5fbd31ed31d4511b2f2299bbce0adab6beb5fe2814"
}""",
"digest_input": "0x19011254f97f8495f704630a238cbcd898a4b8ab20d77bb93e17049d3445f4f81f16e85ffec169d4ebd4b8057e5fbd31ed31d4511b2f2299bbce0adab6beb5fe2814",
"expected_signed_message": """
{
"message_hash": "2d0a437f7d64523386974c2a729d69604f8e2a7f0684b7de923845d8b175ab69",
"r": 30067953785684815030293507105225786115862149795459199007947867478230986262090,
"s": 23299979093064779986156565292425191814752388247725004533191995996670719399433,
"v": 28,
"signature": "4279dbd734584fb07b016bce7d9f029e7f7f80e378dc65cddc06aa6b0c9e064a33835221a0fbcb700de3068169b45572a27756bff479f2887e8c89139e6f96091c"
}""",
},
]
account = Account.from_key(private_key)
instruments = {
"BTC_USDT_Perp": Instrument(
instrument="BTC_USDT_Perp",
instrument_hash="0x030501",
base="BTC",
quote="USDT",
kind=Kind.PERPETUAL,
venues=[],
settlement_period=InstrumentSettlementPeriod.DAILY,
tick_size="0.00000001",
min_size="0.00000001",
create_time="123",
base_decimals=9,
quote_decimals=9,
max_position_size="1000000",
)
}
for tc in test_cases:
config = GrvtApiConfig(
env=GrvtEnv.TESTNET,
private_key=private_key,
trading_account_id=sub_account_id,
api_key="not-needed",
logger=logger,
)
# Get intermediate values
message_data = build_EIP712_order_message_data(tc["order"], instruments)
domain_data = get_EIP712_domain_data(config.env, 326)
signable_message = encode_typed_data(
domain_data, EIP712_ORDER_MESSAGE_TYPE, message_data
)
signed_message = account.sign_message(signable_message)
signed_order = sign_order(tc["order"], config, account, instruments)
# Convert to comparable strings
message_data_json = json.dumps(message_data, indent=2)
domain_data_json = json.dumps(domain_data, indent=2)
signable_message_json = json.dumps(
{
"version": signable_message.version.hex(),
"header": signable_message.header.hex(),
"body": signable_message.body.hex(),
},
indent=2,
)
signed_message_json = json.dumps(
{
"message_hash": signed_message.message_hash.hex(),
"r": signed_message.r,
"s": signed_message.s,
"v": signed_message.v,
"signature": signed_message.signature.hex(),
},
indent=2,
)
# EIP-712 signable message format: https://eips.ethereum.org/EIPS/eip-712
signed_message_hash_preimage = (
"0x19"
+ signable_message.version.hex()
+ signable_message.header.hex()
+ signable_message.body.hex()
)
# Strip whitespace for comparison
assert (
message_data_json.strip() == tc["expected_message_data_json"].strip()
), f"""
Test '{tc['name']}' failed: message_data mismatch.
Wanted:
{tc['expected_message_data_json'].strip()}
Got:
{message_data_json.strip()}
"""
assert (
domain_data_json.strip() == tc["expected_domain_data_json"].strip()
), f"""
Test '{tc['name']}' failed: domain_data mismatch.
Wanted:
{tc['expected_domain_data_json'].strip()}
Got:
{domain_data_json.strip()}
"""
assert (
signable_message_json.strip() == tc["expected_signable_message"].strip()
), f"""
Test '{tc['name']}' failed: signable_message mismatch.
Wanted:
{tc['expected_signable_message'].strip()}
Got:
{signable_message_json.strip()}
"""
assert (
signed_message_hash_preimage.strip() == tc["digest_input"].strip()
), f"""
Test '{tc['name']}' failed: digest_input mismatch.
Wanted:
{tc["digest_input"].strip()
}
Got:
{signed_message_hash_preimage.strip()}
"""
assert (
signed_message_json.strip() == tc["expected_signed_message"].strip()
), f"""
Test '{tc['name']}' failed: signed_message mismatch.
Wanted:
{tc['expected_signed_message'].strip()}
Got:
{signed_message_json.strip()}
"""
assert (
signed_order.signature.signer == str(account.address) == "0x" + public_key
), f"""Test '{tc['name']}' failed: signer mismatch."""
def test_sign_transfer_table():
chainId = 1
private_key = "f7934647276a6e1fa0af3f4467b4b8ddaf45d25a7368fa1a295eef49a446819d"
main_account_id = "0x0c1f4c8ee7acd9ea19b91bbb343cbaf6efd58ce1"
sub_account_id = "8289849667772468"
expiry = "1730800479321350000"
nonce = 828700936
test_cases = [
{
"name": "Transfer $1 from main account to sub account",
"transfer": Transfer(
from_account_id=main_account_id,
from_sub_account_id="0",
to_account_id=main_account_id,
to_sub_account_id=sub_account_id,
currency="USDT",
num_tokens="1",
signature=Signature(
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
),
transfer_type=TransferType.STANDARD,
transfer_metadata="",
),
"expected_message_data_json": """
{
"fromAccount": "0x0c1f4c8ee7acd9ea19b91bbb343cbaf6efd58ce1",
"fromSubAccount": "0",
"toAccount": "0x0c1f4c8ee7acd9ea19b91bbb343cbaf6efd58ce1",
"toSubAccount": "8289849667772468",
"tokenCurrency": 3,
"numTokens": 1000000,
"nonce": 828700936,
"expiration": "1730800479321350000"
}""",
"expected_domain_data_json": """
{
"name": "GRVT Exchange",
"version": "0",
"chainId": 1
}""",
# Per EIP-191: https://eips.ethereum.org/EIPS/eip-191
"expected_signable_message": """
{
"version": "01",
"header": "950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f6",
"body": "8dfbfc161ca14b60b318aec118c0f77137ab6d5c0d6f4aa283a75995ec842a9f"
}""",
# EIP-712 signable message format: https://eips.ethereum.org/EIPS/eip-712
# Pre-image of the signed message's message hash
# encode(domainSeparator : 𝔹²⁵⁶, message : 𝕊) = "\x19"‖ version ‖ domainSeparator ‖ hashStruct(message)
"digest_input": "0x1901950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f68dfbfc161ca14b60b318aec118c0f77137ab6d5c0d6f4aa283a75995ec842a9f",
"expected_signed_message": """
{
"message_hash": "f237c6e8ceaf8f75c35537c26a5810f5029fe3fa0179d636cfa75dba0eeaecda",
"r": 15279414997690414521303216846501751476684522786442627331685751803272563600625,
"s": 49712406548009011982925909577001640710775887931482920893646195243522061953552,
"v": 28,
"signature": "21c7d7a8e225cb146c80dc79bbe818f915536817f1343e974cfdbe2bfc952cf16de83999555f6236e5a56c86876defe00b4776c428ab5a4d7f997d290baaea101c"
}""",
},
{
"name": "Transfer $1.5 from main account to sub account",
"transfer": Transfer(
from_account_id=main_account_id,
from_sub_account_id="0",
to_account_id=main_account_id,
to_sub_account_id=sub_account_id,
currency="USDT",
num_tokens="1.5",
signature=Signature(
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
),
transfer_type=TransferType.STANDARD,
transfer_metadata="",
),
"expected_message_data_json": """
{
"fromAccount": "0x0c1f4c8ee7acd9ea19b91bbb343cbaf6efd58ce1",
"fromSubAccount": "0",
"toAccount": "0x0c1f4c8ee7acd9ea19b91bbb343cbaf6efd58ce1",
"toSubAccount": "8289849667772468",
"tokenCurrency": 3,
"numTokens": 1500000,
"nonce": 828700936,
"expiration": "1730800479321350000"
}""",
"expected_domain_data_json": """
{
"name": "GRVT Exchange",
"version": "0",
"chainId": 1
}""",
"expected_signable_message": """
{
"version": "01",
"header": "950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f6",
"body": "efc25031927cbee99a4b8c408d427168218312cb5fcbc2ef0644a25c411e9cd6"
}""",
"digest_input": "0x1901950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f6efc25031927cbee99a4b8c408d427168218312cb5fcbc2ef0644a25c411e9cd6",
"expected_signed_message": """
{
"message_hash": "e9a82979035693538d20cbd16a231379d68b2484f065a64b5ac962a0ef45ed2c",
"r": 101618044735866410136029494650850506089543609286068143785089360195209387957707,
"s": 2923457745704967739422721965786309607758766142290846761836065357300251710122,
"v": 28,
"signature": "e0a9c66d8d11c3a9ae3624e150cbbdf85d542722cac5255cad4e50af5ac1ddcb06769e5284352ead5735b8b11f9e9510d024bbb889a828889db4cb04132b52aa1c"
}""",
},
{
"name": "Transfer $1 from sub account to main account",
"transfer": Transfer(
from_account_id=main_account_id,
from_sub_account_id=sub_account_id,
to_account_id=main_account_id,
to_sub_account_id="0",
currency="USDT",
num_tokens="1",
signature=Signature(
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
),
transfer_type=TransferType.STANDARD,
transfer_metadata="",
),
"expected_message_data_json": """
{
"fromAccount": "0x0c1f4c8ee7acd9ea19b91bbb343cbaf6efd58ce1",
"fromSubAccount": "8289849667772468",
"toAccount": "0x0c1f4c8ee7acd9ea19b91bbb343cbaf6efd58ce1",
"toSubAccount": "0",
"tokenCurrency": 3,
"numTokens": 1000000,
"nonce": 828700936,
"expiration": "1730800479321350000"
}""",
"expected_domain_data_json": """
{
"name": "GRVT Exchange",
"version": "0",
"chainId": 1
}""",
"expected_signable_message": """
{
"version": "01",
"header": "950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f6",
"body": "325bbcdd0af37fd856ab19d5a34f04c98b4b9125fd924a41d149290d85cf5d1a"
}""",
"digest_input": "0x1901950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f6325bbcdd0af37fd856ab19d5a34f04c98b4b9125fd924a41d149290d85cf5d1a",
"expected_signed_message": """
{
"message_hash": "05a4682aaaffda9347f7f577623dc86be873d75680250d94f30e27fb450b0597",
"r": 87355230145152558239319417798390737905701563801639010151017443441731256782876,
"s": 20754249269006714296744776682911604802815023385414018875577784419437276970483,
"v": 27,
"signature": "c1214ee17dbc14f183297b9dd3f93120b16e633691817ee26045451bc629101c2de27d226a3d3188742629ab222d430d7989d6ea3e6a86bc259606e371123df31b"
}""",
},
{
"name": "Transfer $1.5 from sub account to main account",
"transfer": Transfer(
from_account_id=main_account_id,
from_sub_account_id=sub_account_id,
to_account_id=main_account_id,
to_sub_account_id="0",
currency="USDT",
num_tokens="1.5",
signature=Signature(
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
),
transfer_type=TransferType.STANDARD,
transfer_metadata="",
),
"expected_message_data_json": """
{
"fromAccount": "0x0c1f4c8ee7acd9ea19b91bbb343cbaf6efd58ce1",
"fromSubAccount": "8289849667772468",
"toAccount": "0x0c1f4c8ee7acd9ea19b91bbb343cbaf6efd58ce1",
"toSubAccount": "0",
"tokenCurrency": 3,
"numTokens": 1500000,
"nonce": 828700936,
"expiration": "1730800479321350000"
}""",
"expected_domain_data_json": """
{
"name": "GRVT Exchange",
"version": "0",
"chainId": 1
}""",
"expected_signable_message": """
{
"version": "01",
"header": "950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f6",
"body": "d20efa4f5ca3d7cd996d0a7be827a788fdea40ad9dc3f7d64f909ec3df2f0f1c"
}""",
"digest_input": "0x1901950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f6d20efa4f5ca3d7cd996d0a7be827a788fdea40ad9dc3f7d64f909ec3df2f0f1c",
"expected_signed_message": """
{
"message_hash": "57de810bfd4c46796b923242eba5fba199c69ec6159fd7f0bae13d20e6b2e32f",
"r": 84003073399296424218543069615592899281416934630850361306323613156742252728687,
"s": 27475502714605040799395938380083883707063662756512201821577577919846841662182,
"v": 27,
"signature": "b9b80dfd4b0d53e64b6dd1067d7d936c79a8c3966175bcefb2021cc71d08116f3cbe955c9f56e41f70c658e02df07873e77d347aa5c422943b87fdfd94293ae61b"
}""",
},
{
"name": "Transfer $1 external",
"transfer": Transfer(
from_account_id="0x922a4874196806460fc63b5bcbff45f94c87f76f",
from_sub_account_id="0",
to_account_id="0x6a3434fce60ff567f60d80fb98f2f981e9b081fd",
to_sub_account_id="0",
currency="USDT",
num_tokens="1",
signature=Signature(
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
),
transfer_type=TransferType.STANDARD,
transfer_metadata="",
),
"expected_message_data_json": """
{
"fromAccount": "0x922a4874196806460fc63b5bcbff45f94c87f76f",
"fromSubAccount": "0",
"toAccount": "0x6a3434fce60ff567f60d80fb98f2f981e9b081fd",
"toSubAccount": "0",
"tokenCurrency": 3,
"numTokens": 1000000,
"nonce": 828700936,
"expiration": "1730800479321350000"
}""",
"expected_domain_data_json": """
{
"name": "GRVT Exchange",
"version": "0",
"chainId": 1
}""",
"expected_signable_message": """
{
"version": "01",
"header": "950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f6",
"body": "c92edbd85a6756bbc1d472694218135800c5c34cecd62dfecf8b79696fb30a5a"
}""",
"digest_input": "0x1901950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f6c92edbd85a6756bbc1d472694218135800c5c34cecd62dfecf8b79696fb30a5a",
"expected_signed_message": """
{
"message_hash": "aa540bb37dc69583724239e2ee089f9863760b1f5dd3707f63254f5f9045a36b",
"r": 11015968193451536627767691276906473050663313657726362297424610772811255519811,
"s": 40117308978565698603770991327546519074883337154968913527997965101318785819773,
"v": 28,
"signature": "185ad129ca0de3584fcd91f6df0c25d8065411041db117c50dabd057249a1a4358b1979c1f8c65970578bc2756f17a0b5c7352c27007811800dcd8966351647d1c"
}""",
},
{
"name": "Transfer $1.5 external revert",
"transfer": Transfer(
from_account_id="0x6a3434fce60ff567f60d80fb98f2f981e9b081fd",
from_sub_account_id="0",
to_account_id="0x922a4874196806460fc63b5bcbff45f94c87f76f",
to_sub_account_id="0",
currency="USDT",
num_tokens="1.5",
signature=Signature(
signer="", r="", s="", v=0, expiration=expiry, nonce=nonce
),
transfer_type=TransferType.STANDARD,
transfer_metadata="",
),
"expected_message_data_json": """
{
"fromAccount": "0x6a3434fce60ff567f60d80fb98f2f981e9b081fd",
"fromSubAccount": "0",
"toAccount": "0x922a4874196806460fc63b5bcbff45f94c87f76f",
"toSubAccount": "0",
"tokenCurrency": 3,
"numTokens": 1500000,
"nonce": 828700936,
"expiration": "1730800479321350000"
}""",
"expected_domain_data_json": """
{
"name": "GRVT Exchange",
"version": "0",
"chainId": 1
}""",
"expected_signable_message": """
{
"version": "01",
"header": "950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f6",
"body": "5660d6d82b43a7bd01c7bee9379478b27e2c7774d9e9aebd69ea163c523f8730"
}""",
"digest_input": "0x1901950ca409f14e4f89a10e63790c40b5dc9c5fab89944e8c98112dece5b08415f65660d6d82b43a7bd01c7bee9379478b27e2c7774d9e9aebd69ea163c523f8730",
"expected_signed_message": """
{
"message_hash": "2a02bf2a7772d74f176d75f987e984dd253837aba1faed4b0a65ba5a3038ce06",
"r": 84973466961705873082056285677608514305288271637256010073726199940890275535890,
"s": 4896548729346283309439956649162331116008267310844279523516289648065258100769,
"v": 28,
"signature": "bbdd4726fef5cddb6eeb5fac07ed95702673293133d66481777a6a3ee82adc120ad3592ea3ebf428b260c56723386383253481bc188d9aeb87d9b21b850698211c"
}""",
},
]
account = Account.from_key(private_key)
config = GrvtApiConfig(
env=GrvtEnv.TESTNET,
private_key=private_key,
trading_account_id=sub_account_id,
api_key="not-needed",
logger=logger,
)
for tc in test_cases:
message_data = build_EIP712_transfer_message_data(
tc["transfer"], 3 # assume all test cases are USDT transfers
)
domain_data = get_EIP712_domain_data(config.env, chainId)
signable_message = encode_typed_data(
domain_data, EIP712_TRANSFER_MESSAGE_TYPE, message_data
)
signed_message = account.sign_message(signable_message)
# Convert to comparable strings
message_data_json = json.dumps(message_data, indent=2)
domain_data_json = json.dumps(domain_data, indent=2)
signable_message_json = json.dumps(
{
"version": signable_message.version.hex(),
"header": signable_message.header.hex(),
"body": signable_message.body.hex(),
},
indent=2,
)
signed_message_json = json.dumps(
{
"message_hash": signed_message.message_hash.hex(),
"r": signed_message.r,
"s": signed_message.s,
"v": signed_message.v,
"signature": signed_message.signature.hex(),
},
indent=2,
)
# EIP-712 signable message format: https://eips.ethereum.org/EIPS/eip-712
signed_message_hash_preimage = (
"0x19"
+ signable_message.version.hex()
+ signable_message.header.hex()
+ signable_message.body.hex()
)
# Strip whitespace for comparison
assert (
message_data_json.strip() == tc["expected_message_data_json"].strip()
), f"""
Test '{tc['name']}' failed: message_data mismatch.
Wanted:
{tc['expected_message_data_json'].strip()}
Got:
{message_data_json.strip()}
"""
assert (
domain_data_json.strip() == tc["expected_domain_data_json"].strip()
), f"""
Test '{tc['name']}' failed: domain_data mismatch.
Wanted:
{tc['expected_domain_data_json'].strip()}
Got:
{domain_data_json.strip()}
"""
assert (
signable_message_json.strip() == tc["expected_signable_message"].strip()
), f"""
Test '{tc['name']}' failed: signable_message mismatch.
Wanted:
{tc['expected_signable_message'].strip()}
Got:
{signable_message_json.strip()}
"""
assert (
signed_message_hash_preimage.strip() == tc["digest_input"].strip()
), f"""
Test '{tc['name']}' failed: digest_input mismatch.
Wanted:
{tc["digest_input"].strip()
}
Got:
{signed_message_hash_preimage.strip()}
"""
assert (
signed_message_json.strip() == tc["expected_signed_message"].strip()
), f"""
Test '{tc['name']}' failed: signed_message mismatch.
Wanted:
{tc['expected_signed_message'].strip()}
Got:
{signed_message_json.strip()}
"""
def main():
functions = [
test_sign_transfer_table,
]
for f in functions:
try:
f()
except Exception as e:
logger.error(f"Error in {f.__name__}: {e} {traceback.format_exc()}")
if __name__ == "__main__":
main()
@@ -1,141 +0,0 @@
from pysdk import grvt_raw_types
from pysdk.grvt_raw_base import GrvtError
from pysdk.grvt_raw_sync import GrvtRawSync
from .test_raw_utils import (
get_config,
get_test_order,
get_test_tpsl_order,
get_test_transfer,
get_test_withdrawal,
)
def test_get_all_instruments() -> None:
api = GrvtRawSync(config=get_config())
resp = api.get_all_instruments_v1(
grvt_raw_types.ApiGetAllInstrumentsRequest(is_active=True)
)
if isinstance(resp, GrvtError):
raise ValueError(f"Received error: {resp}")
if resp.result is None:
raise ValueError("Expected results to be non-null")
if len(resp.result) == 0:
raise ValueError("Expected results to be non-empty")
def test_open_orders() -> None:
api = GrvtRawSync(config=get_config())
# Skip test if trading account id is not set
if api.config.trading_account_id is None or api.config.api_key is None:
return None # Skip test if configs are not set
resp = api.open_orders_v1(
grvt_raw_types.ApiOpenOrdersRequest(
# sub_account_id=233, Uncomment to test error path with invalid sub account id
sub_account_id=str(api.config.trading_account_id),
kind=[grvt_raw_types.Kind.PERPETUAL],
base=["BTC", "ETH"],
quote=["USDT"],
)
)
if isinstance(resp, GrvtError):
api.logger.error(f"Received error: {resp}")
return None
if resp.result is None:
raise ValueError("Expected orders to be non-null")
if len(resp.result) == 0:
api.logger.info("Expected orders to be non-empty")
def test_create_order_with_signing() -> None:
api = GrvtRawSync(config=get_config())
inst_resp = api.get_all_instruments_v1(
grvt_raw_types.ApiGetAllInstrumentsRequest(is_active=True)
)
if isinstance(inst_resp, GrvtError):
raise ValueError(f"Received error: {inst_resp}")
order = get_test_order(api, {inst.instrument: inst for inst in inst_resp.result})
if order is None:
return None # Skip test if configs are not set
resp = api.create_order_v1(grvt_raw_types.ApiCreateOrderRequest(order=order))
if isinstance(resp, GrvtError):
raise ValueError(f"Received error: {resp}")
if resp.result is None:
raise ValueError("Expected order to be non-null")
def test_create_tpsl_order_with_signing() -> None:
api = GrvtRawSync(config=get_config())
inst_resp = api.get_all_instruments_v1(
grvt_raw_types.ApiGetAllInstrumentsRequest(is_active=True)
)
if isinstance(inst_resp, GrvtError):
raise ValueError(f"Received error: {inst_resp}")
order = get_test_tpsl_order(
api, {inst.instrument: inst for inst in inst_resp.result}
)
if order is None:
return None # Skip test if configs are not set
resp = api.create_order_v1(grvt_raw_types.ApiCreateOrderRequest(order=order))
if isinstance(resp, GrvtError):
raise ValueError(f"Received error: {resp}")
if resp.result is None:
raise ValueError("Expected order to be non-null")
def test_transfer_with_signing() -> None:
api = GrvtRawSync(config=get_config())
transfer = get_test_transfer(api)
if transfer is None:
return None # Skip test if configs are not set
resp = api.transfer_v1(
grvt_raw_types.ApiTransferRequest(
transfer.from_account_id,
transfer.from_sub_account_id,
transfer.to_account_id,
transfer.to_sub_account_id,
transfer.currency,
transfer.num_tokens,
transfer.signature,
grvt_raw_types.TransferType.STANDARD,
"",
)
)
if isinstance(resp, GrvtError):
raise ValueError(f"Received error: {resp}")
if resp.result is None:
raise ValueError("Expected transfer response to be non-null")
def test_withdrawal_with_signing() -> None:
api = GrvtRawSync(config=get_config())
withdrawal = get_test_withdrawal(api)
if withdrawal is None:
return None # Skip test if configs are not set
resp = api.withdrawal_v1(
grvt_raw_types.ApiWithdrawalRequest(
withdrawal.from_account_id,
withdrawal.to_eth_address,
withdrawal.currency,
withdrawal.num_tokens,
withdrawal.signature,
)
)
if isinstance(resp, GrvtError):
raise ValueError(f"Received error: {resp}")
if resp.result is None:
raise ValueError("Expected withdrawal response to be non-null")
@@ -1,158 +0,0 @@
import logging
import os
import random
import time
from pysdk import grvt_fixed_types, grvt_raw_types
from pysdk.grvt_raw_base import GrvtApiConfig, GrvtError
from pysdk.grvt_raw_env import GrvtEnv
from pysdk.grvt_raw_signing import sign_order, sign_transfer, sign_withdrawal
from pysdk.grvt_raw_sync import GrvtRawSync
def get_config() -> GrvtApiConfig:
logging.basicConfig()
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
conf = GrvtApiConfig(
env=GrvtEnv(os.getenv("GRVT_ENV", "testnet")),
trading_account_id=os.getenv("GRVT_SUB_ACCOUNT_ID"),
private_key=os.getenv("GRVT_PRIVATE_KEY"),
api_key=os.getenv("GRVT_API_KEY"),
logger=logger,
)
logger.debug(conf)
return conf
def get_main_account_id(api: GrvtRawSync) -> str:
resp = api.funding_account_summary_v1(grvt_raw_types.EmptyRequest())
if isinstance(resp, GrvtError):
raise ValueError(f"Received error: {resp}")
if resp.result is None:
raise ValueError("Expected funding_account_summary_v1 response to be non-null")
return resp.result.main_account_id
def get_test_order(
api: GrvtRawSync, instruments: dict[str, grvt_raw_types.Instrument]
) -> grvt_raw_types.Order | None:
# Skip test if configs are not set
if (
api.config.trading_account_id is None
or api.config.private_key is None
or api.config.api_key is None
):
return None
order = grvt_raw_types.Order(
sub_account_id=str(api.config.trading_account_id),
time_in_force=grvt_raw_types.TimeInForce.GOOD_TILL_TIME,
legs=[
grvt_raw_types.OrderLeg(
instrument="BTC_USDT_Perp",
size="1.2", # 1.2 BTC
limit_price="64170.7", # 80,000 USDT
is_buying_asset=True,
)
],
signature=grvt_raw_types.Signature(
signer="", # Populated by sign_order
r="", # Populated by sign_order
s="", # Populated by sign_order
v=0, # Populated by sign_order
expiration=str(
time.time_ns() + 20 * 24 * 60 * 60 * 1_000_000_000
), # 20 days
nonce=random.randint(0, 2**32 - 1),
),
metadata=grvt_raw_types.OrderMetadata(
client_order_id=str(random.randint(0, 2**32 - 1)),
),
)
return sign_order(order, api.config, api.account, instruments)
def get_test_tpsl_order(
api: GrvtRawSync, instruments: dict[str, grvt_raw_types.Instrument]
) -> grvt_raw_types.Order | None:
order = get_test_order(api, instruments)
if order:
order.metadata.trigger = grvt_raw_types.TriggerOrderMetadata(
trigger_type=grvt_raw_types.TriggerType.TAKE_PROFIT,
tpsl=grvt_raw_types.TPSLOrderMetadata(
trigger_by=grvt_raw_types.TriggerBy.LAST,
trigger_price="64000",
),
)
return order
def get_test_transfer(api: GrvtRawSync) -> grvt_fixed_types.Transfer | None:
# Skip test if configs are not set
if (
api.config.trading_account_id is None
or api.config.private_key is None
or api.config.api_key is None
):
return None
funding_account_address = get_main_account_id(api)
return sign_transfer(
grvt_fixed_types.Transfer(
from_account_id=funding_account_address,
from_sub_account_id="0",
to_account_id=funding_account_address,
to_sub_account_id=str(api.config.trading_account_id),
currency="USDT",
num_tokens="1",
signature=grvt_raw_types.Signature(
signer="",
r="",
s="",
v=0,
expiration=str(
time.time_ns() + 20 * 24 * 60 * 60 * 1_000_000_000
), # 20 days
nonce=random.randint(0, 2**32 - 1),
),
transfer_type=grvt_fixed_types.TransferType.STANDARD,
transfer_metadata="",
),
api.config,
api.account,
)
def get_test_withdrawal(api: GrvtRawSync) -> grvt_raw_types.Withdrawal | None:
# Skip test if configs are not set
if (
api.config.trading_account_id is None
or api.config.private_key is None
or api.config.api_key is None
):
return None
funding_account_address = get_main_account_id(api)
return sign_withdrawal(
grvt_raw_types.Withdrawal(
from_account_id=funding_account_address,
to_eth_address="0xed3FF6F4E84a64556e8F7d149dC3533f0c7D9c49", # Just a test address
currency="USDT",
num_tokens="1",
signature=grvt_raw_types.Signature(
signer="",
r="",
s="",
v=0,
expiration=str(
time.time_ns() + 20 * 24 * 60 * 60 * 1_000_000_000
), # 20 days
nonce=random.randint(0, 2**32 - 1),
),
),
api.config,
api.account,
)
@@ -1,4 +0,0 @@
from .test_grvt_raw_sync import test_transfer_with_signing
if __name__ == "__main__":
test_transfer_with_signing()
@@ -1,4 +0,0 @@
from .test_grvt_raw_sync import test_withdrawal_with_signing
if __name__ == "__main__":
test_withdrawal_with_signing()
-1961
View File
File diff suppressed because it is too large Load Diff
+104 -1
View File
@@ -2,6 +2,7 @@ import { setInterval, clearInterval } from "timers";
import { randomInt } from "crypto";
import axios, { AxiosHeaders } from "axios";
import { TDG, MDG } from "@grvt/client";
import { WS, EStream, type TWSRequest } from "@grvt/client/ws";
import { ECandlestickInterval } from "@grvt/client/interfaces/codegen/enums/candlestick-interval";
import { ECandlestickType } from "@grvt/client/interfaces/codegen/enums/candlestick-type";
import { ETimeInForce } from "@grvt/client/interfaces/codegen/enums/time-in-force";
@@ -217,10 +218,16 @@ export class GrvtGateway {
private accountSnapshot: AsterAccountSnapshot | null = null;
private openOrders: AsterOrder[] = [];
private positions: AsterAccountPosition[] = [];
private lastPositionsUpdateAt: number = 0;
private depthSnapshot: AsterDepth | null = null;
private tickerSnapshot: AsterTicker | null = null;
private klineCache: KlineCache | null = null;
// WebSocket state
private ws: WS | null = null;
private wsConnected = false;
private positionsRefreshTimer: ReturnType<typeof setTimeout> | null = null;
private accountListeners = new Set<(snapshot: AsterAccountSnapshot) => void>();
private ordersListeners = new Set<(orders: AsterOrder[]) => void>();
private depthListeners = new Set<(depth: AsterDepth) => void>();
@@ -305,6 +312,7 @@ export class GrvtGateway {
this.refreshKlines(),
]);
this.startPolling();
this.startWebsocket();
this.initialized = true;
}
@@ -417,6 +425,9 @@ export class GrvtGateway {
const response = await this.tdg.createOrder({ order: toApiOrderPayload(signedOrder) });
const order = mapCreateOrderResponse(response, this.symbol);
this.mergeOrder(order);
if (signedOrder.reduce_only) {
void this.refreshPositions().catch((error) => this.logger("postOrderPositionRefresh", error));
}
return order;
} catch (error) {
const payload = {
@@ -500,6 +511,87 @@ export class GrvtGateway {
}
}
private startWebsocket(): void {
try {
if (this.ws) {
this.ws.disconnect();
this.ws = null;
}
const base = this.hosts.trades.replace(/\/?$/g, "");
const url = new URL(base.replace(/^http/, "ws") + "/ws/lite");
// Pass auth as query params (server reads headers/cookies from params per client implementation)
const params: Record<string, string> = {};
for (const [key, value] of Object.entries(this.headers)) {
params[key] = value;
}
const ws = new WS({
url: url.toString(),
timeout: 10000,
reconnectStrategy: (retries: number) => {
const attempt = Math.max(0, retries);
const base = 1000 * Math.pow(2, Math.min(8, attempt));
const jitter = Math.floor(Math.random() * 250);
return Math.min(30000, base + jitter);
},
});
ws.setParams(params);
ws.onConnect(() => {
this.wsConnected = true;
// Prefer WS: pause HTTP position polling when connected
if (this.accountTimer) {
clearInterval(this.accountTimer);
this.accountTimer = null;
}
// Subscribe to position/order/state feeds
const subs: TWSRequest[] = [
{
stream: EStream.POSITION,
params: { sub_account_id: this.subAccountId, instrument: this.instrument },
onData: () => this.schedulePositionsRefresh(150),
} as any,
{
stream: EStream.ORDER,
params: { sub_account_id: this.subAccountId, instrument: this.instrument },
onData: () => this.schedulePositionsRefresh(100),
} as any,
{
stream: EStream.STATE,
params: { sub_account_id: this.subAccountId, instrument: this.instrument },
onData: () => this.schedulePositionsRefresh(100),
} as any,
];
ws.subscribes(...subs);
});
ws.onClose(() => {
this.wsConnected = false;
// Resume HTTP position polling when WS closes
if (!this.accountTimer) {
this.accountTimer = setInterval(() => {
void this.refreshAccountSnapshot().catch((error) => this.logger("accountPoll", error));
void this.refreshPositions().catch((error) => this.logger("positionPoll", error));
}, this.pollIntervals.account);
}
});
ws.onError((e) => {
this.logger("ws", e);
});
ws.connect();
this.ws = ws;
} catch (error) {
this.logger("startWebsocket", error);
}
}
private schedulePositionsRefresh(delayMs: number): void {
if (this.positionsRefreshTimer) {
clearTimeout(this.positionsRefreshTimer);
}
this.positionsRefreshTimer = setTimeout(() => {
this.positionsRefreshTimer = null;
void this.refreshPositions().catch((error) => this.logger("wsPositionsRefresh", error));
}, Math.max(0, delayMs));
}
private async refreshAccountSnapshot(): Promise<void> {
const response = await this.tdg.subAccountSummary({ sub_account_id: this.subAccountId });
const snapshot = mapAccountSnapshot(response, this.symbol, this.instrument);
@@ -509,7 +601,13 @@ export class GrvtGateway {
private async refreshPositions(): Promise<void> {
const response = await this.tdg.positions({ sub_account_id: this.subAccountId });
this.positions = mapPositions(response, this.symbol, this.instrument);
const mapped = mapPositions(response, this.symbol, this.instrument);
const newestEventTime = getNewestPositionEventTime(mapped);
if (newestEventTime < this.lastPositionsUpdateAt) {
return;
}
this.positions = mapped;
this.lastPositionsUpdateAt = newestEventTime;
const snapshot = this.accountSnapshot;
if (snapshot) {
snapshot.positions = this.positions.map((position) => ({ ...position }));
@@ -929,6 +1027,11 @@ function sumUnrealized(positions: AsterAccountPosition[]): number {
return positions.reduce((total, position) => total + Number(position.unrealizedProfit ?? 0), 0);
}
function getNewestPositionEventTime(positions: AsterAccountPosition[]): number {
if (!positions.length) return Date.now();
return positions.reduce((max, p) => Math.max(max, Number(p.updateTime) || 0), 0);
}
function mapAccountSnapshot(
response: IApiSubAccountSummaryResponse,
symbol: string,